Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 33 additions & 30 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -1,44 +1,47 @@
name: Playwright Tests

on:
push:
branches: [ main, master ]
branches:
- main
- release-*
pull_request:
branches: [ main, master ]
branches:
- main
- release-*

jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
defaults:
run:
working-directory: gcs

steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Install dependencies
working-directory: gcs
run: npm install -g yarn && yarn

- name: Install Playwright Browsers
working-directory: gcs
run: yarn playwright install --with-deps

- name: Run FGCS frontend
run: yarn dev:test &

- name: Run Playwright tests
uses: coactions/setup-xvfb@v1
with:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Install dependencies
working-directory: gcs
run: npm install -g yarn && yarn

- name: Install Playwright Browsers
working-directory: gcs
run: yarn playwright install --with-deps

- name: Run FGCS frontend
run: yarn dev:test &

- name: Run Playwright tests
run: yarn playwright test
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30

- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
28 changes: 17 additions & 11 deletions gcs/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default defineConfig({
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
baseURL: 'http://127.0.0.1:5173',

/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
Expand All @@ -36,18 +36,23 @@ export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
use: {
...devices['Desktop Chrome'],
// Use system Chrome browser in local dev, fall back to Chromium in CI
...(process.env.CI ? {} : { channel: 'chrome' })
},
},

{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
// Disable other browsers for now to focus on Chrome
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },

{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },

/* Test against mobile viewports. */
// {
Expand All @@ -72,8 +77,9 @@ export default defineConfig({

/* Run your local dev server before starting the tests */
// webServer: {
// command: 'yarn dev',
// command: 'yarn dev:test &',
// url: 'http://127.0.0.1:5173',
// reuseExistingServer: !process.env.CI,
// timeout: 120 * 1000,
// },
});
7 changes: 0 additions & 7 deletions gcs/tests/dashboard.spec.ts

This file was deleted.

201 changes: 201 additions & 0 deletions gcs/tests/error-handling.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { test, expect } from '@playwright/test';

test.describe('GCS Error Handling and Application Stability Tests', () => {
test('should load application without critical JavaScript errors', async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});

await page.goto('/');

// Wait for the application to load
await page.waitForLoadState('networkidle');
await expect(page.locator('#root')).toBeVisible();

// Filter out known/acceptable errors (if any)
const criticalErrors = consoleErrors.filter(error =>
!error.includes('Failed to load resource') && // Network errors are expected when no server
!error.includes('WebSocket') && // WebSocket connection errors are expected
!error.includes('favicon') // Favicon errors are not critical
);

// Should not have critical JavaScript errors
expect(criticalErrors.length).toBe(0);
});

test('should display proper disconnection messages when no drone is connected', async ({ page }) => {
// Test each page that should show disconnection messages
const pagesWithDisconnectionMessages = [
{ path: '/#/config', expectedMessage: 'Not connected to drone. Please connect to view config' },
{ path: '/#/missions', expectedMessage: 'Not connected to drone. Please connect to view missions' },
{ path: '/#/graphs', expectedMessage: 'Not connected to drone. Please connect to view graphs' },
{ path: '/#/params', expectedMessage: 'Not connected to drone. Please connect to view params' }
];

for (const pageInfo of pagesWithDisconnectionMessages) {
await page.goto(pageInfo.path);
await expect(page.locator('#root')).toBeVisible();

// Look for the specific full disconnection message
const disconnectionMessage = page.locator(`text=${pageInfo.expectedMessage}`);
await expect(disconnectionMessage).toBeVisible();
}
});

test('should handle navigation errors gracefully without breaking application structure', async ({ page }) => {
const consoleErrors: string[] = [];
page.on('console', (msg) => {
if (msg.type() === 'error') {
consoleErrors.push(msg.text());
}
});

// Test navigation to all main pages
const pages = ['/', '/#/config', '/#/missions', '/#/graphs', '/#/params', '/#/fla'];

for (const pageUrl of pages) {
await page.goto(pageUrl);
await expect(page.locator('#root')).toBeVisible();

// Check for error boundaries or critical UI failures
const errorBoundary = page.locator('[data-testid="error-boundary"], .error-boundary');
if (await errorBoundary.count() > 0) {
await expect(errorBoundary.first()).not.toBeVisible();
}

// Verify the application's core layout components are still present
const layout = page.locator('nav, [data-testid="navbar"], .navbar');
if (await layout.count() > 0) {
await expect(layout.first()).toBeVisible();
}
}

// Filter critical errors
const criticalErrors = consoleErrors.filter(error =>
!error.includes('Failed to load resource') &&
!error.includes('WebSocket') &&
!error.includes('favicon')
);

expect(criticalErrors.length).toBe(0);
});

test('should handle page refresh without breaking application functionality', async ({ page }) => {
// Go to dashboard
await page.goto('/');
await expect(page.locator('#root')).toBeVisible();

// Refresh the page and verify core elements are still present
await page.reload();
await expect(page.locator('#root')).toBeVisible();

// Go to a different page and refresh
await page.goto('/#/config');
await expect(page.locator('#root')).toBeVisible();

await page.reload();
await expect(page.locator('#root')).toBeVisible();

// After refresh, should show disconnection message
const configMessage = page.locator('text=Not connected to drone. Please connect to view config');
await expect(configMessage).toBeVisible();
});

test('should handle browser back/forward navigation without errors', async ({ page }) => {
// Navigate through several pages
await page.goto('/');
await expect(page.locator('#root')).toBeVisible();

await page.goto('/#/config');
await expect(page.locator('#root')).toBeVisible();

await page.goto('/#/missions');
await expect(page.locator('#root')).toBeVisible();

// Use browser back button
await page.goBack();
await expect(page.locator('#root')).toBeVisible();

await page.goBack();
await expect(page.locator('#root')).toBeVisible();

// Use browser forward button
await page.goForward();
await expect(page.locator('#root')).toBeVisible();

await page.goForward();
await expect(page.locator('#root')).toBeVisible();
});

test('should maintain application stability during rapid navigation cycles', async ({ page }) => {
// Start at dashboard
await page.goto('/');
await expect(page.locator('#root')).toBeVisible();

// Navigate to different pages rapidly
const pages = ['/#/config', '/#/missions', '/#/graphs', '/#/params', '/#/fla', '/'];

for (let i = 0; i < 3; i++) { // Do this cycle multiple times
for (const pageUrl of pages) {
await page.goto(pageUrl);
await expect(page.locator('#root')).toBeVisible();

// Verify basic application structure remains intact
const appContainer = page.locator('#root');
await expect(appContainer).toBeVisible();

// Ensure no critical error messages appear
const criticalErrorMessages = page.locator('text=/error|exception|failed|crash/i');
const visibleErrors = await criticalErrorMessages.filter({ hasText: /critical|fatal|crash/i }).count();
expect(visibleErrors).toBe(0);
}
}
});

test('should handle invalid routes gracefully and allow recovery', async ({ page }) => {
// Try to navigate to a non-existent route
await page.goto('/#/invalid-route');

// Should still show the app container (React Router should handle this)
await expect(page.locator('#root')).toBeVisible();

// Try another invalid route
await page.goto('/#/does-not-exist');
await expect(page.locator('#root')).toBeVisible();

// Should be able to navigate back to valid routes
await page.goto('/');
await expect(page.locator('#root')).toBeVisible();

// Verify we can still navigate to other valid routes after invalid ones
await page.goto('/#/config');
await expect(page.locator('#root')).toBeVisible();
});

test('should handle window resize without breaking layout', async ({ page }) => {
await page.goto('/');
await expect(page.locator('#root')).toBeVisible();

// Test different viewport sizes
const viewports = [
{ width: 1920, height: 1080 },
{ width: 1280, height: 720 },
{ width: 800, height: 600 }
];

for (const viewport of viewports) {
await page.setViewportSize(viewport);
await page.waitForTimeout(500); // Allow layout to adjust

// Verify the app is still visible and functional
await expect(page.locator('#root')).toBeVisible();

// Test navigation still works at different screen sizes
await page.goto('/#/config');
await expect(page.locator('#root')).toBeVisible();
}
});
});
18 changes: 0 additions & 18 deletions gcs/tests/example.spec.ts

This file was deleted.

Loading
Loading