Skip to content
Merged
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
57 changes: 57 additions & 0 deletions frontend/e2e/items.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { test, expect } from '@playwright/test';

test.describe('Items Page', () => {
test('renders the page heading', async ({ page }) => {
await page.goto('/items');

await expect(page.getByRole('heading', { level: 1 })).toHaveText('Items', {
timeout: 10_000,
});
});

test('displays the items table with data from the API', async ({ page }) => {
await page.goto('/items');

// Wait for loading spinner to disappear, indicating data has loaded
await expect(page.getByRole('progressbar')).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('progressbar')).not.toBeVisible({ timeout: 10_000 });

// Table should be visible with expected column headers
await expect(page.getByRole('table')).toBeVisible({ timeout: 10_000 });
await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'Price' })).toBeVisible();
});

test('navigates to Items page via nav link and back', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Welcome to the Full Stack Application',
{ timeout: 10_000 }
);

await page.getByRole('link', { name: /items/i }).click();
await expect(page).toHaveURL('/items');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Items', {
timeout: 10_000,
});

await page.getByRole('link', { name: /home/i }).click();
await expect(page).toHaveURL('/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Welcome to the Full Stack Application'
);
});

test('renders table column headers', async ({ page }) => {
await page.goto('/items');

await expect(page.getByRole('columnheader', { name: 'ID' })).toBeVisible({
timeout: 10_000,
});
await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'Price' })).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'Description' })).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'Created At' })).toBeVisible();
await expect(page.getByRole('columnheader', { name: 'Updated At' })).toBeVisible();
});
});
14 changes: 14 additions & 0 deletions frontend/e2e/navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ test.describe('Navigation', () => {
await expect(page.getByText('Full Stack App', { exact: true })).toBeVisible();
await expect(page.getByRole('link', { name: /home/i })).toBeVisible();
await expect(page.getByRole('link', { name: /health/i })).toBeVisible();
await expect(page.getByRole('link', { name: /items/i })).toBeVisible();
});

test('navigates from Home to Items via nav link', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Welcome to the Full Stack Application'
);

await page.getByRole('link', { name: /items/i }).click();
await expect(page).toHaveURL('/items');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('Items', {
timeout: 10_000,
});
});

test('renders the footer with current year', async ({ page }) => {
Expand Down
79 changes: 79 additions & 0 deletions frontend/e2e/websocket.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { test, expect } from '@playwright/test';

/**
* WebSocket integration e2e tests.
*
* These tests verify that the app remains fully functional regardless of the
* WebSocket connection state. The WebSocket provider connects in the background,
* so we assert on observable UI behaviour rather than on the socket itself.
*/
test.describe('WebSocket Integration', () => {
test('app loads and renders correctly with WebSocket running in background', async ({ page }) => {
const wsErrors: string[] = [];
page.on('pageerror', (error) => {
if (
error.message.toLowerCase().includes('websocket') ||
error.message.includes('ws://')
) {
wsErrors.push(error.message);
}
});

await page.goto('/');

await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Welcome to the Full Stack Application',
{ timeout: 10_000 }
);

// No uncaught WebSocket-related JS errors should surface to the page
expect(wsErrors).toHaveLength(0);
});

test('navigation works correctly while WebSocket is active', async ({ page }) => {
await page.goto('/');
await expect(
page.getByRole('heading', { level: 1 })
).toHaveText('Welcome to the Full Stack Application', { timeout: 10_000 });

// Navigate away and back — the WebSocket provider should survive route changes
await page.getByRole('link', { name: /health/i }).click();
await expect(page).toHaveURL('/health');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'System Health',
{ timeout: 10_000 }
);

await page.getByRole('link', { name: /home/i }).click();
await expect(page).toHaveURL('/');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'Welcome to the Full Stack Application'
);
});

test('app recovers gracefully when WebSocket connection is interrupted', async ({
page,
context,
}) => {
await page.goto('/');
await expect(
page.getByRole('heading', { level: 1 })
).toHaveText('Welcome to the Full Stack Application', { timeout: 10_000 });

// Simulate a network interruption by going offline then online
await context.setOffline(true);
await context.setOffline(false);

// App UI must still be functional after the interruption
await expect(
page.getByRole('link', { name: /health/i })
).toBeVisible({ timeout: 10_000 });

await page.getByRole('link', { name: /health/i }).click();
await expect(page).toHaveURL('/health');
await expect(page.getByRole('heading', { level: 1 })).toHaveText(
'System Health',
{ timeout: 10_000 }
);
});
});
7 changes: 7 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-router-dom": "^7.6.1",
"reconnecting-websocket": "^4.4.0",
"typescript": "^5.8.3",
"vite": "^6.3.5"
},
Expand Down
9 changes: 6 additions & 3 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
import { BrowserRouter } from 'react-router-dom';
import AppRoutes from './routes';
import Layout from './components/Layout';
import { WebSocketProvider } from './context/WebSocketContext';

const theme = createTheme({
palette: {
Expand All @@ -20,9 +21,11 @@ function App() {
<ThemeProvider theme={theme}>
<CssBaseline />
<BrowserRouter>
<Layout>
<AppRoutes />
</Layout>
<WebSocketProvider>
<Layout>
<AppRoutes />
</Layout>
</WebSocketProvider>
</BrowserRouter>
</ThemeProvider>
);
Expand Down
13 changes: 12 additions & 1 deletion frontend/src/__tests__/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@ import { describe, it, expect, vi } from 'vitest';
import { render, screen } from '@testing-library/react';
import App from '../App';

// Mock the health service to prevent actual API calls during render
// Prevent WebSocketProvider from opening a real connection in jsdom
vi.mock('reconnecting-websocket', () => ({
default: class MockRWS extends EventTarget {
close() {}
},
}));

// Mock the health and item services to prevent actual API calls during render
vi.mock('../api/client', () => ({
healthService: {
checkLiveness: vi.fn().mockReturnValue(new Promise(() => {})),
checkReadiness: vi.fn().mockReturnValue(new Promise(() => {})),
},
itemService: {
list: vi.fn().mockReturnValue(new Promise(() => {})),
},
}));

describe('App', () => {
Expand All @@ -16,6 +26,7 @@ describe('App', () => {
expect(screen.getByText('Full Stack App')).toBeInTheDocument();
expect(screen.getByRole('link', { name: /home/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /health/i })).toBeInTheDocument();
expect(screen.getByRole('link', { name: /items/i })).toBeInTheDocument();
});

it('renders the Home page by default', () => {
Expand Down
Loading
Loading