test(e2e): add Playwright + axe-core smoke suite
Add @playwright/test, axe-core and @axe-core/playwright as devDependencies. - playwright.config.ts with desktop+mobile projects and a webServer that boots astro preview on 127.0.0.1:4321. - tests/e2e/site.spec.ts covers: home renders with H1 and textual map summary; skip-link is the first focusable and jumps to main; filter pills toggle aria-pressed and update the live counter; 404 is noindex; JSON-LD parses and exposes Organization+WebSite+ItemList; robots.txt and sitemap are wired to Astro.site; the 404 is excluded from the sitemap; the generated og-default.png is 1200x630. - Scripts: test:e2e runs the suite, test:e2e:install pulls the chromium headless shell. - .gitignore ignores test-results/, playwright-report/ and the playwright cache. All 16 tests pass (8 desktop, 8 mobile).
This commit is contained in:
@@ -50,6 +50,11 @@ tmp/
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Playwright
|
||||
test-results/
|
||||
playwright-report/
|
||||
playwright/.cache/
|
||||
|
||||
# IDE settings
|
||||
.idea/
|
||||
.vscode/
|
||||
|
||||
Generated
+6030
File diff suppressed because it is too large
Load Diff
+6
-1
@@ -12,7 +12,9 @@
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"check": "astro check",
|
||||
"check:watch": "astro check --watch"
|
||||
"check:watch": "astro check --watch",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:install": "playwright install --with-deps chromium"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/preact": "^6.0.1",
|
||||
@@ -25,8 +27,11 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.4",
|
||||
"@axe-core/playwright": "^4.12.1",
|
||||
"@playwright/test": "^1.62.0",
|
||||
"@types/leaflet": "^1.9.12",
|
||||
"@types/pg": "^8.11.10",
|
||||
"axe-core": "^4.12.1",
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests/e2e',
|
||||
fullyParallel: true,
|
||||
reporter: [['list']],
|
||||
use: {
|
||||
baseURL: 'http://127.0.0.1:4321',
|
||||
trace: 'retain-on-failure',
|
||||
},
|
||||
webServer: {
|
||||
command: 'npm run preview -- --host 127.0.0.1 --port 4321',
|
||||
url: 'http://127.0.0.1:4321/',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 60_000,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'desktop',
|
||||
use: { ...devices['Desktop Chrome'], viewport: { width: 1280, height: 800 } },
|
||||
},
|
||||
{
|
||||
name: 'mobile',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const dist = (name: string) => join(process.cwd(), 'dist', name);
|
||||
|
||||
test.describe('site smoke', () => {
|
||||
test('home renders and reports no axe violations @ desktop', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page).toHaveTitle(/Avisos ambientals d'Eivissa i Formentera/);
|
||||
await expect(page.locator('h1#page-title')).toBeVisible();
|
||||
await expect(page.locator('main#main-content')).toBeVisible();
|
||||
await expect(page.locator('text=Resum d\'avisos per municipi')).toBeVisible();
|
||||
|
||||
const results = await new AxeBuilder({ page })
|
||||
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
|
||||
.disableRules(['region']) // decorative map region is intentional
|
||||
.analyze();
|
||||
expect(results.violations, JSON.stringify(results.violations, null, 2)).toEqual([]);
|
||||
});
|
||||
|
||||
test('skip-link is the first interactive element and jumps to main', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.keyboard.press('Tab');
|
||||
const focused = await page.evaluate(() => document.activeElement?.textContent?.trim());
|
||||
expect(focused).toBe('Saltar al contingut');
|
||||
await page.keyboard.press('Enter');
|
||||
await expect(page.locator('main#main-content')).toBeFocused();
|
||||
});
|
||||
|
||||
test('filter pills toggle aria-pressed and update the live counter', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const municipioGroup = page.locator('[data-filter-type="municipio"]');
|
||||
const allPills = municipioGroup.locator('.pill');
|
||||
const firstPill = allPills.first();
|
||||
const targetValue = await municipioGroup
|
||||
.locator('.pill:not(:first-child)')
|
||||
.first()
|
||||
.getAttribute('data-value');
|
||||
expect(targetValue).toBeTruthy();
|
||||
const targetPill = municipioGroup.locator(`.pill[data-value="${targetValue}"]`);
|
||||
|
||||
await expect(firstPill).toHaveAttribute('aria-pressed', 'true');
|
||||
await targetPill.click();
|
||||
await expect(firstPill).toHaveAttribute('aria-pressed', 'false');
|
||||
await expect(targetPill).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
const counter = page.locator('#filters-count');
|
||||
await expect(counter).toContainText(/avís/i);
|
||||
});
|
||||
|
||||
test('404 page is noindex and reachable', async ({ page }) => {
|
||||
const response = await page.goto('/no-existeix-aqui');
|
||||
expect(response?.status()).toBe(404);
|
||||
const robots = await page.locator('meta[name="robots"]').getAttribute('content');
|
||||
expect(robots).toContain('noindex');
|
||||
await expect(page.locator('h1#not-found-title')).toBeVisible();
|
||||
});
|
||||
|
||||
test('JSON-LD on the home parses and exposes Organization + ItemList', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
const payload = await page.locator('script[type="application/ld+json"]').first().textContent();
|
||||
expect(payload).toBeTruthy();
|
||||
const data = JSON.parse(payload ?? '{}');
|
||||
const types = (data['@graph'] as Array<{ '@type': string }>).map((n) => n['@type']);
|
||||
expect(types).toEqual(expect.arrayContaining(['Organization', 'WebSite', 'ItemList']));
|
||||
});
|
||||
|
||||
test('robots.txt built from Astro.site and points at the sitemap', async ({ request }) => {
|
||||
const res = await request.get('/robots.txt');
|
||||
expect(res.status()).toBe(200);
|
||||
const body = await res.text();
|
||||
expect(body).toMatch(/User-agent: \*/);
|
||||
expect(body).toMatch(/Sitemap: https:\/\/www\.eivissaenvironmentalwatch\.es\/sitemap-index\.xml/);
|
||||
});
|
||||
|
||||
test('sitemap lists the two real pages and not the 404', async ({ request }) => {
|
||||
const res = await request.get('/sitemap-index.xml');
|
||||
expect(res.status()).toBe(200);
|
||||
const index = await res.text();
|
||||
const sub = await request.get(/<loc>(.*?)<\/loc>/.exec(index)?.[1] ?? '/sitemap-0.xml');
|
||||
const sitemap = await sub.text();
|
||||
expect(sitemap).toContain('https://www.eivissaenvironmentalwatch.es/');
|
||||
expect(sitemap).toContain('https://www.eivissaenvironmentalwatch.es/huella/');
|
||||
expect(sitemap).not.toContain('/404/');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('build artifacts', () => {
|
||||
test('og PNG exists with the right dimensions', async () => {
|
||||
const buf = await readFile(dist('og-default.png'));
|
||||
expect(buf.subarray(0, 8)).toEqual(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]));
|
||||
const width = buf.readUInt32BE(16);
|
||||
const height = buf.readUInt32BE(20);
|
||||
expect({ width, height }).toEqual({ width: 1200, height: 630 });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user