mirror of
https://github.com/jcreek/LivingDexTracker.git
synced 2026-09-16 02:22:17 +00:00
ff29095c47
- Ship one hashed Tailwind stylesheet instead of two (one render-blocking) - Compress responses in-app (brotli/gzip, streaming-safe) and precompress the node build so local and CI measurements match production - Validate the Supabase session once per request - Render the homepage immediately and stream public stats - Stream a Pokedex's entries with the page instead of fetching after hydration, running the rows and count queries in parallel - Shrink the avatar and offline placeholder images, fix layout shift, contrast, link names and missing meta descriptions - Add Lighthouse CI (mobile + desktop) with score and metric budgets, bundle-size budgets in the build tests, and signed-in speed scenarios
78 lines
3.0 KiB
TypeScript
78 lines
3.0 KiB
TypeScript
import { createBdd } from 'playwright-bdd';
|
|
import type { Page } from '@playwright/test';
|
|
import { test, expect } from '../fixtures';
|
|
|
|
const { When, Then } = createBdd(test);
|
|
|
|
// Per-page scratch values; scenarios run one at a time (workers: 1).
|
|
const timings = new WeakMap<
|
|
Page,
|
|
{ entriesMs?: number; switchMs: number[]; entryRequests: number }
|
|
>();
|
|
|
|
function entriesVisible(page: Page) {
|
|
return expect(page.getByRole('button', { name: /^View details for / }).first()).toBeVisible({
|
|
timeout: 30_000
|
|
});
|
|
}
|
|
|
|
When('I load the Pokédex page directly', async ({ page, state }) => {
|
|
if (!state.pokedexId) throw new Error('A Pokédex must exist before it can be opened');
|
|
const record = { switchMs: [], entryRequests: 0 };
|
|
timings.set(page, record);
|
|
// Only requests made while the page first loads matter; the page's 60s reconciliation refetch
|
|
// can't fire within this window.
|
|
const countEntryRequests = (request: { url(): string }) => {
|
|
if (/\/api\/pokedexes\/[^/]+\/combined-data/.test(request.url())) record.entryRequests++;
|
|
};
|
|
page.on('request', countEntryRequests);
|
|
|
|
const started = Date.now();
|
|
await page.goto(`/pokedex/${state.pokedexId}`);
|
|
await entriesVisible(page);
|
|
(record as { entriesMs?: number }).entriesMs = Date.now() - started;
|
|
page.off('request', countEntryRequests);
|
|
});
|
|
|
|
Then('its entries appear within {int} seconds', async ({ page }, seconds: number) => {
|
|
const entriesMs = timings.get(page)?.entriesMs;
|
|
expect(entriesMs, 'entries never became visible').toBeDefined();
|
|
expect(entriesMs!).toBeLessThan(seconds * 1000);
|
|
});
|
|
|
|
Then('the browser did not request the entries separately', async ({ page }) => {
|
|
// The server load streams the first page of entries with the HTML, so the page must not make
|
|
// the old hydrate-then-fetch round trip.
|
|
expect(timings.get(page)?.entryRequests).toBe(0);
|
|
});
|
|
|
|
When('I switch between my Pokédex list and the Pokédex', async ({ page, state }) => {
|
|
if (!state.pokedexName) throw new Error('A Pokédex must exist before switching to it');
|
|
const record = { switchMs: [] as number[], entryRequests: 0 };
|
|
timings.set(page, record);
|
|
await page.goto('/my-pokedexes');
|
|
const card = page.locator('.card').filter({ hasText: state.pokedexName }).first();
|
|
await expect(card).toBeVisible();
|
|
|
|
for (let round = 0; round < 2; round++) {
|
|
// List -> Pokédex: a client-side navigation through the card's View button.
|
|
let started = Date.now();
|
|
await card.getByRole('button', { name: 'View', exact: true }).click();
|
|
await page.waitForURL('**/pokedex/**');
|
|
await entriesVisible(page);
|
|
record.switchMs.push(Date.now() - started);
|
|
|
|
// Pokédex -> list: back navigation is also handled by the client router.
|
|
started = Date.now();
|
|
await page.goBack();
|
|
await expect(card).toBeVisible();
|
|
record.switchMs.push(Date.now() - started);
|
|
}
|
|
});
|
|
|
|
Then('each switch finishes within {int} seconds', async ({ page }, seconds: number) => {
|
|
const switchMs = timings.get(page)?.switchMs ?? [];
|
|
expect(switchMs).toHaveLength(4);
|
|
for (const ms of switchMs) expect(ms).toBeLessThan(seconds * 1000);
|
|
});
|