Jarian Cottingham 80ff136c31 feat: add Playwright E2E tests to CI
- 15 tests: home, listing, detail, nav, status, RSS/Atom, theme toggle
- CI e2e job seeds test DB (80 articles, 3 sources) in Docker container
- Tests run in mcr.microsoft.com/playwright:v1.51.0-jammy image
- Port 5000 published for health check from runner
- node_modules + test-results excluded from git
2026-07-07 22:45:52 +00:00

129 lines
4.5 KiB
TypeScript

import { test, expect } from '@playwright/test';
const TEST_SOURCE = 'Test News';
test.describe('Home page', () => {
test('loads and shows sources', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveTitle('NewsArchiver');
await expect(page.locator('h1').filter({ hasText: 'News Archives' })).toBeVisible();
await expect(page.locator('.newspaper-list')).toBeVisible();
});
test('shows source count', async ({ page }) => {
await page.goto('/');
const count = await page.locator('p:has-text("Total sources:")').textContent();
expect(count || '').toMatch(/Total sources: \d+/);
});
test('navigates to a source', async ({ page }) => {
await page.goto('/');
const firstLink = page.locator('.newspaper-item h2 a').first();
const sourceName = (await firstLink.textContent())?.trim() || '';
await firstLink.click();
await expect(page).toHaveURL(/\/source\//);
await expect(page.locator('h1')).toContainText(sourceName);
});
});
test.describe('Article listing', () => {
test('shows articles for a source', async ({ page }) => {
await page.goto(`/source/${TEST_SOURCE.toLowerCase()}`);
await expect(page.locator('.article-list')).toBeVisible();
const articles = page.locator('.article-item');
await expect.poll(() => articles.count()).toBeGreaterThan(0);
});
test('article links navigate to detail', async ({ page }) => {
await page.goto(`/source/${TEST_SOURCE.toLowerCase()}`);
const firstArticle = page.locator('.article-item h3 a').first();
const title = (await firstArticle.textContent())?.trim() || '';
await firstArticle.click();
await expect(page).toHaveURL(/\/article\/\d+/);
await expect(page.locator('.article-view h1')).toContainText(title);
});
test('shows article metadata', async ({ page }) => {
await page.goto(`/source/${TEST_SOURCE.toLowerCase()}/article/1`);
await expect(page.locator('.article-source')).toBeVisible();
await expect(page.locator('.article-meta')).toBeVisible();
});
test('back link returns to listing', async ({ page }) => {
await page.goto(`/source/${TEST_SOURCE.toLowerCase()}/article/1`);
await page.locator('.back-link').click();
await expect(page).toHaveURL(new RegExp(`/source/${TEST_SOURCE.toLowerCase()}`));
await expect(page.locator('.article-list')).toBeVisible();
});
});
test.describe('Pagination', () => {
test('shows page info', async ({ page }) => {
await page.goto(`/source/${TEST_SOURCE.toLowerCase()}`);
await expect(page.locator('.pagination span')).toContainText('Page 1');
});
});
test.describe('Navigation', () => {
test('header nav links work', async ({ page }) => {
await page.goto('/');
await page.locator('nav a:has-text("Archives")').click();
await expect(page).toHaveURL(/\/$/);
await page.locator('nav a:has-text("Status")').click();
await expect(page).toHaveURL(/\/status/);
});
});
test.describe('Status page', () => {
test('shows system status', async ({ page }) => {
await page.goto('/status');
await expect(page.locator('h1')).toBeVisible();
});
test('shows total articles count', async ({ page }) => {
await page.goto('/status');
await expect(page.locator('text=/Total Articles: \d+/')).toBeVisible();
});
});
test.describe('RSS feed', () => {
test('serves RSS XML', async ({ request }) => {
const resp = await request.get('/rss');
expect(resp.ok()).toBeTruthy();
expect(resp.headers()['content-type']).toContain('application/rss+xml');
const body = await resp.text();
expect(body).toContain('<rss');
expect(body).toContain('<channel>');
});
});
test.describe('Atom feed', () => {
test('serves Atom XML', async ({ request }) => {
const resp = await request.get('/atom');
expect(resp.ok()).toBeTruthy();
expect(resp.headers()['content-type']).toContain('application/atom+xml');
const body = await resp.text();
expect(body).toContain('<feed');
});
});
test.describe('Theme toggle', () => {
test('toggles dark mode', async ({ page }) => {
await page.goto('/');
const html = page.locator('html');
await page.locator('#theme-toggle').click();
await expect(html).toHaveAttribute('data-theme', 'dark');
await page.locator('#theme-toggle').click();
await expect(html).toHaveAttribute('data-theme', 'light');
});
test('persists theme in localStorage', async ({ page }) => {
await page.goto('/');
await page.locator('#theme-toggle').click();
const theme = await page.evaluate(() => localStorage.getItem('theme'));
expect(theme).toBe('dark');
});
});