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
2 changes: 0 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ export default defineConfig({
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
// CI runners have 4 vCPUs; the default (half the cores) leaves two idle.
workers: process.env.CI ? "100%" : undefined,
reporter: process.env.CI ? [["github"], ["html", { open: "never" }]] : "list",
use: {
baseURL: externalBaseUrl ?? `http://localhost:${port}`,
Expand Down
4 changes: 2 additions & 2 deletions tests/e2e/examples.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ for (const example of EXAMPLES) {

const pitches = page.getByRole("region", { name: "Pitches", exact: true }).getByRole("listitem");
await expect(pitches).toHaveCount(3);
await expect(page.getByRole("region", { name: "Map of pitches and Qloo taste affinity" })).toBeVisible();
await expect(page.getByRole("region", { name: "Map of pitches and Qloo taste affinity" })).toBeVisible({ timeout: 15_000 });
// The pitch's details (card, setlist, crowd, weights) open in a sheet, not inline.
await expect(page.getByRole("heading", { name: /^Suggested set for / })).toHaveCount(0);
const sheet = await openDetails(page);
Expand Down Expand Up @@ -76,7 +76,7 @@ for (const example of EXAMPLES) {

// The link reopens the sheet on that pitch (shareable deep link).
await page.reload();
await expect(page.getByRole("dialog").getByRole("heading", { name: /^Suggested set for / })).toBeVisible();
await expect(page.getByRole("dialog").getByRole("heading", { name: /^Suggested set for / })).toBeVisible({ timeout: 15_000 });
});
});
}
Expand Down
6 changes: 4 additions & 2 deletions tests/e2e/landing.spec.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ASSET_COUNT, COMPONENT_COUNT } from "../../src/components/brand/brand-tables";
import { callKinds } from "../../src/lib/qloo-map";
import { expect, test } from "./fixtures";
import { WIDTHS } from "./layout-helpers";

// The landing against the design deck's Main board (QA's audit, B-018..B-026).
test.describe("landing, as the deck draws it", () => {
for (const width of [390, 768, 1440]) {
test(`the headline keeps "What to play." on one line at ${width}px (B-025)`, async ({ page }) => {
for (const width of WIDTHS) {
test(`the headline keeps "What to play." on one line at ${width}px (B-025)`, async ({ page }, info) => {
test.skip(info.project.name !== "desktop", "width tests run once, in the desktop project");
await page.setViewportSize({ width, height: 900 });
await page.goto("/");
const h1 = page.getByRole("heading", { level: 1 });
Expand Down
5 changes: 4 additions & 1 deletion tests/e2e/layout-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@

/** Visible, in view and on top: the element at its centre is the control itself or inside it. */
export async function expectOnTop(control: Locator, what: string) {
await control.scrollIntoViewIfNeeded();

Check failure on line 68 in tests/e2e/layout-helpers.ts

View workflow job for this annotation

GitHub Actions / Playwright judge paths (offline fixtures, shard 1/4)

[desktop] › tests/e2e/layout.spec.ts:301:13 › layout at phone

1) [desktop] › tests/e2e/layout.spec.ts:301:13 › layout at phone, tablet and desktop widths › 768 px › privacy: no sideways overflow, controls on top Error: locator.scrollIntoViewIfNeeded: Element is not attached to the DOM Call log: - attempting scroll into view action - waiting for element to be stable at layout-helpers.ts:68 66 | /** Visible, in view and on top: the element at its centre is the control itself or inside it. */ 67 | export async function expectOnTop(control: Locator, what: string) { > 68 | await control.scrollIntoViewIfNeeded(); | ^ 69 | await expect(control, what).toBeVisible(); 70 | await expect 71 | .poll( at expectOnTop (/home/runner/work/busk-map/busk-map/tests/e2e/layout-helpers.ts:68:17) at Object.controls (/home/runner/work/busk-map/busk-map/tests/e2e/layout.spec.ts:100:24) at /home/runner/work/busk-map/busk-map/tests/e2e/layout.spec.ts:305:19
await expect(control, what).toBeVisible();
await expect
.poll(
Expand All @@ -78,7 +78,7 @@
const hit = document.elementFromPoint(x, y);
return hit && (hit === el || el.contains(hit)) ? "on top" : `covered by ${hit?.outerHTML.slice(0, 140) ?? "nothing"}`;
}),
{ message: `${what} is covered or out of view`, timeout: 5_000 },
{ message: `${what} is covered or out of view`, timeout: 15_000 },
)
.toBe("on top");
}
Expand Down Expand Up @@ -107,3 +107,6 @@
});
}


/** The three widths every layout and axe test runs at, one test per width, in the desktop project only. */
export const WIDTHS = [390, 768, 1440] as const;
12 changes: 10 additions & 2 deletions tests/e2e/one-song-one-artist.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,19 @@ test.describe("one Song, one Artist (unify rule)", () => {
test("a busk (/p/[id]): the setlist, the crowd and the tool cards use the one Song and one Artist", async ({ page }) => {
await page.goto("/");
const example = EXAMPLES[0];
const plan = page.waitForResponse((r) => r.url().endsWith("/api/plan") && r.request().method() === "POST");
// Read the plan stream through a route: after the client navigates to /p/[id] the browser drops the
// response body, so response.text() can fail under load.
let body = "";
await page.route("**/api/plan", async (route) => {
if (route.request().method() !== "POST") return route.continue();
const response = await route.fetch();
body = await response.text();
await route.fulfill({ response, body });
});
await page.getByRole("link", { name: new RegExp(`^${example.label} `) }).click();
await expect(page).toHaveURL(/\/p\/[0-9A-Za-z]{9}$/);
const body = await (await plan).text();
await waitForPlan(page);
await expect.poll(() => body.length).toBeGreaterThan(0);
const found = collect(body.split("\n").filter(Boolean).map((line) => JSON.parse(line) as unknown));
for (const s of example.performer.songs) found.songs.add(s.title);
for (const a of example.performer.artists) found.artists.add(a);
Expand Down
6 changes: 4 additions & 2 deletions tests/e2e/plan-inputs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import AxeBuilder from "@axe-core/playwright";
import { SERVER_TODAY } from "./clock";
import { expect, LOCAL, openDetails, SATURDAY, shortDay, test, waitForPlan } from "./fixtures";
import { WIDTHS } from "./layout-helpers";

/**
* Plan inputs S5/S6 (docs/plan-inputs-architecture.md): the busk form's one time control, the read-only Repertoire block, and an any-time plan with a window per pitch.
Expand Down Expand Up @@ -207,8 +208,9 @@ test.describe("an any-time plan shows each pitch's hours (offline)", () => {
test.describe("layout and axe on the busk form and an any-time plan", () => {
test.skip(!LOCAL, "Local offline server only");

for (const width of [390, 768, 1440]) {
test(`the form and an any-time plan at ${width}px: no horizontal overflow, axe clean`, async ({ page }) => {
for (const width of WIDTHS) {
test(`layout and axe: the form and an any-time plan at ${width}px`, async ({ page }, info) => {
test.skip(info.project.name !== "desktop", "width tests run once, in the desktop project");
await page.setViewportSize({ width, height: 900 });
await seed(page);
await page.goto("/busks");
Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/poster.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { expect, test } from "./fixtures";
// Poster size regression (QA Landing v5): the poster variant owns its heights, 76px in the hero and 64px in the closing call to action.
test.describe("poster buttons", () => {
for (const width of [390, 1440]) {
test(`hero 76px and closing 64px, outline focus ring 3px at 10px offset (${width}px)`, async ({ page }) => {
test(`hero 76px and closing 64px, outline focus ring 3px at 10px offset (${width}px)`, async ({ page }, info) => {
test.skip(info.project.name !== "desktop", "width tests run once, in the desktop project");
await page.setViewportSize({ width, height: 900 });
await page.goto("/");
const hero = page.locator('[data-variant="poster"][data-size="poster-lg"]').first();
Expand Down
108 changes: 55 additions & 53 deletions tests/e2e/repertoire-planners.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AxeBuilder from "@axe-core/playwright";
import { expect, LOCAL, openDetails, test, waitForPlan } from "./fixtures";
import { WIDTHS } from "./layout-helpers";

/** Stage R2: the planners read the Repertoire (docs/playlist-architecture.md §3). Offline, memory store, one cookie per context. */
const WCAG = ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa", "wcag22a", "wcag22aa"];
Expand Down Expand Up @@ -132,68 +133,69 @@ test.describe("planners read the Repertoire (offline)", () => {
expect(await state(page)).toEqual(before);
});

test("layout and axe at 390, 768 and 1440: the form with the Repertoire, the plan", async ({ page }, info) => {
await seed(page);
const check = async (label: string) => {
const { violations } = await new AxeBuilder({ page }).withTags(WCAG).analyze();
expect(violations.map((v) => `${label} ${v.impact} ${v.id}: ${v.nodes.slice(0, 3).map((n) => n.target.join(" ")).join(" | ")}`)).toEqual([]);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
};
const shot = async (name: string, width: number) => {
if (SHOTS && info.project.name === "desktop") await page.screenshot({ path: `${SHOTS}/r2-${name}-${width}.png`, fullPage: true });
};
const widths = [390, 768, 1440];
await page.goto("/busks");
for (const width of widths) {
await page.setViewportSize({ width, height: 900 });
await check(`form-${width}`);
await form(page).scrollIntoViewIfNeeded();
await shot("busk-form", width);
}
await page.goto("/busks");
await form(page).getByRole("button", { name: "Plan my busk" }).click();
await waitForPlan(page);
const learnHere = (await openDetails(page)).getByRole("region", { name: "Songs you should learn" });
await learnHere.getByRole("button", { name: /^Add / }).first().click();
await expect(learnHere.getByRole("status").filter({ hasText: "Added to Songs you should learn." })).toBeVisible();
for (const width of widths) {
await page.setViewportSize({ width, height: 900 });
await check(`plan-${width}`);
await shot("plan", width);
}
// No Repertoire: the plan shows the save nudge.
const other = await page.context().browser()!.newContext({ baseURL: info.project.use.baseURL });
const p2 = await other.newPage();
await p2.goto("/plan?name=Nudge%20Duo&artists=Paul%20Kelly&songs=Gravy~Paul%20Kelly&date=2099-01-03&from=18&to=21");
await waitForPlan(p2);
await expect(p2.getByTestId("save-as-repertoire")).toBeVisible();
for (const width of widths) {
await p2.setViewportSize({ width, height: 900 });
const { violations } = await new AxeBuilder({ page: p2 }).withTags(WCAG).analyze();
expect(violations.map((v) => `nudge-${width} ${v.id}`)).toEqual([]);
if (SHOTS && info.project.name === "desktop") await p2.screenshot({ path: `${SHOTS}/r2-plan-save-nudge-${width}.png`, fullPage: true });
}
await other.close();
});
for (const width of WIDTHS) {
test.describe(`at ${width}px`, () => {
test.use({ viewport: { width, height: 900 } });

const axeClean = async (page: Page, label: string) => {
const { violations } = await new AxeBuilder({ page }).withTags(WCAG).analyze();
expect(violations.map((v) => `${label} ${v.impact} ${v.id}: ${v.nodes.slice(0, 3).map((n) => n.target.join(" ")).join(" | ")}`)).toEqual([]);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
};

test("layout and axe: the busk form with the Repertoire", async ({ page }, info) => {
test.skip(info.project.name !== "desktop", "width tests run once, in the desktop project");
await seed(page);
await page.goto("/busks");
await axeClean(page, `form-${width}`);
await form(page).scrollIntoViewIfNeeded();
if (SHOTS) await page.screenshot({ path: `${SHOTS}/r2-busk-form-${width}.png`, fullPage: true });
});

test("layout and axe: the plan, with a song added to Songs you should learn", async ({ page }, info) => {
test.skip(info.project.name !== "desktop", "width tests run once, in the desktop project");
await seed(page);
await page.goto("/busks");
await form(page).getByRole("button", { name: "Plan my busk" }).click();
await waitForPlan(page);
const learnHere = (await openDetails(page)).getByRole("region", { name: "Songs you should learn" });
await learnHere.getByRole("button", { name: /^Add / }).first().click();
await expect(learnHere.getByRole("status").filter({ hasText: "Added to Songs you should learn." })).toBeVisible();
await axeClean(page, `plan-${width}`);
if (SHOTS) await page.screenshot({ path: `${SHOTS}/r2-plan-${width}.png`, fullPage: true });
});

test("layout and axe: the plan with no Repertoire shows the save nudge", async ({ page }, info) => {
test.skip(info.project.name !== "desktop", "width tests run once, in the desktop project");
await page.goto("/plan?name=Nudge%20Duo&artists=Paul%20Kelly&songs=Gravy~Paul%20Kelly&date=2099-01-03&from=18&to=21");
await waitForPlan(page);
await expect(page.getByTestId("save-as-repertoire")).toBeVisible();
const { violations } = await new AxeBuilder({ page }).withTags(WCAG).analyze();
expect(violations.map((v) => `nudge-${width} ${v.id}`)).toEqual([]);
if (SHOTS) await page.screenshot({ path: `${SHOTS}/r2-plan-save-nudge-${width}.png`, fullPage: true });
});

test("layout and axe: R4 words, Suggested set and Songs you should learn", async ({ page }, info) => {
test.skip(info.project.name !== "desktop", "width tests run once, in the desktop project");
await page.goto("/plan?name=Words%20Duo&artists=Paul%20Kelly&songs=Gravy~Paul%20Kelly&date=2099-01-03&from=18&to=21");
await waitForPlan(page);
const card = (await openDetails(page)).getByRole("region", { name: "Suggested set" });
await card.scrollIntoViewIfNeeded();
await axeClean(page, `words-${width}`);
if (SHOTS) await card.screenshot({ path: `${SHOTS}/r4-suggested-set-${width}.png` });
});
});
}

test("R4 words: the plan says Suggested set and Songs you should learn, never Setlist or Learn these (3 widths, axe, screenshots)", async ({ page }, info) => {
test("R4 words: the plan says Suggested set and Songs you should learn, never Setlist or Learn these", async ({ page }) => {
await page.goto("/plan?name=Words%20Duo&artists=Paul%20Kelly&songs=Gravy~Paul%20Kelly&date=2099-01-03&from=18&to=21");
await waitForPlan(page);
const sheet = await openDetails(page);
const heading = sheet.getByRole("heading", { name: /^Suggested set for / });
await expect(heading).toBeVisible();
const card = sheet.getByRole("region", { name: "Suggested set" });
const learn = sheet.getByRole("region", { name: "Songs you should learn" });
await expect(learn.getByRole("heading", { name: "Songs you should learn" })).toBeVisible();
await expect(page.getByRole("heading", { name: /Setlist|Learn these/ })).toHaveCount(0);
await expect(page.getByText(/\bsetlist\b|learn these|playlist/i)).toHaveCount(0);
for (const width of [390, 768, 1440]) {
await page.setViewportSize({ width, height: 900 });
await card.scrollIntoViewIfNeeded();
const { violations } = await new AxeBuilder({ page }).withTags(WCAG).analyze();
expect(violations.map((v) => `${width} ${v.impact} ${v.id}: ${v.nodes.slice(0, 3).map((n) => n.target.join(" ")).join(" | ")}`)).toEqual([]);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true);
if (SHOTS && info.project.name === "desktop") await card.screenshot({ path: `${SHOTS}/r4-suggested-set-${width}.png` });
}
});
});
Loading
Loading