- 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
129 lines
4.5 KiB
TypeScript
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');
|
|
});
|
|
});
|