diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1d43844a5..27dfe8261 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,14 +33,6 @@ updates: cooldown: default-days: 7 open-pull-requests-limit: 5 - # @playwright/test 1.61 rewrote its config loader to call - # `require(..., {conditions})` and to return module sources from an ESM - # `load` hook. Yarn PnP supports neither, so every version past 1.60 - # dies before the first test file is collected. Held back until PnP - # catches up or the frontend moves off it. - ignore: - - dependency-name: "@playwright/test" - versions: [ ">= 1.61" ] groups: frontend: applies-to: version-updates diff --git a/services/frontend/package.json b/services/frontend/package.json index 4550a105a..bcacaf041 100644 --- a/services/frontend/package.json +++ b/services/frontend/package.json @@ -61,7 +61,7 @@ "@eslint/js": "^10.0.1", "@hey-api/openapi-ts": "0.99.0", "@originjs/vite-plugin-commonjs": "^1.0.3", - "@playwright/test": "1.60.0", + "@playwright/test": "1.63.0", "@types/luxon": "^3.7.5", "@types/node": "^26.2.0", "@types/webfontloader": "^1.6.38", diff --git a/services/frontend/tests/e2e/board-slice-open.motion.spec.ts b/services/frontend/tests/e2e/board-slice-open.motion.spec.ts index f4570b93d..4b36449e9 100644 --- a/services/frontend/tests/e2e/board-slice-open.motion.spec.ts +++ b/services/frontend/tests/e2e/board-slice-open.motion.spec.ts @@ -267,13 +267,8 @@ test.describe("a member's slice opening on a phone", () => { test.describe("a member's slice opening for a visitor who asked for less motion", () => { test("clamps both movements to the ceiling the island allows", async ({page}) => { - /* - * Asked of the page rather than declared as an option, which is what the season spec next - * door does and for the same reason: on Playwright 1.60 `use.reducedMotion` does not reach - * the page at all, so `matchMedia` answers false however the option is set — #852. A test of - * what a visitor with the preference gets has to actually be one, so it says so here. Once - * #852 is fixed this line is what should go. - */ + // Asked of the page rather than taken from the project: this file runs in the motion + // project, which is the one project that asks for no preference at all. await page.emulateMedia({reducedMotion: "reduce"}) const watched = await boardOnAPhone(page) diff --git a/services/frontend/tests/e2e/boards-swipe.motion.spec.ts b/services/frontend/tests/e2e/boards-swipe.motion.spec.ts index 832ab3250..15528e1b3 100644 --- a/services/frontend/tests/e2e/boards-swipe.motion.spec.ts +++ b/services/frontend/tests/e2e/boards-swipe.motion.spec.ts @@ -266,10 +266,8 @@ test.describe("dragging the board page", () => { /** * What a visitor who asked for reduced motion gets, which is the gesture without its tails. * - * The preference is emulated for this test rather than taken from the project, because - * `use.reducedMotion` does not reach the page on Playwright 1.60 — #852 — so every - * "deterministic" project in this suite is in fact running with full motion. The same line and - * the same reasoning are in `esports-season-on-show.spec.ts`; when #852 is fixed, both go. + * The preference is emulated for this test rather than taken from the project: this file runs + * in the motion project, which is the one project that asks for no preference at all. * * The durations are read off the animations themselves rather than timed with a clock. Eight * workers share four vCPUs here, so a wall-clock measurement of "it settled quickly" is a diff --git a/services/frontend/tests/e2e/boards.spec.ts b/services/frontend/tests/e2e/boards.spec.ts index 2cb070493..96882a5b4 100644 --- a/services/frontend/tests/e2e/boards.spec.ts +++ b/services/frontend/tests/e2e/boards.spec.ts @@ -48,10 +48,10 @@ const restingDissolve = (page: Page): Promise => page.evaluate(() => ( * it computes. A named direction in here at all is a picture fading the wrong way — to the * right, across the reading direction, which is the shape this one replaced. * - * The depth is left loose on purpose. This suite runs with full motion whatever the config says - * (#852), so the depth read a moment after a press is wherever the eased dissolve had got to, - * and a whole number of percent is a coin toss on how busy the machine is. Where it comes to - * rest is asserted frame by frame in the motion spec, which is the layer that can watch it. + * The depth is left loose on purpose. The dissolve is eased even under the reduced ceiling, so + * the depth read a moment after a press is wherever it had got to, and a whole number of percent + * is a coin toss on how busy the machine is. Where it comes to rest is asserted frame by frame in + * the motion spec, which is the layer that can watch it. */ const DOWNWARDS = /^linear-gradient\(rgb\(0, 0, 0\) 0px, rgb\(0, 0, 0\) \d+(?:\.\d+)?%, rgba\(0, 0, 0, 0\) 100%\)$/ @@ -793,10 +793,10 @@ test.describe("board page", () => { /* * How far down each of the two has gone, as a percentage of the picture's band. * - * Both read inside one `evaluate`, so both are the same frame's answer. This suite runs - * with full motion whatever the config says (#852), so the depth a moment after a press is - * wherever the ease had got to — which is exactly why the two have to be read together and - * why the pair is polled until it settles rather than sampled once. + * Both read inside one `evaluate`, so both are the same frame's answer. The dissolve is eased + * even under the reduced ceiling, so the depth a moment after a press is wherever it had got + * to — which is exactly why the two have to be read together and why the pair is polled until + * it settles rather than sampled once. */ const depths = () => member.getByRole("button").evaluate((body) => { const depth = (style: CSSStyleDeclaration) => { diff --git a/services/frontend/tests/e2e/esports-season-drag.motion.spec.ts b/services/frontend/tests/e2e/esports-season-drag.motion.spec.ts index 4f0354fc2..b970d1d84 100644 --- a/services/frontend/tests/e2e/esports-season-drag.motion.spec.ts +++ b/services/frontend/tests/e2e/esports-season-drag.motion.spec.ts @@ -365,11 +365,10 @@ test.describe("dragging the esports index between seasons", () => { /** * What a visitor who asked for reduced motion gets, which is the gesture without its tails. * - * The preference is emulated for this test rather than taken from the project, because - * `use.reducedMotion` does not reach the page on Playwright 1.60 — #852 — so every - * "deterministic" project in this suite is in fact running with full motion. The same line and - * the same reasoning are in `boards-swipe.motion.spec.ts` and `esports-season-on-show.spec.ts`; - * when #852 is fixed, all of them go. + * The preference is emulated for this test rather than taken from the project, because this + * file runs in the motion project, which asks for no preference on purpose. A reduced-motion + * case living here has to say so itself. The same line and the same reasoning are in + * `boards-swipe.motion.spec.ts` and `board-slice-open.motion.spec.ts`. * * The durations are read off the animations themselves rather than timed with a clock. Eight * workers share four vCPUs here, so a wall-clock measurement of "it settled quickly" is a diff --git a/services/frontend/tests/e2e/esports-season-on-show.spec.ts b/services/frontend/tests/e2e/esports-season-on-show.spec.ts index 5aafc35e4..a4113aa9a 100644 --- a/services/frontend/tests/e2e/esports-season-on-show.spec.ts +++ b/services/frontend/tests/e2e/esports-season-on-show.spec.ts @@ -1,6 +1,6 @@ import {expect, test} from "./test" import {installApiMocks} from "./mocks" -import {landing} from "./sliceBand" +import {landingFrom} from "./sliceBand" /** * The season a page opens on, and that it is the same season everything on it describes. @@ -9,9 +9,9 @@ import {landing} from "./sliceBand" * about which season is arrived at. The movement itself is in * `esports-season-swipe.motion.spec.ts`. * - * Those projects are meant to emulate reduced motion and do not: see #852. So nothing here may - * assume the choreography is switched off, and the one test below that is about the preference - * sets it for itself. + * Those projects run as a visitor who asked for reduced motion, which shortens the pass rather + * than removing it: the two stops are both on the page for the clamped duration, so a landing is + * watched for from before the season is asked for rather than sampled after the fact. * * CS:GO is the game that makes these assertions mean something: it played the older of the two * seasons and nothing since, so a page that lets the api pick a season per game shows it, and @@ -95,14 +95,17 @@ test.describe("the season a page opens on", () => { await page.getByTestId("esports-game-CS2").hover() await expect(page.getByTestId("esports-game-CS2")).toHaveClass(/slice--open/) + // Watched from before the change is asked for: this visitor's pass is a tenth of a second, + // which is shorter than the round trip a loaded runner takes to install a watch. + const drawn = await landingFrom(page, SWIPE, "esports-game-CS2", "esports-game-CSGO") + await page.getByTestId("esports-season-node-19").click() await expect(page.getByTestId("season-swipe")).toHaveAttribute("data-swipe", "past") // Open in the frame the arriving season is first drawn in, while the pass is still on, // rather than growing again once it is over. CS:GO is what says the band is the older // season's, the slice being read having stood on the season before it as well. - const drawn = await landing(page, SWIPE, "esports-game-CS2", "esports-game-CSGO") - expect(drawn).toEqual({open: true, panels: 2}) + expect(await drawn()).toEqual({open: true, panels: 2}) // The season travelled. The subject did not change under them on the way. await expect(page.getByTestId("esports-game-CS2")).toHaveClass(/slice--open/) @@ -118,15 +121,7 @@ test.describe("the season a page opens on", () => { }) test("crosses the seasons over rather than travelling, for a visitor who asked for less motion", async ({page}) => { - // Asked for here rather than left to the project. - // - // `use.reducedMotion: "reduce"` is set on every project but the motion one, and on - // Playwright 1.60 it does not reach the page: `matchMedia("(prefers-reduced-motion: - // reduce)")` answers false throughout the deterministic suites. That is a fault in the - // harness rather than in this behaviour — #852 — and it is not this spec's to fix, but a - // test of what a visitor with the preference gets has to actually be one. Once #852 is - // fixed this line is what should go. - await page.emulateMedia({reducedMotion: "reduce"}) + // The preference comes from the project, which every project but the motion one sets. await installApiMocks(page) await page.goto("/esports/competitive-scene") diff --git a/services/frontend/tests/e2e/esports-season-swipe.motion.spec.ts b/services/frontend/tests/e2e/esports-season-swipe.motion.spec.ts index 89d7a2c9e..db2ad7c8f 100644 --- a/services/frontend/tests/e2e/esports-season-swipe.motion.spec.ts +++ b/services/frontend/tests/e2e/esports-season-swipe.motion.spec.ts @@ -1,7 +1,7 @@ import {expect, test} from "./test" import {installApiMocks} from "./mocks" import {eightSeasonFixtures} from "./esportsStrip" -import {landing} from "./sliceBand" +import {landingFrom} from "./sliceBand" /** * Moving between seasons, and which way. @@ -66,12 +66,13 @@ test.describe("swiping between seasons", () => { await page.goto("/esports/valorant") await page.getByTestId("season-swipe").waitFor() + const drawn = await landingFrom(page, SWIPE, "team-roster-52") + await page.getByTestId(`esports-season-node-${OLDER}`).click() // Its team's slice is open in the frame the band is first drawn in, with the pass still on: // the pass is the whole animation, so nothing grows once it is over. - const drawn = await landing(page, SWIPE, "team-roster-52") - expect(drawn).toEqual({open: true, panels: 2}) + expect(await drawn()).toEqual({open: true, panels: 2}) // The season that left is gone rather than parked off-screen, and the one that arrived is // where the band always sits. diff --git a/services/frontend/tests/e2e/membership.spec.ts b/services/frontend/tests/e2e/membership.spec.ts index 41b70432d..66107dbde 100644 --- a/services/frontend/tests/e2e/membership.spec.ts +++ b/services/frontend/tests/e2e/membership.spec.ts @@ -108,12 +108,9 @@ test.describe("membership page", () => { * The one moving thing on the page, for a visitor who asked for less of it: the words are * simply there, at rest, rather than arriving. * - * The preference is emulated here rather than left to the project: `use.reducedMotion` does - * not reach the page on Playwright 1.60 (#852), so a spec that assumes it is asserting the - * unreduced path under a reduced name. + * The preference comes from the project, which every project but the motion one sets. */ test("does not animate the pitch in for a visitor who asked for less motion", async ({page}) => { - await page.emulateMedia({reducedMotion: "reduce"}) await installApiMocks(page) await page.goto("/membership") diff --git a/services/frontend/tests/e2e/sliceBand.ts b/services/frontend/tests/e2e/sliceBand.ts index 81037916d..c3f6e04d8 100644 --- a/services/frontend/tests/e2e/sliceBand.ts +++ b/services/frontend/tests/e2e/sliceBand.ts @@ -103,25 +103,39 @@ export function arrivedOpen(seen: {open: boolean, height: number}[]): void { } /** - * How the band at [swipe] is drawn in the frame [testid] is first on the page: whether that - * slice is open, and how many panels the band holds, two of them saying the pass is still on. + * Watches the band at [swipe] from now on, answering with a read of the frame [gate] is first + * drawn in: whether [testid] is open in it, and how many panels the band holds, two of them + * saying the pass is still on. * - * Read at a frame boundary rather than polled, since the claim is about the first frame. [gate] - * is what says the arriving stop is the one being read, where the slice is not itself that proof; - * the band leaving answers to no name, its testids taken off it at the swap. + * Begun before the change is asked for, and counted on the page. A pass is over in the time a + * visitor who asked for less motion allows it — a tenth of a second — so a watch installed + * after the click is a race against the runner's own round trips, and a loaded one loses it: + * the band then answers with one panel for a pass that did happen. [gate] is what says the + * arriving stop is the one being read, where the slice is not itself that proof; the band + * leaving answers to no name, its testids taken off it at the swap. */ -export async function landing( +export async function landingFrom( page: Page, swipe: string, testid: string, gate = testid, -): Promise<{open: boolean, panels: number}> { - const handle = await page.waitForFunction(([sel, id, key]) => { - if (!document.querySelector(`[data-testid="${key}"]`)) return null - const slice = document.querySelector(`[data-testid="${id}"]`) - return { - open: slice?.className.includes("slice--open") ?? false, - panels: document.querySelectorAll(`${sel} > *`).length, +): Promise<() => Promise<{open: boolean, panels: number}>> { + await page.evaluate(([sel, id, key]) => { + const tick = () => { + if (!document.querySelector(`[data-testid="${key}"]`)) return requestAnimationFrame(tick) + const slice = document.querySelector(`[data-testid="${id}"]`) + ;(window as unknown as {__landing?: unknown}).__landing = { + open: slice?.className.includes("slice--open") ?? false, + panels: document.querySelectorAll(`${sel} > *`).length, + } } + delete (window as unknown as {__landing?: unknown}).__landing + requestAnimationFrame(tick) }, [swipe, testid, gate] as const) - return handle.jsonValue() + + return async () => { + const handle = await page.waitForFunction(() => (window as unknown as { + __landing?: {open: boolean, panels: number} + }).__landing ?? null) + return handle.jsonValue() + } } /** The height the band at [swipe] is holding right now, or nothing where it holds none. */ diff --git a/services/frontend/yarn.lock b/services/frontend/yarn.lock index a611aa654..197c3f4b2 100644 --- a/services/frontend/yarn.lock +++ b/services/frontend/yarn.lock @@ -1291,14 +1291,14 @@ __metadata: languageName: node linkType: hard -"@playwright/test@npm:1.60.0": - version: 1.60.0 - resolution: "@playwright/test@npm:1.60.0" +"@playwright/test@npm:1.63.0": + version: 1.63.0 + resolution: "@playwright/test@npm:1.63.0" dependencies: - playwright: "npm:1.60.0" + playwright: "npm:1.63.0" bin: playwright: cli.js - checksum: 10c0/86b06e6437933e741c7cd43f362024e857e7bc28a55fcbb0553ef55e01a2a403c64f4786868de8af86a6e303fe99e98a18a42ba19489f43ae122e457f9e2d189 + checksum: 10c0/795cd06c2eb6cb6142e72792f4df8f97665aa8c551c16278a7f2b3f31664e0cff07ed3c066490da55daf2fe51bc18e9c1ec6cba05b2c37387a0c49ad63017db8 languageName: node linkType: hard @@ -2920,7 +2920,7 @@ __metadata: "@hey-api/openapi-ts": "npm:0.99.0" "@mdi/font": "npm:^7.4.47" "@originjs/vite-plugin-commonjs": "npm:^1.0.3" - "@playwright/test": "npm:1.60.0" + "@playwright/test": "npm:1.63.0" "@tailwindcss/vite": "npm:^4.3.3" "@types/luxon": "npm:^3.7.5" "@types/node": "npm:^26.2.0" @@ -4280,16 +4280,6 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:2.3.2": - version: 2.3.2 - resolution: "fsevents@npm:2.3.2" - dependencies: - node-gyp: "npm:latest" - checksum: 10c0/be78a3efa3e181cda3cf7a4637cb527bcebb0bd0ea0440105a3bb45b86f9245b307dc10a2507e8f4498a7d4ec349d1910f4d73e4d4495b16103106e07eee735b - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@npm:~2.3.3": version: 2.3.3 resolution: "fsevents@npm:2.3.3" @@ -4300,15 +4290,6 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin": - version: 2.3.2 - resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" - dependencies: - node-gyp: "npm:latest" - conditions: os=darwin - languageName: node - linkType: hard - "fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": version: 2.3.3 resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" @@ -5946,27 +5927,23 @@ __metadata: languageName: node linkType: hard -"playwright-core@npm:1.60.0": - version: 1.60.0 - resolution: "playwright-core@npm:1.60.0" +"playwright-core@npm:1.63.0": + version: 1.63.0 + resolution: "playwright-core@npm:1.63.0" bin: playwright-core: cli.js - checksum: 10c0/99ccd43923b6e9355e0723b7fe221e6326efd4687f8dafff951313662aea11db51f542a9c2122c704c445fb9baae1c9ec9fa6f895126bbddd9fe92313f6942c9 + checksum: 10c0/39c1ebae4029ca2e9371e00a5588d962038e24b6e0bdb7d0d27374dde04838f349b83ae0c8b3b648653d0935cba7fb9011f1201adaab9dd0db7801ad6c75e9aa languageName: node linkType: hard -"playwright@npm:1.60.0": - version: 1.60.0 - resolution: "playwright@npm:1.60.0" +"playwright@npm:1.63.0": + version: 1.63.0 + resolution: "playwright@npm:1.63.0" dependencies: - fsevents: "npm:2.3.2" - playwright-core: "npm:1.60.0" - dependenciesMeta: - fsevents: - optional: true + playwright-core: "npm:1.63.0" bin: playwright: cli.js - checksum: 10c0/714ad76d85b4865d7e43c0012f9039800c1485373388973ed39d79339cee5ad467052d1e2f1eaeca107a1cb6e65342186a8578a4c3504853d84c3a691250d5db + checksum: 10c0/d1fd048a89be27c9aba6757dc4f70eff4a5163b49f439ad691767be8b2a482327d0dd1928b4f73c8fe0d8b33aab2c9dc2670424ab60f112d9e98e91b94e3b090 languageName: node linkType: hard