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
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,29 @@ jobs:
working-directory: app
- run: npm run lint
working-directory: app

e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 20
cache: npm
cache-dependency-path: app/package-lock.json
- run: npm ci
working-directory: app
- run: npx playwright install --with-deps chromium
working-directory: app
# Build and serve a production bundle, not `next dev`: several past
# regressions (e.g. searchParams 500s) only reproduce in prod rendering.
- run: npm run build
working-directory: app
# `@perf`-tagged specs (Web Vitals thresholds, cold-start journey) are
# timing-sensitive / require a deployed URL, so they are not gated here.
- name: Run e2e against production build
working-directory: app
run: |
npm start &
npx --yes wait-on --timeout 60000 http://localhost:3000
BASE_URL=http://localhost:3000 npx playwright test --grep-invert @perf
2 changes: 1 addition & 1 deletion app/e2e/journey-performance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const SURNAMES = [
const baseURL = process.env.BASE_URL ?? "";
const isRemote = /^https?:\/\//.test(baseURL) && !/localhost|127\.0\.0\.1/.test(baseURL);

test.describe("home → search → athlete journey (cold path)", () => {
test.describe("home → search → athlete journey (cold path) @perf", () => {
// A retry would warm the function and hide a cold-path regression.
test.describe.configure({ retries: 0 });

Expand Down
2 changes: 1 addition & 1 deletion app/e2e/performance.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const pages = [
];

for (const { name, path } of pages) {
test(`${name} page (${path}) loads with acceptable Web Vitals`, async ({
test(`${name} page (${path}) loads with acceptable Web Vitals @perf`, async ({
page,
}) => {
// Inject web-vitals collection before navigating
Expand Down
4 changes: 2 additions & 2 deletions app/e2e/result-share-and-pills.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ test("result page shows AG percentile pills on each discipline card", async ({ p
await page.waitForURL(new RegExp(`/race/${RACE_SLUG}/result/\\d+`));

// Each of the four discipline cards should have an amber percentile pill.
// Pills render either "<N>%" or "—".
// Pills render either "Top <N>%" (see formatTopPercent) or "—".
const pills = page.locator(".bg-amber-400\\/15");
await expect(pills).toHaveCount(4);
for (let i = 0; i < 4; i++) {
const text = (await pills.nth(i).innerText()).trim();
expect(text).toMatch(/^(—|\d{1,3}%)$/);
expect(text).toMatch(/^(—|Top \d{1,3}%)$/);
}
});

Expand Down
6 changes: 6 additions & 0 deletions app/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
timeout: 30_000,
// On CI the server is a freshly-started production process: the first hit to a
// data-heavy dynamic route pays a one-time ~190MB corpus load, which under
// parallel load can lose a race with the 5s expect timeout. Retries re-run the
// failed test on a now-warm process; genuine failures still fail every attempt.
// Local runs stay strict (0) so real flakes surface.
retries: process.env.CI ? 2 : 0,
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
headless: true,
Expand Down