diff --git a/.gitignore b/.gitignore index 536d360..d515d75 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ simulator/static/review_audio.html # Static build outputs (tools/build_static.py) /dist/ /dist-media/ + +# Playwright e2e artifacts (should never be committed) +simulator/e2e/test-results/ +simulator/e2e/playwright-report/ diff --git a/simulator/e2e/test-results/a11y-Task-1-—-Output-panel-36682-its-below-the-Audio-control/error-context.md b/simulator/e2e/test-results/a11y-Task-1-—-Output-panel-36682-its-below-the-Audio-control/error-context.md deleted file mode 100644 index 92c12c7..0000000 --- a/simulator/e2e/test-results/a11y-Task-1-—-Output-panel-36682-its-below-the-Audio-control/error-context.md +++ /dev/null @@ -1,137 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 1 — Output panel layout >> language picker sits below the Audio control -- Location: tests/a11y.spec.ts:15:7 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [ref=e2]: "{\"detail\":\"Not Found\"}" -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { -> 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). - 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/a11y-Task-1-—-Output-panel-4e222-e-language-select-same-row-/error-context.md b/simulator/e2e/test-results/a11y-Task-1-—-Output-panel-4e222-e-language-select-same-row-/error-context.md deleted file mode 100644 index 8bfa68d..0000000 --- a/simulator/e2e/test-results/a11y-Task-1-—-Output-panel-4e222-e-language-select-same-row-/error-context.md +++ /dev/null @@ -1,202 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 1 — Output panel layout >> globe icon is inline-left of the language select (same row) -- Location: tests/a11y.spec.ts:27:7 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" - - generic [ref=e69]: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { -> 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). - 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/a11y-Task-4-—-Photosensiti-eae44-otice-and-the-Speed-control/error-context.md b/simulator/e2e/test-results/a11y-Task-4-—-Photosensiti-eae44-otice-and-the-Speed-control/error-context.md deleted file mode 100644 index 861139d..0000000 --- a/simulator/e2e/test-results/a11y-Task-4-—-Photosensiti-eae44-otice-and-the-Speed-control/error-context.md +++ /dev/null @@ -1,226 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 4 — Photosensitivity notice >> the welcome screen carries the heads-up notice and the Speed control -- Location: tests/a11y.spec.ts:40:7 - -# Error details - -``` -Error: expect(locator).toBeVisible() failed - -Locator: locator('#welcome-title') -Expected: visible -Timeout: 5000ms -Error: element(s) not found - -Call log: - - Expect "toBeVisible" with timeout 5000ms - - waiting for locator('#welcome-title') - -``` - -```yaml -- banner: - - heading "Human Experience Filter — Alteration Preview" [level=1] - - link "About": - - /url: about.html - - link "ⓘ Credits & licenses": - - /url: credits.html -- main: - - button "Run simulation" - - dialog "Heads up — motion & flashing": - - heading "Heads up — motion & flashing" [level=2] - - paragraph: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" - - status: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output": - - text: Output - - checkbox "Video" - - text: Video Audio - - slider "Audio 0 /10": "0" - - text: 0 /10 Speed - - slider "Speed 1.00×": "1" - - text: 1.00× 🌐 - - combobox "Language": - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude": - - text: Altitude - - slider "Altitude — turn or use arrow keys to change scale": - - button "cosmos" - - button "orbit" - - button "sky" - - button "coast" - - button "reef" - - button "abyss" - - text: ALTITUDE - - text: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)": - - text: Experience knobs (0–4) Think - - slider "Think": "0" - - text: Feel - - slider "Feel": "0" - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light": "0" - - checkbox "Dev Mode" - - text: Dev Mode -- text: ⬇ caching clips 363/599 -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { - 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). -> 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - | ^ Error: expect(locator).toBeVisible() failed - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | - 110 | test.describe("Task 6 — Keyboard + ARIA dial", () => { - 111 | test("altitude dial is a keyboard-operable slider", async ({ page }) => { - 112 | await page.goto("/"); - 113 | await enter(page); - 114 | const dial = page.locator("#dial"); - 115 | await expect(dial).toHaveAttribute("role", "slider"); - 116 | await expect(dial).toHaveAttribute("tabindex", "0"); - 117 | await dial.focus(); - 118 | const before = await page.locator("#scale-name").textContent(); - 119 | await dial.press("ArrowDown"); // descend one altitude - 120 | await expect - 121 | .poll(async () => page.locator("#scale-name").textContent()) - 122 | .not.toBe(before); - 123 | await dial.press("Home"); // jump to the top scale (cosmos) - 124 | await expect(page.locator("#scale-name")).toHaveText(/cosmos|宇宙/i); - 125 | await expect(dial).toHaveAttribute("aria-valuenow", "0"); - 126 | }); - 127 | }); - 128 | - 129 | test.describe("Task 7 — Screen-reader narration", () => { - 130 | test("an aria-live region narrates the current scale", async ({ page }) => { - 131 | await page.goto("/"); - 132 | await enter(page); - 133 | const live = page.locator("#sr-status"); - 134 | await expect(live).toHaveAttribute("aria-live", "polite"); - 135 | await page.locator("#dial").focus(); - 136 | await page.locator("#dial").press("Home"); - 137 | await expect - 138 | .poll(async () => (await live.textContent())?.toLowerCase()) - 139 | .toContain("cosmos"); - 140 | }); - 141 | - 142 | test("decorative overlay SVGs are hidden from assistive tech", async ({ page }) => { - 143 | await page.goto("/"); -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-509d1-e-loop-video-s-playbackRate/error-context.md b/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-509d1-e-loop-video-s-playbackRate/error-context.md deleted file mode 100644 index 1c767f1..0000000 --- a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-509d1-e-loop-video-s-playbackRate/error-context.md +++ /dev/null @@ -1,202 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 5 — Playback speed slider (replaces Reduce motion) >> forward speed sets the loop video's playbackRate -- Location: tests/a11y.spec.ts:62:7 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" - - generic [ref=e69]: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { -> 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). - 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-78ef6-slider-and-freezes-the-loop/error-context.md b/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-78ef6-slider-and-freezes-the-loop/error-context.md deleted file mode 100644 index b6de8e4..0000000 --- a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-78ef6-slider-and-freezes-the-loop/error-context.md +++ /dev/null @@ -1,206 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 5 — Playback speed slider (replaces Reduce motion) >> OS reduced-motion disables the slider and freezes the loop -- Location: tests/a11y.spec.ts:99:7 - -# Error details - -``` -Error: Channel closed -``` - -``` -Error: page.waitForFunction: Test ended. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [disabled] [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" - - generic [ref=e69]: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { -> 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ Error: page.waitForFunction: Test ended. - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). - 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-7f910-n-0x-freezes-the-loop-video/error-context.md b/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-7f910-n-0x-freezes-the-loop-video/error-context.md deleted file mode 100644 index 84569fd..0000000 --- a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-7f910-n-0x-freezes-the-loop-video/error-context.md +++ /dev/null @@ -1,202 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 5 — Playback speed slider (replaces Reduce motion) >> 0x freezes the loop video -- Location: tests/a11y.spec.ts:72:7 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" - - generic [ref=e69]: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { -> 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). - 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-d97a0-erse-manual-frame-stepping-/error-context.md b/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-d97a0-erse-manual-frame-stepping-/error-context.md deleted file mode 100644 index e8a80f8..0000000 --- a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-d97a0-erse-manual-frame-stepping-/error-context.md +++ /dev/null @@ -1,202 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 5 — Playback speed slider (replaces Reduce motion) >> negative speed plays the loop in reverse (manual frame-stepping) -- Location: tests/a11y.spec.ts:82:7 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" - - generic [ref=e69]: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { -> 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). - 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-f4262--replaced-by-a-speed-slider/error-context.md b/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-f4262--replaced-by-a-speed-slider/error-context.md deleted file mode 100644 index c0ddf1d..0000000 --- a/simulator/e2e/test-results/a11y-Task-5-—-Playback-spe-f4262--replaced-by-a-speed-slider/error-context.md +++ /dev/null @@ -1,202 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: a11y.spec.ts >> Task 5 — Playback speed slider (replaces Reduce motion) >> the Reduce motion toggle is gone, replaced by a speed slider -- Location: tests/a11y.spec.ts:52:7 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" - - generic [ref=e69]: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // Accessibility pass (feat/accessibility-pass) — non-kiosk public-web a11y. - 4 | // Each block maps to a task in - 5 | // docs/superpowers/plans/2026-06-30-accessibility-and-about-page.md. - 6 | - 7 | // Enter the simulation via the welcome screen (the panel is hidden until then). - 8 | async function enter(page: Page) { -> 9 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 10 | await page.locator("#welcome-launch").click(); - 11 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 12 | } - 13 | - 14 | test.describe("Task 1 — Output panel layout", () => { - 15 | test("language picker sits below the Audio control", async ({ page }) => { - 16 | await page.goto("/"); - 17 | await enter(page); - 18 | const audio = page.locator("#audio"); - 19 | const lang = page.locator("#lang-select"); - 20 | await expect(audio).toBeVisible(); - 21 | await expect(lang).toBeVisible(); - 22 | const aBox = await audio.boundingBox(); - 23 | const lBox = await lang.boundingBox(); - 24 | expect(lBox!.y).toBeGreaterThan(aBox!.y); // lang renders lower than audio - 25 | }); - 26 | - 27 | test("globe icon is inline-left of the language select (same row)", async ({ page }) => { - 28 | await page.goto("/"); - 29 | await enter(page); - 30 | const pick = page.locator(".panel .lang-pick"); - 31 | const select = page.locator("#lang-select"); - 32 | const pBox = await pick.boundingBox(); - 33 | const sBox = await select.boundingBox(); - 34 | expect(sBox!.x).toBeGreaterThan(pBox!.x); // select right of the label's left edge - 35 | expect(pBox!.height).toBeLessThan(40); // one row, not stacked - 36 | }); - 37 | }); - 38 | - 39 | test.describe("Task 4 — Photosensitivity notice", () => { - 40 | test("the welcome screen carries the heads-up notice and the Speed control", async ({ page }) => { - 41 | await page.goto("/"); - 42 | // The heads-up is shown on the welcome screen itself (it is the entry gate). - 43 | await expect(page.locator("#welcome-title")).toBeVisible(); - 44 | await expect(page.locator("#welcome-title")).toHaveText(/motion|flashing/i); - 45 | // The Speed control + Launch button sit right there, before the visitor launches. - 46 | await expect(page.locator("#welcome-play-speed")).toBeVisible(); - 47 | await expect(page.locator("#welcome-launch")).toBeVisible(); - 48 | }); - 49 | }); - 50 | - 51 | test.describe("Task 5 — Playback speed slider (replaces Reduce motion)", () => { - 52 | test("the Reduce motion toggle is gone, replaced by a speed slider", async ({ page }) => { - 53 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 54 | await page.goto("/"); - 55 | await enter(page); - 56 | await expect(page.locator("#reduce-motion")).toHaveCount(0); - 57 | const slider = page.locator("#play-speed"); - 58 | await expect(slider).toBeVisible(); - 59 | await expect(slider).toHaveValue("1"); // default = normal speed - 60 | }); - 61 | - 62 | test("forward speed sets the loop video's playbackRate", async ({ page }) => { - 63 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 64 | await page.goto("/"); - 65 | await enter(page); - 66 | await page.locator("#play-speed").fill("2"); - 67 | await expect - 68 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.playbackRate)) - 69 | .toBe(2); - 70 | }); - 71 | - 72 | test("0x freezes the loop video", async ({ page }) => { - 73 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 74 | await page.goto("/"); - 75 | await enter(page); - 76 | await page.locator("#play-speed").fill("0"); - 77 | await expect - 78 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 79 | .toBe(true); - 80 | }); - 81 | - 82 | test("negative speed plays the loop in reverse (manual frame-stepping)", async ({ page }) => { - 83 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 84 | await page.goto("/"); - 85 | await enter(page); - 86 | await page.locator("#play-speed").fill("-1.5"); - 87 | await page.waitForTimeout(150); - 88 | const a = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 89 | const paused = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused); - 90 | await page.waitForTimeout(250); - 91 | const b = await page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.currentTime); - 92 | // The reverse loop pauses the element and drives currentTime by hand — so the - 93 | // element reads paused, yet currentTime keeps moving (≠ forward play, ≠ freeze). - 94 | // (It may wrap loopStart→tail, so we assert movement, not a strict decrease.) - 95 | expect(paused).toBe(true); - 96 | expect(b).not.toBe(a); - 97 | }); - 98 | - 99 | test("OS reduced-motion disables the slider and freezes the loop", async ({ page }) => { - 100 | await page.emulateMedia({ reducedMotion: "reduce" }); - 101 | await page.goto("/"); - 102 | await enter(page); - 103 | await expect(page.locator("#play-speed")).toBeDisabled(); - 104 | await expect - 105 | .poll(() => page.locator("#vid-loop").evaluate((v: HTMLVideoElement) => v.paused)) - 106 | .toBe(true); // OS prefers-reduced-motion → frozen regardless of slider - 107 | }); - 108 | }); - 109 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/altitude-lock-a-landed-cli-4f073-et-no-jump-back-to-frame-0-/error-context.md b/simulator/e2e/test-results/altitude-lock-a-landed-cli-4f073-et-no-jump-back-to-frame-0-/error-context.md deleted file mode 100644 index bf7473a..0000000 --- a/simulator/e2e/test-results/altitude-lock-a-landed-cli-4f073-et-no-jump-back-to-frame-0-/error-context.md +++ /dev/null @@ -1,271 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: altitude-lock.spec.ts >> a landed clip loops from the morph-tail offset (no jump-back to frame 0) -- Location: tests/altitude-lock.spec.ts:137:5 - -# Error details - -``` -Error: Channel closed -``` - -``` -Error: page.waitForFunction: Test ended. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" [checked] - - generic [ref=e69]: Dev Mode - - generic [ref=e70]: - - group "Audio diagnostics" [ref=e71]: - - generic [ref=e72]: Audio diagnostics - - generic [ref=e73]: "audio: off (level 0)" - - paragraph [ref=e74]: Native player — tests the soundtrack file directly (bypasses all app code). If it plays here but the Audio toggle is silent, it's a code/autoplay issue, not the file. - - group "Pool — pick a clip" [ref=e76]: - - generic [ref=e77]: Pool — pick a clip - - combobox "Choose a clip from this altitude's pool" [ref=e78]: - - option "cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)" [selected] - - option "cosmos_galaxies · Flying Through Galaxies (NASA SVS)" - - option "cosmos_orion · Orion Nebula — visible/IR flythrough (NASA SVS)" - - option "cosmos_tarantula · Tarantula Nebula (30 Doradus) zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_westerlund · Flight through star cluster Westerlund 2 (ESA/Hubble)" - - option "cosmos_southernring · Southern Ring Nebula zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_carina_eso · Carina Nebula zoom (ESO)" - - button "🎲 Re-roll random" [ref=e79] [cursor=pointer] - - group "Active clip" [ref=e80]: - - generic [ref=e81]: Active clip - - generic [ref=e82]: - - term [ref=e83]: id - - definition [ref=e84]: cosmos - - term [ref=e85]: title - - definition [ref=e86]: Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - term [ref=e87]: source - - definition [ref=e88]: NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); 4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough. - - term [ref=e89]: license - - definition [ref=e90]: CC-BY-class — credit NASA, ESA, CSA, STScI - - term [ref=e91]: base_file - - definition [ref=e92]: cosmos/base.mp4 - - group "Annotations & affect" [ref=e93]: - - generic [ref=e94]: Annotations & affect - - generic [ref=e95]: - - generic [ref=e96]: detected.nebula · sal 4 · 4 tiers - - generic [ref=e97]: detected.star · sal 2 · 4 tiers - - generic [ref=e98]: measure.distance · min L3 · 1 tier - - generic [ref=e99]: feel.wonder · min L1 · 4 tiers - - generic [ref=e100]: feel.sublime · min L2 · 4 tiers - - generic [ref=e101]: feel.vastness · min L3 · 4 tiers - - generic [ref=e102]: feel.insignificance · min L4 · 4 tiers - - group "RenderPlan" [ref=e103]: - - generic [ref=e104]: RenderPlan - - generic [ref=e105]: "{ \"plan\": { \"grade\": { \"tone\": 0 }, \"overlay\": { \"level\": 0, \"intensity\": 0 }, \"affect\": { \"strength\": 0, \"intensity\": 0 }, \"dream\": { \"strength\": 0, \"intensity\": 0 }, \"is_identity\": true }, \"render\": { \"video\": { \"shown\": false }, \"audio\": { \"source\": \"off\", \"url\": null, \"altitude_coupled\": false } } }" - - group "Cache & playback" [ref=e106]: - - generic [ref=e107]: Cache & playback - - generic [ref=e108]: - - term [ref=e109]: cache - - definition [ref=e110]: 496/41 cached - - term [ref=e111]: active source - - definition [ref=e112]: memory (blob) - - term [ref=e113]: resolution - - definition [ref=e114]: — - - term [ref=e115]: duration - - definition [ref=e116]: — - - term [ref=e117]: loop pos - - definition [ref=e118]: — -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // These assert the two behaviors the operator asked for: - 4 | // 1. zooming plays the morph matching the clip we land on, and - 5 | // 2. after landing the clip is LOCKED — it does not change while parked. - 6 | // - 7 | // The played morph paths are read from window.__hefMorphs (a diagnostic seam in - 8 | // advance()), which is reliable even when the morph is served from an in-memory - 9 | // blob cache. The locked clip is read from the Dev-Mode "#scale-name" readout - 10 | // (" · (i/n)"). - 11 | - 12 | async function boot(page: Page) { - 13 | await page.addInitScript(() => { - 14 | localStorage.setItem("hef.devMode", "1"); - 15 | }); - 16 | await page.goto("/"); - 17 | // Wait for phase-1 preload, then enter via the welcome screen's Launch button - 18 | // (Video defaults on, Audio 2 → the experience is running once we're in). -> 19 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ Error: page.waitForFunction: Test ended. - 20 | await page.locator("#welcome-launch").click(); - 21 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 22 | await expect(page.locator("#scale-name")).toContainText("cosmos"); - 23 | } - 24 | - 25 | async function morphs(page: Page): Promise { - 26 | return await page.evaluate(() => (window as any).__hefMorphs || []); - 27 | } - 28 | - 29 | async function wheelOnStage(page: Page, deltaY: number) { - 30 | await page.locator("#stage").dispatchEvent("wheel", { deltaY }); - 31 | } - 32 | - 33 | // The Audio control is a 0–10 level slider; set it to full + fire `input` directly - 34 | // (headless Chromium relaxes the autoplay gesture requirement). - 35 | async function enableAudio(page: Page) { - 36 | await page.evaluate(() => { - 37 | const c = document.querySelector("#audio") as HTMLInputElement; - 38 | c.value = "10"; - 39 | c.dispatchEvent(new Event("input", { bubbles: true })); - 40 | }); - 41 | } - 42 | - 43 | // The Video toggle gates the base-clip loop (update() only loads media when video is - 44 | // shown). Same hidden-checkbox pattern. - 45 | async function enableVideo(page: Page) { - 46 | await page.evaluate(() => { - 47 | const c = document.querySelector("#visual") as HTMLInputElement; - 48 | c.checked = true; - 49 | c.dispatchEvent(new Event("change", { bubbles: true })); - 50 | }); - 51 | } - 52 | - 53 | test("two audio elements exist for crossfade", async ({ page }) => { - 54 | await boot(page); - 55 | const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length); - 56 | expect(n).toBe(2); - 57 | }); - 58 | - 59 | test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => { - 60 | await boot(page); - 61 | // Reproduce the race: a fade-to-0 in flight, then the same element is brought back - 62 | // up (as happens when a faded idle element becomes the newly-landed active scale). - 63 | // The stale fade must NOT drag it back to 0. - 64 | const vol = await page.evaluate(async () => { - 65 | const a = document.querySelector("#aud") as HTMLAudioElement; - 66 | a.volume = 1; - 67 | (window as any).__hefFade(a, 0, 600); // start a slow fade to 0 - 68 | await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down - 69 | (window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one - 70 | await new Promise((r) => setTimeout(r, 350)); // well past both fades - 71 | return a.volume; - 72 | }); - 73 | expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0 - 74 | }); - 75 | - 76 | test("audio is a 0-10 level dial", async ({ page }) => { - 77 | await boot(page); - 78 | const ctl = await page.evaluate(() => { - 79 | const c = document.querySelector("#audio") as HTMLInputElement; - 80 | return { type: c.type, min: c.min, max: c.max }; - 81 | }); - 82 | expect(ctl.type).toBe("range"); - 83 | expect(ctl.min).toBe("0"); - 84 | expect(ctl.max).toBe("10"); - 85 | }); - 86 | - 87 | // (Entry via the welcome screen — defaults, Launch, control carry-over, State B — - 88 | // is covered in welcome.spec.ts.) - 89 | - 90 | test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => { - 91 | await boot(page); - 92 | await enableAudio(page); // hidden custom toggle → set + dispatch change - 93 | const box = (await page.locator("#dial").boundingBox())!; - 94 | const cx = box.x + box.width / 2, cy = box.y + box.height / 2; - 95 | await page.mouse.move(cx, cy - 30); - 96 | await page.mouse.down(); - 97 | await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent) - 98 | // The currentTime seek is throttled to one rAF; wait for it to land. - 99 | await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); - 100 | const state = await page.evaluate(() => ({ - 101 | t: (document.querySelector("video") as HTMLVideoElement).currentTime, - 102 | ga: (document.querySelector("#aud") as HTMLAudioElement).volume, - 103 | gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume, - 104 | })); - 105 | expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0 - 106 | expect(state.gb).toBeGreaterThan(0); // next scale fading in - 107 | expect(state.ga).toBeLessThan(1); // current scale fading out - 108 | await page.mouse.up(); - 109 | }); - 110 | - 111 | test("wheel auto-scrubs one altitude and locks", async ({ page }) => { - 112 | await boot(page); - 113 | const start = (await page.locator("#scale-name").textContent())!; - 114 | await wheelOnStage(page, 60); // wheel down = descend one altitude - 115 | await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 }); - 116 | const landed = (await page.locator("#scale-name").textContent())!; - 117 | expect(landed).toContain("orbit"); - 118 | await page.waitForTimeout(1500); - 119 | expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/altitude-lock-a-new-volume-6541e-not-silenced-after-landing-/error-context.md b/simulator/e2e/test-results/altitude-lock-a-new-volume-6541e-not-silenced-after-landing-/error-context.md deleted file mode 100644 index 4c28318..0000000 --- a/simulator/e2e/test-results/altitude-lock-a-new-volume-6541e-not-silenced-after-landing-/error-context.md +++ /dev/null @@ -1,267 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: altitude-lock.spec.ts >> a new volume fade supersedes a stale one (audio not silenced after landing) -- Location: tests/altitude-lock.spec.ts:59:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" [checked] - - generic [ref=e69]: Dev Mode - - generic [ref=e70]: - - group "Audio diagnostics" [ref=e71]: - - generic [ref=e72]: Audio diagnostics - - generic [ref=e73]: "audio: off (level 0)" - - paragraph [ref=e74]: Native player — tests the soundtrack file directly (bypasses all app code). If it plays here but the Audio toggle is silent, it's a code/autoplay issue, not the file. - - group "Pool — pick a clip" [ref=e76]: - - generic [ref=e77]: Pool — pick a clip - - combobox "Choose a clip from this altitude's pool" [ref=e78]: - - option "cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)" [selected] - - option "cosmos_galaxies · Flying Through Galaxies (NASA SVS)" - - option "cosmos_orion · Orion Nebula — visible/IR flythrough (NASA SVS)" - - option "cosmos_tarantula · Tarantula Nebula (30 Doradus) zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_westerlund · Flight through star cluster Westerlund 2 (ESA/Hubble)" - - option "cosmos_southernring · Southern Ring Nebula zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_carina_eso · Carina Nebula zoom (ESO)" - - button "🎲 Re-roll random" [ref=e79] [cursor=pointer] - - group "Active clip" [ref=e80]: - - generic [ref=e81]: Active clip - - generic [ref=e82]: - - term [ref=e83]: id - - definition [ref=e84]: cosmos - - term [ref=e85]: title - - definition [ref=e86]: Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - term [ref=e87]: source - - definition [ref=e88]: NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); 4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough. - - term [ref=e89]: license - - definition [ref=e90]: CC-BY-class — credit NASA, ESA, CSA, STScI - - term [ref=e91]: base_file - - definition [ref=e92]: cosmos/base.mp4 - - group "Annotations & affect" [ref=e93]: - - generic [ref=e94]: Annotations & affect - - generic [ref=e95]: - - generic [ref=e96]: detected.nebula · sal 4 · 4 tiers - - generic [ref=e97]: detected.star · sal 2 · 4 tiers - - generic [ref=e98]: measure.distance · min L3 · 1 tier - - generic [ref=e99]: feel.wonder · min L1 · 4 tiers - - generic [ref=e100]: feel.sublime · min L2 · 4 tiers - - generic [ref=e101]: feel.vastness · min L3 · 4 tiers - - generic [ref=e102]: feel.insignificance · min L4 · 4 tiers - - group "RenderPlan" [ref=e103]: - - generic [ref=e104]: RenderPlan - - generic [ref=e105]: "{ \"plan\": { \"grade\": { \"tone\": 0 }, \"overlay\": { \"level\": 0, \"intensity\": 0 }, \"affect\": { \"strength\": 0, \"intensity\": 0 }, \"dream\": { \"strength\": 0, \"intensity\": 0 }, \"is_identity\": true }, \"render\": { \"video\": { \"shown\": false }, \"audio\": { \"source\": \"off\", \"url\": null, \"altitude_coupled\": false } } }" - - group "Cache & playback" [ref=e106]: - - generic [ref=e107]: Cache & playback - - generic [ref=e108]: - - term [ref=e109]: cache - - definition [ref=e110]: 497/41 cached - - term [ref=e111]: active source - - definition [ref=e112]: memory (blob) - - term [ref=e113]: resolution - - definition [ref=e114]: — - - term [ref=e115]: duration - - definition [ref=e116]: — - - term [ref=e117]: loop pos - - definition [ref=e118]: — -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // These assert the two behaviors the operator asked for: - 4 | // 1. zooming plays the morph matching the clip we land on, and - 5 | // 2. after landing the clip is LOCKED — it does not change while parked. - 6 | // - 7 | // The played morph paths are read from window.__hefMorphs (a diagnostic seam in - 8 | // advance()), which is reliable even when the morph is served from an in-memory - 9 | // blob cache. The locked clip is read from the Dev-Mode "#scale-name" readout - 10 | // (" · (i/n)"). - 11 | - 12 | async function boot(page: Page) { - 13 | await page.addInitScript(() => { - 14 | localStorage.setItem("hef.devMode", "1"); - 15 | }); - 16 | await page.goto("/"); - 17 | // Wait for phase-1 preload, then enter via the welcome screen's Launch button - 18 | // (Video defaults on, Audio 2 → the experience is running once we're in). -> 19 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 20 | await page.locator("#welcome-launch").click(); - 21 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 22 | await expect(page.locator("#scale-name")).toContainText("cosmos"); - 23 | } - 24 | - 25 | async function morphs(page: Page): Promise { - 26 | return await page.evaluate(() => (window as any).__hefMorphs || []); - 27 | } - 28 | - 29 | async function wheelOnStage(page: Page, deltaY: number) { - 30 | await page.locator("#stage").dispatchEvent("wheel", { deltaY }); - 31 | } - 32 | - 33 | // The Audio control is a 0–10 level slider; set it to full + fire `input` directly - 34 | // (headless Chromium relaxes the autoplay gesture requirement). - 35 | async function enableAudio(page: Page) { - 36 | await page.evaluate(() => { - 37 | const c = document.querySelector("#audio") as HTMLInputElement; - 38 | c.value = "10"; - 39 | c.dispatchEvent(new Event("input", { bubbles: true })); - 40 | }); - 41 | } - 42 | - 43 | // The Video toggle gates the base-clip loop (update() only loads media when video is - 44 | // shown). Same hidden-checkbox pattern. - 45 | async function enableVideo(page: Page) { - 46 | await page.evaluate(() => { - 47 | const c = document.querySelector("#visual") as HTMLInputElement; - 48 | c.checked = true; - 49 | c.dispatchEvent(new Event("change", { bubbles: true })); - 50 | }); - 51 | } - 52 | - 53 | test("two audio elements exist for crossfade", async ({ page }) => { - 54 | await boot(page); - 55 | const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length); - 56 | expect(n).toBe(2); - 57 | }); - 58 | - 59 | test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => { - 60 | await boot(page); - 61 | // Reproduce the race: a fade-to-0 in flight, then the same element is brought back - 62 | // up (as happens when a faded idle element becomes the newly-landed active scale). - 63 | // The stale fade must NOT drag it back to 0. - 64 | const vol = await page.evaluate(async () => { - 65 | const a = document.querySelector("#aud") as HTMLAudioElement; - 66 | a.volume = 1; - 67 | (window as any).__hefFade(a, 0, 600); // start a slow fade to 0 - 68 | await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down - 69 | (window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one - 70 | await new Promise((r) => setTimeout(r, 350)); // well past both fades - 71 | return a.volume; - 72 | }); - 73 | expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0 - 74 | }); - 75 | - 76 | test("audio is a 0-10 level dial", async ({ page }) => { - 77 | await boot(page); - 78 | const ctl = await page.evaluate(() => { - 79 | const c = document.querySelector("#audio") as HTMLInputElement; - 80 | return { type: c.type, min: c.min, max: c.max }; - 81 | }); - 82 | expect(ctl.type).toBe("range"); - 83 | expect(ctl.min).toBe("0"); - 84 | expect(ctl.max).toBe("10"); - 85 | }); - 86 | - 87 | // (Entry via the welcome screen — defaults, Launch, control carry-over, State B — - 88 | // is covered in welcome.spec.ts.) - 89 | - 90 | test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => { - 91 | await boot(page); - 92 | await enableAudio(page); // hidden custom toggle → set + dispatch change - 93 | const box = (await page.locator("#dial").boundingBox())!; - 94 | const cx = box.x + box.width / 2, cy = box.y + box.height / 2; - 95 | await page.mouse.move(cx, cy - 30); - 96 | await page.mouse.down(); - 97 | await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent) - 98 | // The currentTime seek is throttled to one rAF; wait for it to land. - 99 | await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); - 100 | const state = await page.evaluate(() => ({ - 101 | t: (document.querySelector("video") as HTMLVideoElement).currentTime, - 102 | ga: (document.querySelector("#aud") as HTMLAudioElement).volume, - 103 | gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume, - 104 | })); - 105 | expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0 - 106 | expect(state.gb).toBeGreaterThan(0); // next scale fading in - 107 | expect(state.ga).toBeLessThan(1); // current scale fading out - 108 | await page.mouse.up(); - 109 | }); - 110 | - 111 | test("wheel auto-scrubs one altitude and locks", async ({ page }) => { - 112 | await boot(page); - 113 | const start = (await page.locator("#scale-name").textContent())!; - 114 | await wheelOnStage(page, 60); // wheel down = descend one altitude - 115 | await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 }); - 116 | const landed = (await page.locator("#scale-name").textContent())!; - 117 | expect(landed).toContain("orbit"); - 118 | await page.waitForTimeout(1500); - 119 | expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/altitude-lock-audio-is-a-0-10-level-dial/error-context.md b/simulator/e2e/test-results/altitude-lock-audio-is-a-0-10-level-dial/error-context.md deleted file mode 100644 index 5021f90..0000000 --- a/simulator/e2e/test-results/altitude-lock-audio-is-a-0-10-level-dial/error-context.md +++ /dev/null @@ -1,267 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: altitude-lock.spec.ts >> audio is a 0-10 level dial -- Location: tests/altitude-lock.spec.ts:76:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" [checked] - - generic [ref=e69]: Dev Mode - - generic [ref=e70]: - - group "Audio diagnostics" [ref=e71]: - - generic [ref=e72]: Audio diagnostics - - generic [ref=e73]: "audio: off (level 0)" - - paragraph [ref=e74]: Native player — tests the soundtrack file directly (bypasses all app code). If it plays here but the Audio toggle is silent, it's a code/autoplay issue, not the file. - - group "Pool — pick a clip" [ref=e76]: - - generic [ref=e77]: Pool — pick a clip - - combobox "Choose a clip from this altitude's pool" [ref=e78]: - - option "cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)" [selected] - - option "cosmos_galaxies · Flying Through Galaxies (NASA SVS)" - - option "cosmos_orion · Orion Nebula — visible/IR flythrough (NASA SVS)" - - option "cosmos_tarantula · Tarantula Nebula (30 Doradus) zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_westerlund · Flight through star cluster Westerlund 2 (ESA/Hubble)" - - option "cosmos_southernring · Southern Ring Nebula zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_carina_eso · Carina Nebula zoom (ESO)" - - button "🎲 Re-roll random" [ref=e79] [cursor=pointer] - - group "Active clip" [ref=e80]: - - generic [ref=e81]: Active clip - - generic [ref=e82]: - - term [ref=e83]: id - - definition [ref=e84]: cosmos - - term [ref=e85]: title - - definition [ref=e86]: Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - term [ref=e87]: source - - definition [ref=e88]: NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); 4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough. - - term [ref=e89]: license - - definition [ref=e90]: CC-BY-class — credit NASA, ESA, CSA, STScI - - term [ref=e91]: base_file - - definition [ref=e92]: cosmos/base.mp4 - - group "Annotations & affect" [ref=e93]: - - generic [ref=e94]: Annotations & affect - - generic [ref=e95]: - - generic [ref=e96]: detected.nebula · sal 4 · 4 tiers - - generic [ref=e97]: detected.star · sal 2 · 4 tiers - - generic [ref=e98]: measure.distance · min L3 · 1 tier - - generic [ref=e99]: feel.wonder · min L1 · 4 tiers - - generic [ref=e100]: feel.sublime · min L2 · 4 tiers - - generic [ref=e101]: feel.vastness · min L3 · 4 tiers - - generic [ref=e102]: feel.insignificance · min L4 · 4 tiers - - group "RenderPlan" [ref=e103]: - - generic [ref=e104]: RenderPlan - - generic [ref=e105]: "{ \"plan\": { \"grade\": { \"tone\": 0 }, \"overlay\": { \"level\": 0, \"intensity\": 0 }, \"affect\": { \"strength\": 0, \"intensity\": 0 }, \"dream\": { \"strength\": 0, \"intensity\": 0 }, \"is_identity\": true }, \"render\": { \"video\": { \"shown\": false }, \"audio\": { \"source\": \"off\", \"url\": null, \"altitude_coupled\": false } } }" - - group "Cache & playback" [ref=e106]: - - generic [ref=e107]: Cache & playback - - generic [ref=e108]: - - term [ref=e109]: cache - - definition [ref=e110]: 496/41 cached - - term [ref=e111]: active source - - definition [ref=e112]: memory (blob) - - term [ref=e113]: resolution - - definition [ref=e114]: — - - term [ref=e115]: duration - - definition [ref=e116]: — - - term [ref=e117]: loop pos - - definition [ref=e118]: — -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // These assert the two behaviors the operator asked for: - 4 | // 1. zooming plays the morph matching the clip we land on, and - 5 | // 2. after landing the clip is LOCKED — it does not change while parked. - 6 | // - 7 | // The played morph paths are read from window.__hefMorphs (a diagnostic seam in - 8 | // advance()), which is reliable even when the morph is served from an in-memory - 9 | // blob cache. The locked clip is read from the Dev-Mode "#scale-name" readout - 10 | // (" · (i/n)"). - 11 | - 12 | async function boot(page: Page) { - 13 | await page.addInitScript(() => { - 14 | localStorage.setItem("hef.devMode", "1"); - 15 | }); - 16 | await page.goto("/"); - 17 | // Wait for phase-1 preload, then enter via the welcome screen's Launch button - 18 | // (Video defaults on, Audio 2 → the experience is running once we're in). -> 19 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 20 | await page.locator("#welcome-launch").click(); - 21 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 22 | await expect(page.locator("#scale-name")).toContainText("cosmos"); - 23 | } - 24 | - 25 | async function morphs(page: Page): Promise { - 26 | return await page.evaluate(() => (window as any).__hefMorphs || []); - 27 | } - 28 | - 29 | async function wheelOnStage(page: Page, deltaY: number) { - 30 | await page.locator("#stage").dispatchEvent("wheel", { deltaY }); - 31 | } - 32 | - 33 | // The Audio control is a 0–10 level slider; set it to full + fire `input` directly - 34 | // (headless Chromium relaxes the autoplay gesture requirement). - 35 | async function enableAudio(page: Page) { - 36 | await page.evaluate(() => { - 37 | const c = document.querySelector("#audio") as HTMLInputElement; - 38 | c.value = "10"; - 39 | c.dispatchEvent(new Event("input", { bubbles: true })); - 40 | }); - 41 | } - 42 | - 43 | // The Video toggle gates the base-clip loop (update() only loads media when video is - 44 | // shown). Same hidden-checkbox pattern. - 45 | async function enableVideo(page: Page) { - 46 | await page.evaluate(() => { - 47 | const c = document.querySelector("#visual") as HTMLInputElement; - 48 | c.checked = true; - 49 | c.dispatchEvent(new Event("change", { bubbles: true })); - 50 | }); - 51 | } - 52 | - 53 | test("two audio elements exist for crossfade", async ({ page }) => { - 54 | await boot(page); - 55 | const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length); - 56 | expect(n).toBe(2); - 57 | }); - 58 | - 59 | test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => { - 60 | await boot(page); - 61 | // Reproduce the race: a fade-to-0 in flight, then the same element is brought back - 62 | // up (as happens when a faded idle element becomes the newly-landed active scale). - 63 | // The stale fade must NOT drag it back to 0. - 64 | const vol = await page.evaluate(async () => { - 65 | const a = document.querySelector("#aud") as HTMLAudioElement; - 66 | a.volume = 1; - 67 | (window as any).__hefFade(a, 0, 600); // start a slow fade to 0 - 68 | await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down - 69 | (window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one - 70 | await new Promise((r) => setTimeout(r, 350)); // well past both fades - 71 | return a.volume; - 72 | }); - 73 | expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0 - 74 | }); - 75 | - 76 | test("audio is a 0-10 level dial", async ({ page }) => { - 77 | await boot(page); - 78 | const ctl = await page.evaluate(() => { - 79 | const c = document.querySelector("#audio") as HTMLInputElement; - 80 | return { type: c.type, min: c.min, max: c.max }; - 81 | }); - 82 | expect(ctl.type).toBe("range"); - 83 | expect(ctl.min).toBe("0"); - 84 | expect(ctl.max).toBe("10"); - 85 | }); - 86 | - 87 | // (Entry via the welcome screen — defaults, Launch, control carry-over, State B — - 88 | // is covered in welcome.spec.ts.) - 89 | - 90 | test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => { - 91 | await boot(page); - 92 | await enableAudio(page); // hidden custom toggle → set + dispatch change - 93 | const box = (await page.locator("#dial").boundingBox())!; - 94 | const cx = box.x + box.width / 2, cy = box.y + box.height / 2; - 95 | await page.mouse.move(cx, cy - 30); - 96 | await page.mouse.down(); - 97 | await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent) - 98 | // The currentTime seek is throttled to one rAF; wait for it to land. - 99 | await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); - 100 | const state = await page.evaluate(() => ({ - 101 | t: (document.querySelector("video") as HTMLVideoElement).currentTime, - 102 | ga: (document.querySelector("#aud") as HTMLAudioElement).volume, - 103 | gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume, - 104 | })); - 105 | expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0 - 106 | expect(state.gb).toBeGreaterThan(0); // next scale fading in - 107 | expect(state.ga).toBeLessThan(1); // current scale fading out - 108 | await page.mouse.up(); - 109 | }); - 110 | - 111 | test("wheel auto-scrubs one altitude and locks", async ({ page }) => { - 112 | await boot(page); - 113 | const start = (await page.locator("#scale-name").textContent())!; - 114 | await wheelOnStage(page, 60); // wheel down = descend one altitude - 115 | await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 }); - 116 | const landed = (await page.locator("#scale-name").textContent())!; - 117 | expect(landed).toContain("orbit"); - 118 | await page.waitForTimeout(1500); - 119 | expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/altitude-lock-auto-scrub-s-cb764-tep-jump-takes-2x-a-1-step-/error-context.md b/simulator/e2e/test-results/altitude-lock-auto-scrub-s-cb764-tep-jump-takes-2x-a-1-step-/error-context.md deleted file mode 100644 index 8b18286..0000000 --- a/simulator/e2e/test-results/altitude-lock-auto-scrub-s-cb764-tep-jump-takes-2x-a-1-step-/error-context.md +++ /dev/null @@ -1,267 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: altitude-lock.spec.ts >> auto-scrub speed is constant per altitude (a 2-step jump takes ~2x a 1-step) -- Location: tests/altitude-lock.spec.ts:122:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" [checked] - - generic [ref=e69]: Dev Mode - - generic [ref=e70]: - - group "Audio diagnostics" [ref=e71]: - - generic [ref=e72]: Audio diagnostics - - generic [ref=e73]: "audio: off (level 0)" - - paragraph [ref=e74]: Native player — tests the soundtrack file directly (bypasses all app code). If it plays here but the Audio toggle is silent, it's a code/autoplay issue, not the file. - - group "Pool — pick a clip" [ref=e76]: - - generic [ref=e77]: Pool — pick a clip - - combobox "Choose a clip from this altitude's pool" [ref=e78]: - - option "cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)" [selected] - - option "cosmos_galaxies · Flying Through Galaxies (NASA SVS)" - - option "cosmos_orion · Orion Nebula — visible/IR flythrough (NASA SVS)" - - option "cosmos_tarantula · Tarantula Nebula (30 Doradus) zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_westerlund · Flight through star cluster Westerlund 2 (ESA/Hubble)" - - option "cosmos_southernring · Southern Ring Nebula zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_carina_eso · Carina Nebula zoom (ESO)" - - button "🎲 Re-roll random" [ref=e79] [cursor=pointer] - - group "Active clip" [ref=e80]: - - generic [ref=e81]: Active clip - - generic [ref=e82]: - - term [ref=e83]: id - - definition [ref=e84]: cosmos - - term [ref=e85]: title - - definition [ref=e86]: Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - term [ref=e87]: source - - definition [ref=e88]: NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); 4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough. - - term [ref=e89]: license - - definition [ref=e90]: CC-BY-class — credit NASA, ESA, CSA, STScI - - term [ref=e91]: base_file - - definition [ref=e92]: cosmos/base.mp4 - - group "Annotations & affect" [ref=e93]: - - generic [ref=e94]: Annotations & affect - - generic [ref=e95]: - - generic [ref=e96]: detected.nebula · sal 4 · 4 tiers - - generic [ref=e97]: detected.star · sal 2 · 4 tiers - - generic [ref=e98]: measure.distance · min L3 · 1 tier - - generic [ref=e99]: feel.wonder · min L1 · 4 tiers - - generic [ref=e100]: feel.sublime · min L2 · 4 tiers - - generic [ref=e101]: feel.vastness · min L3 · 4 tiers - - generic [ref=e102]: feel.insignificance · min L4 · 4 tiers - - group "RenderPlan" [ref=e103]: - - generic [ref=e104]: RenderPlan - - generic [ref=e105]: "{ \"plan\": { \"grade\": { \"tone\": 0 }, \"overlay\": { \"level\": 0, \"intensity\": 0 }, \"affect\": { \"strength\": 0, \"intensity\": 0 }, \"dream\": { \"strength\": 0, \"intensity\": 0 }, \"is_identity\": true }, \"render\": { \"video\": { \"shown\": false }, \"audio\": { \"source\": \"off\", \"url\": null, \"altitude_coupled\": false } } }" - - group "Cache & playback" [ref=e106]: - - generic [ref=e107]: Cache & playback - - generic [ref=e108]: - - term [ref=e109]: cache - - definition [ref=e110]: 496/41 cached - - term [ref=e111]: active source - - definition [ref=e112]: memory (blob) - - term [ref=e113]: resolution - - definition [ref=e114]: — - - term [ref=e115]: duration - - definition [ref=e116]: — - - term [ref=e117]: loop pos - - definition [ref=e118]: — -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // These assert the two behaviors the operator asked for: - 4 | // 1. zooming plays the morph matching the clip we land on, and - 5 | // 2. after landing the clip is LOCKED — it does not change while parked. - 6 | // - 7 | // The played morph paths are read from window.__hefMorphs (a diagnostic seam in - 8 | // advance()), which is reliable even when the morph is served from an in-memory - 9 | // blob cache. The locked clip is read from the Dev-Mode "#scale-name" readout - 10 | // (" · (i/n)"). - 11 | - 12 | async function boot(page: Page) { - 13 | await page.addInitScript(() => { - 14 | localStorage.setItem("hef.devMode", "1"); - 15 | }); - 16 | await page.goto("/"); - 17 | // Wait for phase-1 preload, then enter via the welcome screen's Launch button - 18 | // (Video defaults on, Audio 2 → the experience is running once we're in). -> 19 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 20 | await page.locator("#welcome-launch").click(); - 21 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 22 | await expect(page.locator("#scale-name")).toContainText("cosmos"); - 23 | } - 24 | - 25 | async function morphs(page: Page): Promise { - 26 | return await page.evaluate(() => (window as any).__hefMorphs || []); - 27 | } - 28 | - 29 | async function wheelOnStage(page: Page, deltaY: number) { - 30 | await page.locator("#stage").dispatchEvent("wheel", { deltaY }); - 31 | } - 32 | - 33 | // The Audio control is a 0–10 level slider; set it to full + fire `input` directly - 34 | // (headless Chromium relaxes the autoplay gesture requirement). - 35 | async function enableAudio(page: Page) { - 36 | await page.evaluate(() => { - 37 | const c = document.querySelector("#audio") as HTMLInputElement; - 38 | c.value = "10"; - 39 | c.dispatchEvent(new Event("input", { bubbles: true })); - 40 | }); - 41 | } - 42 | - 43 | // The Video toggle gates the base-clip loop (update() only loads media when video is - 44 | // shown). Same hidden-checkbox pattern. - 45 | async function enableVideo(page: Page) { - 46 | await page.evaluate(() => { - 47 | const c = document.querySelector("#visual") as HTMLInputElement; - 48 | c.checked = true; - 49 | c.dispatchEvent(new Event("change", { bubbles: true })); - 50 | }); - 51 | } - 52 | - 53 | test("two audio elements exist for crossfade", async ({ page }) => { - 54 | await boot(page); - 55 | const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length); - 56 | expect(n).toBe(2); - 57 | }); - 58 | - 59 | test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => { - 60 | await boot(page); - 61 | // Reproduce the race: a fade-to-0 in flight, then the same element is brought back - 62 | // up (as happens when a faded idle element becomes the newly-landed active scale). - 63 | // The stale fade must NOT drag it back to 0. - 64 | const vol = await page.evaluate(async () => { - 65 | const a = document.querySelector("#aud") as HTMLAudioElement; - 66 | a.volume = 1; - 67 | (window as any).__hefFade(a, 0, 600); // start a slow fade to 0 - 68 | await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down - 69 | (window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one - 70 | await new Promise((r) => setTimeout(r, 350)); // well past both fades - 71 | return a.volume; - 72 | }); - 73 | expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0 - 74 | }); - 75 | - 76 | test("audio is a 0-10 level dial", async ({ page }) => { - 77 | await boot(page); - 78 | const ctl = await page.evaluate(() => { - 79 | const c = document.querySelector("#audio") as HTMLInputElement; - 80 | return { type: c.type, min: c.min, max: c.max }; - 81 | }); - 82 | expect(ctl.type).toBe("range"); - 83 | expect(ctl.min).toBe("0"); - 84 | expect(ctl.max).toBe("10"); - 85 | }); - 86 | - 87 | // (Entry via the welcome screen — defaults, Launch, control carry-over, State B — - 88 | // is covered in welcome.spec.ts.) - 89 | - 90 | test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => { - 91 | await boot(page); - 92 | await enableAudio(page); // hidden custom toggle → set + dispatch change - 93 | const box = (await page.locator("#dial").boundingBox())!; - 94 | const cx = box.x + box.width / 2, cy = box.y + box.height / 2; - 95 | await page.mouse.move(cx, cy - 30); - 96 | await page.mouse.down(); - 97 | await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent) - 98 | // The currentTime seek is throttled to one rAF; wait for it to land. - 99 | await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); - 100 | const state = await page.evaluate(() => ({ - 101 | t: (document.querySelector("video") as HTMLVideoElement).currentTime, - 102 | ga: (document.querySelector("#aud") as HTMLAudioElement).volume, - 103 | gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume, - 104 | })); - 105 | expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0 - 106 | expect(state.gb).toBeGreaterThan(0); // next scale fading in - 107 | expect(state.ga).toBeLessThan(1); // current scale fading out - 108 | await page.mouse.up(); - 109 | }); - 110 | - 111 | test("wheel auto-scrubs one altitude and locks", async ({ page }) => { - 112 | await boot(page); - 113 | const start = (await page.locator("#scale-name").textContent())!; - 114 | await wheelOnStage(page, 60); // wheel down = descend one altitude - 115 | await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 }); - 116 | const landed = (await page.locator("#scale-name").textContent())!; - 117 | expect(landed).toContain("orbit"); - 118 | await page.waitForTimeout(1500); - 119 | expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/altitude-lock-dragging-the-07d2b-currentTime-and-audio-gains/error-context.md b/simulator/e2e/test-results/altitude-lock-dragging-the-07d2b-currentTime-and-audio-gains/error-context.md deleted file mode 100644 index d7db140..0000000 --- a/simulator/e2e/test-results/altitude-lock-dragging-the-07d2b-currentTime-and-audio-gains/error-context.md +++ /dev/null @@ -1,267 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: altitude-lock.spec.ts >> dragging the dial scrubs morph currentTime and audio gains -- Location: tests/altitude-lock.spec.ts:90:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" [checked] - - generic [ref=e69]: Dev Mode - - generic [ref=e70]: - - group "Audio diagnostics" [ref=e71]: - - generic [ref=e72]: Audio diagnostics - - generic [ref=e73]: "audio: off (level 0)" - - paragraph [ref=e74]: Native player — tests the soundtrack file directly (bypasses all app code). If it plays here but the Audio toggle is silent, it's a code/autoplay issue, not the file. - - group "Pool — pick a clip" [ref=e76]: - - generic [ref=e77]: Pool — pick a clip - - combobox "Choose a clip from this altitude's pool" [ref=e78]: - - option "cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)" [selected] - - option "cosmos_galaxies · Flying Through Galaxies (NASA SVS)" - - option "cosmos_orion · Orion Nebula — visible/IR flythrough (NASA SVS)" - - option "cosmos_tarantula · Tarantula Nebula (30 Doradus) zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_westerlund · Flight through star cluster Westerlund 2 (ESA/Hubble)" - - option "cosmos_southernring · Southern Ring Nebula zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_carina_eso · Carina Nebula zoom (ESO)" - - button "🎲 Re-roll random" [ref=e79] [cursor=pointer] - - group "Active clip" [ref=e80]: - - generic [ref=e81]: Active clip - - generic [ref=e82]: - - term [ref=e83]: id - - definition [ref=e84]: cosmos - - term [ref=e85]: title - - definition [ref=e86]: Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - term [ref=e87]: source - - definition [ref=e88]: NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); 4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough. - - term [ref=e89]: license - - definition [ref=e90]: CC-BY-class — credit NASA, ESA, CSA, STScI - - term [ref=e91]: base_file - - definition [ref=e92]: cosmos/base.mp4 - - group "Annotations & affect" [ref=e93]: - - generic [ref=e94]: Annotations & affect - - generic [ref=e95]: - - generic [ref=e96]: detected.nebula · sal 4 · 4 tiers - - generic [ref=e97]: detected.star · sal 2 · 4 tiers - - generic [ref=e98]: measure.distance · min L3 · 1 tier - - generic [ref=e99]: feel.wonder · min L1 · 4 tiers - - generic [ref=e100]: feel.sublime · min L2 · 4 tiers - - generic [ref=e101]: feel.vastness · min L3 · 4 tiers - - generic [ref=e102]: feel.insignificance · min L4 · 4 tiers - - group "RenderPlan" [ref=e103]: - - generic [ref=e104]: RenderPlan - - generic [ref=e105]: "{ \"plan\": { \"grade\": { \"tone\": 0 }, \"overlay\": { \"level\": 0, \"intensity\": 0 }, \"affect\": { \"strength\": 0, \"intensity\": 0 }, \"dream\": { \"strength\": 0, \"intensity\": 0 }, \"is_identity\": true }, \"render\": { \"video\": { \"shown\": false }, \"audio\": { \"source\": \"off\", \"url\": null, \"altitude_coupled\": false } } }" - - group "Cache & playback" [ref=e106]: - - generic [ref=e107]: Cache & playback - - generic [ref=e108]: - - term [ref=e109]: cache - - definition [ref=e110]: 497/41 cached - - term [ref=e111]: active source - - definition [ref=e112]: memory (blob) - - term [ref=e113]: resolution - - definition [ref=e114]: — - - term [ref=e115]: duration - - definition [ref=e116]: — - - term [ref=e117]: loop pos - - definition [ref=e118]: — -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // These assert the two behaviors the operator asked for: - 4 | // 1. zooming plays the morph matching the clip we land on, and - 5 | // 2. after landing the clip is LOCKED — it does not change while parked. - 6 | // - 7 | // The played morph paths are read from window.__hefMorphs (a diagnostic seam in - 8 | // advance()), which is reliable even when the morph is served from an in-memory - 9 | // blob cache. The locked clip is read from the Dev-Mode "#scale-name" readout - 10 | // (" · (i/n)"). - 11 | - 12 | async function boot(page: Page) { - 13 | await page.addInitScript(() => { - 14 | localStorage.setItem("hef.devMode", "1"); - 15 | }); - 16 | await page.goto("/"); - 17 | // Wait for phase-1 preload, then enter via the welcome screen's Launch button - 18 | // (Video defaults on, Audio 2 → the experience is running once we're in). -> 19 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 20 | await page.locator("#welcome-launch").click(); - 21 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 22 | await expect(page.locator("#scale-name")).toContainText("cosmos"); - 23 | } - 24 | - 25 | async function morphs(page: Page): Promise { - 26 | return await page.evaluate(() => (window as any).__hefMorphs || []); - 27 | } - 28 | - 29 | async function wheelOnStage(page: Page, deltaY: number) { - 30 | await page.locator("#stage").dispatchEvent("wheel", { deltaY }); - 31 | } - 32 | - 33 | // The Audio control is a 0–10 level slider; set it to full + fire `input` directly - 34 | // (headless Chromium relaxes the autoplay gesture requirement). - 35 | async function enableAudio(page: Page) { - 36 | await page.evaluate(() => { - 37 | const c = document.querySelector("#audio") as HTMLInputElement; - 38 | c.value = "10"; - 39 | c.dispatchEvent(new Event("input", { bubbles: true })); - 40 | }); - 41 | } - 42 | - 43 | // The Video toggle gates the base-clip loop (update() only loads media when video is - 44 | // shown). Same hidden-checkbox pattern. - 45 | async function enableVideo(page: Page) { - 46 | await page.evaluate(() => { - 47 | const c = document.querySelector("#visual") as HTMLInputElement; - 48 | c.checked = true; - 49 | c.dispatchEvent(new Event("change", { bubbles: true })); - 50 | }); - 51 | } - 52 | - 53 | test("two audio elements exist for crossfade", async ({ page }) => { - 54 | await boot(page); - 55 | const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length); - 56 | expect(n).toBe(2); - 57 | }); - 58 | - 59 | test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => { - 60 | await boot(page); - 61 | // Reproduce the race: a fade-to-0 in flight, then the same element is brought back - 62 | // up (as happens when a faded idle element becomes the newly-landed active scale). - 63 | // The stale fade must NOT drag it back to 0. - 64 | const vol = await page.evaluate(async () => { - 65 | const a = document.querySelector("#aud") as HTMLAudioElement; - 66 | a.volume = 1; - 67 | (window as any).__hefFade(a, 0, 600); // start a slow fade to 0 - 68 | await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down - 69 | (window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one - 70 | await new Promise((r) => setTimeout(r, 350)); // well past both fades - 71 | return a.volume; - 72 | }); - 73 | expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0 - 74 | }); - 75 | - 76 | test("audio is a 0-10 level dial", async ({ page }) => { - 77 | await boot(page); - 78 | const ctl = await page.evaluate(() => { - 79 | const c = document.querySelector("#audio") as HTMLInputElement; - 80 | return { type: c.type, min: c.min, max: c.max }; - 81 | }); - 82 | expect(ctl.type).toBe("range"); - 83 | expect(ctl.min).toBe("0"); - 84 | expect(ctl.max).toBe("10"); - 85 | }); - 86 | - 87 | // (Entry via the welcome screen — defaults, Launch, control carry-over, State B — - 88 | // is covered in welcome.spec.ts.) - 89 | - 90 | test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => { - 91 | await boot(page); - 92 | await enableAudio(page); // hidden custom toggle → set + dispatch change - 93 | const box = (await page.locator("#dial").boundingBox())!; - 94 | const cx = box.x + box.width / 2, cy = box.y + box.height / 2; - 95 | await page.mouse.move(cx, cy - 30); - 96 | await page.mouse.down(); - 97 | await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent) - 98 | // The currentTime seek is throttled to one rAF; wait for it to land. - 99 | await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); - 100 | const state = await page.evaluate(() => ({ - 101 | t: (document.querySelector("video") as HTMLVideoElement).currentTime, - 102 | ga: (document.querySelector("#aud") as HTMLAudioElement).volume, - 103 | gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume, - 104 | })); - 105 | expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0 - 106 | expect(state.gb).toBeGreaterThan(0); // next scale fading in - 107 | expect(state.ga).toBeLessThan(1); // current scale fading out - 108 | await page.mouse.up(); - 109 | }); - 110 | - 111 | test("wheel auto-scrubs one altitude and locks", async ({ page }) => { - 112 | await boot(page); - 113 | const start = (await page.locator("#scale-name").textContent())!; - 114 | await wheelOnStage(page, 60); // wheel down = descend one altitude - 115 | await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 }); - 116 | const landed = (await page.locator("#scale-name").textContent())!; - 117 | expect(landed).toContain("orbit"); - 118 | await page.waitForTimeout(1500); - 119 | expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/altitude-lock-two-audio-elements-exist-for-crossfade/error-context.md b/simulator/e2e/test-results/altitude-lock-two-audio-elements-exist-for-crossfade/error-context.md deleted file mode 100644 index 3e76f39..0000000 --- a/simulator/e2e/test-results/altitude-lock-two-audio-elements-exist-for-crossfade/error-context.md +++ /dev/null @@ -1,147 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: altitude-lock.spec.ts >> two audio elements exist for crossfade -- Location: tests/altitude-lock.spec.ts:53:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [ref=e2]: "{\"detail\":\"Not Found\"}" -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // These assert the two behaviors the operator asked for: - 4 | // 1. zooming plays the morph matching the clip we land on, and - 5 | // 2. after landing the clip is LOCKED — it does not change while parked. - 6 | // - 7 | // The played morph paths are read from window.__hefMorphs (a diagnostic seam in - 8 | // advance()), which is reliable even when the morph is served from an in-memory - 9 | // blob cache. The locked clip is read from the Dev-Mode "#scale-name" readout - 10 | // (" · (i/n)"). - 11 | - 12 | async function boot(page: Page) { - 13 | await page.addInitScript(() => { - 14 | localStorage.setItem("hef.devMode", "1"); - 15 | }); - 16 | await page.goto("/"); - 17 | // Wait for phase-1 preload, then enter via the welcome screen's Launch button - 18 | // (Video defaults on, Audio 2 → the experience is running once we're in). -> 19 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 20 | await page.locator("#welcome-launch").click(); - 21 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 22 | await expect(page.locator("#scale-name")).toContainText("cosmos"); - 23 | } - 24 | - 25 | async function morphs(page: Page): Promise { - 26 | return await page.evaluate(() => (window as any).__hefMorphs || []); - 27 | } - 28 | - 29 | async function wheelOnStage(page: Page, deltaY: number) { - 30 | await page.locator("#stage").dispatchEvent("wheel", { deltaY }); - 31 | } - 32 | - 33 | // The Audio control is a 0–10 level slider; set it to full + fire `input` directly - 34 | // (headless Chromium relaxes the autoplay gesture requirement). - 35 | async function enableAudio(page: Page) { - 36 | await page.evaluate(() => { - 37 | const c = document.querySelector("#audio") as HTMLInputElement; - 38 | c.value = "10"; - 39 | c.dispatchEvent(new Event("input", { bubbles: true })); - 40 | }); - 41 | } - 42 | - 43 | // The Video toggle gates the base-clip loop (update() only loads media when video is - 44 | // shown). Same hidden-checkbox pattern. - 45 | async function enableVideo(page: Page) { - 46 | await page.evaluate(() => { - 47 | const c = document.querySelector("#visual") as HTMLInputElement; - 48 | c.checked = true; - 49 | c.dispatchEvent(new Event("change", { bubbles: true })); - 50 | }); - 51 | } - 52 | - 53 | test("two audio elements exist for crossfade", async ({ page }) => { - 54 | await boot(page); - 55 | const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length); - 56 | expect(n).toBe(2); - 57 | }); - 58 | - 59 | test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => { - 60 | await boot(page); - 61 | // Reproduce the race: a fade-to-0 in flight, then the same element is brought back - 62 | // up (as happens when a faded idle element becomes the newly-landed active scale). - 63 | // The stale fade must NOT drag it back to 0. - 64 | const vol = await page.evaluate(async () => { - 65 | const a = document.querySelector("#aud") as HTMLAudioElement; - 66 | a.volume = 1; - 67 | (window as any).__hefFade(a, 0, 600); // start a slow fade to 0 - 68 | await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down - 69 | (window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one - 70 | await new Promise((r) => setTimeout(r, 350)); // well past both fades - 71 | return a.volume; - 72 | }); - 73 | expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0 - 74 | }); - 75 | - 76 | test("audio is a 0-10 level dial", async ({ page }) => { - 77 | await boot(page); - 78 | const ctl = await page.evaluate(() => { - 79 | const c = document.querySelector("#audio") as HTMLInputElement; - 80 | return { type: c.type, min: c.min, max: c.max }; - 81 | }); - 82 | expect(ctl.type).toBe("range"); - 83 | expect(ctl.min).toBe("0"); - 84 | expect(ctl.max).toBe("10"); - 85 | }); - 86 | - 87 | // (Entry via the welcome screen — defaults, Launch, control carry-over, State B — - 88 | // is covered in welcome.spec.ts.) - 89 | - 90 | test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => { - 91 | await boot(page); - 92 | await enableAudio(page); // hidden custom toggle → set + dispatch change - 93 | const box = (await page.locator("#dial").boundingBox())!; - 94 | const cx = box.x + box.width / 2, cy = box.y + box.height / 2; - 95 | await page.mouse.move(cx, cy - 30); - 96 | await page.mouse.down(); - 97 | await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent) - 98 | // The currentTime seek is throttled to one rAF; wait for it to land. - 99 | await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); - 100 | const state = await page.evaluate(() => ({ - 101 | t: (document.querySelector("video") as HTMLVideoElement).currentTime, - 102 | ga: (document.querySelector("#aud") as HTMLAudioElement).volume, - 103 | gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume, - 104 | })); - 105 | expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0 - 106 | expect(state.gb).toBeGreaterThan(0); // next scale fading in - 107 | expect(state.ga).toBeLessThan(1); // current scale fading out - 108 | await page.mouse.up(); - 109 | }); - 110 | - 111 | test("wheel auto-scrubs one altitude and locks", async ({ page }) => { - 112 | await boot(page); - 113 | const start = (await page.locator("#scale-name").textContent())!; - 114 | await wheelOnStage(page, 60); // wheel down = descend one altitude - 115 | await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 }); - 116 | const landed = (await page.locator("#scale-name").textContent())!; - 117 | expect(landed).toContain("orbit"); - 118 | await page.waitForTimeout(1500); - 119 | expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/altitude-lock-wheel-auto-scrubs-one-altitude-and-locks/error-context.md b/simulator/e2e/test-results/altitude-lock-wheel-auto-scrubs-one-altitude-and-locks/error-context.md deleted file mode 100644 index a7d8769..0000000 --- a/simulator/e2e/test-results/altitude-lock-wheel-auto-scrubs-one-altitude-and-locks/error-context.md +++ /dev/null @@ -1,267 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: altitude-lock.spec.ts >> wheel auto-scrubs one altitude and locks -- Location: tests/altitude-lock.spec.ts:111:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" [checked] - - generic [ref=e69]: Dev Mode - - generic [ref=e70]: - - group "Audio diagnostics" [ref=e71]: - - generic [ref=e72]: Audio diagnostics - - generic [ref=e73]: "audio: off (level 0)" - - paragraph [ref=e74]: Native player — tests the soundtrack file directly (bypasses all app code). If it plays here but the Audio toggle is silent, it's a code/autoplay issue, not the file. - - group "Pool — pick a clip" [ref=e76]: - - generic [ref=e77]: Pool — pick a clip - - combobox "Choose a clip from this altitude's pool" [ref=e78]: - - option "cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI)" [selected] - - option "cosmos_galaxies · Flying Through Galaxies (NASA SVS)" - - option "cosmos_orion · Orion Nebula — visible/IR flythrough (NASA SVS)" - - option "cosmos_tarantula · Tarantula Nebula (30 Doradus) zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_westerlund · Flight through star cluster Westerlund 2 (ESA/Hubble)" - - option "cosmos_southernring · Southern Ring Nebula zoom (NASA/ESA/CSA/STScI)" - - option "cosmos_carina_eso · Carina Nebula zoom (ESO)" - - button "🎲 Re-roll random" [ref=e79] [cursor=pointer] - - group "Active clip" [ref=e80]: - - generic [ref=e81]: Active clip - - generic [ref=e82]: - - term [ref=e83]: id - - definition [ref=e84]: cosmos - - term [ref=e85]: title - - definition [ref=e86]: Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - term [ref=e87]: source - - definition [ref=e88]: NASA SVS 31348 Clifs-3d-STScI (Exploring the Cosmic Cliffs in 3D, Webb NIRCam); 4K source, trim 10–34s, crossfade-loop. Text-free — replaces the Orion SVS 30957 flythrough. - - term [ref=e89]: license - - definition [ref=e90]: CC-BY-class — credit NASA, ESA, CSA, STScI - - term [ref=e91]: base_file - - definition [ref=e92]: cosmos/base.mp4 - - group "Annotations & affect" [ref=e93]: - - generic [ref=e94]: Annotations & affect - - generic [ref=e95]: - - generic [ref=e96]: detected.nebula · sal 4 · 4 tiers - - generic [ref=e97]: detected.star · sal 2 · 4 tiers - - generic [ref=e98]: measure.distance · min L3 · 1 tier - - generic [ref=e99]: feel.wonder · min L1 · 4 tiers - - generic [ref=e100]: feel.sublime · min L2 · 4 tiers - - generic [ref=e101]: feel.vastness · min L3 · 4 tiers - - generic [ref=e102]: feel.insignificance · min L4 · 4 tiers - - group "RenderPlan" [ref=e103]: - - generic [ref=e104]: RenderPlan - - generic [ref=e105]: "{ \"plan\": { \"grade\": { \"tone\": 0 }, \"overlay\": { \"level\": 0, \"intensity\": 0 }, \"affect\": { \"strength\": 0, \"intensity\": 0 }, \"dream\": { \"strength\": 0, \"intensity\": 0 }, \"is_identity\": true }, \"render\": { \"video\": { \"shown\": false }, \"audio\": { \"source\": \"off\", \"url\": null, \"altitude_coupled\": false } } }" - - group "Cache & playback" [ref=e106]: - - generic [ref=e107]: Cache & playback - - generic [ref=e108]: - - term [ref=e109]: cache - - definition [ref=e110]: 497/41 cached - - term [ref=e111]: active source - - definition [ref=e112]: memory (blob) - - term [ref=e113]: resolution - - definition [ref=e114]: — - - term [ref=e115]: duration - - definition [ref=e116]: — - - term [ref=e117]: loop pos - - definition [ref=e118]: — -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // These assert the two behaviors the operator asked for: - 4 | // 1. zooming plays the morph matching the clip we land on, and - 5 | // 2. after landing the clip is LOCKED — it does not change while parked. - 6 | // - 7 | // The played morph paths are read from window.__hefMorphs (a diagnostic seam in - 8 | // advance()), which is reliable even when the morph is served from an in-memory - 9 | // blob cache. The locked clip is read from the Dev-Mode "#scale-name" readout - 10 | // (" · (i/n)"). - 11 | - 12 | async function boot(page: Page) { - 13 | await page.addInitScript(() => { - 14 | localStorage.setItem("hef.devMode", "1"); - 15 | }); - 16 | await page.goto("/"); - 17 | // Wait for phase-1 preload, then enter via the welcome screen's Launch button - 18 | // (Video defaults on, Audio 2 → the experience is running once we're in). -> 19 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 20 | await page.locator("#welcome-launch").click(); - 21 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 22 | await expect(page.locator("#scale-name")).toContainText("cosmos"); - 23 | } - 24 | - 25 | async function morphs(page: Page): Promise { - 26 | return await page.evaluate(() => (window as any).__hefMorphs || []); - 27 | } - 28 | - 29 | async function wheelOnStage(page: Page, deltaY: number) { - 30 | await page.locator("#stage").dispatchEvent("wheel", { deltaY }); - 31 | } - 32 | - 33 | // The Audio control is a 0–10 level slider; set it to full + fire `input` directly - 34 | // (headless Chromium relaxes the autoplay gesture requirement). - 35 | async function enableAudio(page: Page) { - 36 | await page.evaluate(() => { - 37 | const c = document.querySelector("#audio") as HTMLInputElement; - 38 | c.value = "10"; - 39 | c.dispatchEvent(new Event("input", { bubbles: true })); - 40 | }); - 41 | } - 42 | - 43 | // The Video toggle gates the base-clip loop (update() only loads media when video is - 44 | // shown). Same hidden-checkbox pattern. - 45 | async function enableVideo(page: Page) { - 46 | await page.evaluate(() => { - 47 | const c = document.querySelector("#visual") as HTMLInputElement; - 48 | c.checked = true; - 49 | c.dispatchEvent(new Event("change", { bubbles: true })); - 50 | }); - 51 | } - 52 | - 53 | test("two audio elements exist for crossfade", async ({ page }) => { - 54 | await boot(page); - 55 | const n = await page.evaluate(() => document.querySelectorAll("audio#aud, audio#aud-b").length); - 56 | expect(n).toBe(2); - 57 | }); - 58 | - 59 | test("a new volume fade supersedes a stale one (audio not silenced after landing)", async ({ page }) => { - 60 | await boot(page); - 61 | // Reproduce the race: a fade-to-0 in flight, then the same element is brought back - 62 | // up (as happens when a faded idle element becomes the newly-landed active scale). - 63 | // The stale fade must NOT drag it back to 0. - 64 | const vol = await page.evaluate(async () => { - 65 | const a = document.querySelector("#aud") as HTMLAudioElement; - 66 | a.volume = 1; - 67 | (window as any).__hefFade(a, 0, 600); // start a slow fade to 0 - 68 | await new Promise((r) => setTimeout(r, 120)); // let it ramp partway down - 69 | (window as any).__hefFade(a, 0.8, 90); // new fade up — must cancel the old one - 70 | await new Promise((r) => setTimeout(r, 350)); // well past both fades - 71 | return a.volume; - 72 | }); - 73 | expect(vol).toBeGreaterThan(0.7); // ended near 0.8, not dragged to 0 - 74 | }); - 75 | - 76 | test("audio is a 0-10 level dial", async ({ page }) => { - 77 | await boot(page); - 78 | const ctl = await page.evaluate(() => { - 79 | const c = document.querySelector("#audio") as HTMLInputElement; - 80 | return { type: c.type, min: c.min, max: c.max }; - 81 | }); - 82 | expect(ctl.type).toBe("range"); - 83 | expect(ctl.min).toBe("0"); - 84 | expect(ctl.max).toBe("10"); - 85 | }); - 86 | - 87 | // (Entry via the welcome screen — defaults, Launch, control carry-over, State B — - 88 | // is covered in welcome.spec.ts.) - 89 | - 90 | test("dragging the dial scrubs morph currentTime and audio gains", async ({ page }) => { - 91 | await boot(page); - 92 | await enableAudio(page); // hidden custom toggle → set + dispatch change - 93 | const box = (await page.locator("#dial").boundingBox())!; - 94 | const cx = box.x + box.width / 2, cy = box.y + box.height / 2; - 95 | await page.mouse.move(cx, cy - 30); - 96 | await page.mouse.down(); - 97 | await page.mouse.move(cx + 18, cy - 24, { steps: 8 }); // partial clockwise turn (~0.5 detent) - 98 | // The currentTime seek is throttled to one rAF; wait for it to land. - 99 | await page.waitForFunction(() => (document.querySelector("video") as HTMLVideoElement).currentTime > 0, null, { timeout: 5000 }); - 100 | const state = await page.evaluate(() => ({ - 101 | t: (document.querySelector("video") as HTMLVideoElement).currentTime, - 102 | ga: (document.querySelector("#aud") as HTMLAudioElement).volume, - 103 | gb: (document.querySelector("#aud-b") as HTMLAudioElement).volume, - 104 | })); - 105 | expect(state.t).toBeGreaterThan(0); // morph scrubbed off frame 0 - 106 | expect(state.gb).toBeGreaterThan(0); // next scale fading in - 107 | expect(state.ga).toBeLessThan(1); // current scale fading out - 108 | await page.mouse.up(); - 109 | }); - 110 | - 111 | test("wheel auto-scrubs one altitude and locks", async ({ page }) => { - 112 | await boot(page); - 113 | const start = (await page.locator("#scale-name").textContent())!; - 114 | await wheelOnStage(page, 60); // wheel down = descend one altitude - 115 | await page.waitForFunction((s) => document.querySelector("#scale-name")?.textContent !== s, start, { timeout: 20000 }); - 116 | const landed = (await page.locator("#scale-name").textContent())!; - 117 | expect(landed).toContain("orbit"); - 118 | await page.waitForTimeout(1500); - 119 | expect(await page.locator("#scale-name").textContent()).toBe(landed); // locked -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/i18n-language-dropdown-loc-69a23-resets-to-English-on-reload/error-context.md b/simulator/e2e/test-results/i18n-language-dropdown-loc-69a23-resets-to-English-on-reload/error-context.md deleted file mode 100644 index c80b047..0000000 --- a/simulator/e2e/test-results/i18n-language-dropdown-loc-69a23-resets-to-English-on-reload/error-context.md +++ /dev/null @@ -1,80 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: i18n.spec.ts >> language dropdown localizes chrome and resets to English on reload -- Location: tests/i18n.spec.ts:15:5 - -# Error details - -``` -Error: expect(locator).toHaveCount(expected) failed - -Locator: locator('#welcome-lang option') -Expected: 4 -Received: 0 -Timeout: 5000ms - -Call log: - - Expect "toHaveCount" with timeout 5000ms - - waiting for locator('#welcome-lang option') - 13 × locator resolved to 0 elements - - unexpected value "0" - -``` - -# Page snapshot - -```yaml -- generic [ref=e2]: "{\"detail\":\"Not Found\"}" -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // The language dropdown localizes the visitor-facing chrome live (and resets to - 4 | // English on reload — selection is session-only, no persistence). Annotation - 5 | // re-rendering is covered by the unit tests (resolveStrings) + the manifest - 6 | // invariant test; here we assert the DOM behavior of the dropdown itself. - 7 | - 8 | async function boot(page: Page) { - 9 | await page.goto("/"); - 10 | // initLanguage() populates BOTH selects during startup (before media preload). - 11 | // The welcome select is the visible one (the panel is hidden until entry). -> 12 | await expect(page.locator("#welcome-lang option")).toHaveCount(4); - | ^ Error: expect(locator).toHaveCount(expected) failed - 13 | } - 14 | - 15 | test("language dropdown localizes chrome and resets to English on reload", async ({ page }) => { - 16 | await boot(page); - 17 | const sel = page.locator("#welcome-lang"); // the welcome screen's selector - 18 | - 19 | // English selected by default (both selects mirror activeLang) - 20 | await expect(sel).toHaveValue("en"); - 21 | await expect(page.locator("#lang-select")).toHaveValue("en"); - 22 | await expect(page.locator('[data-i18n="output.legend"]')).toHaveText("Output"); - 23 | await expect(page.locator("html")).toHaveAttribute("lang", "en"); - 24 | - 25 | // switch to Spanish -> chrome changes (incl. the hidden panel), updates - 26 | await sel.selectOption("es"); - 27 | await expect(page.locator('[data-i18n="output.legend"]')).toHaveText("Salida"); - 28 | await expect(page.locator("#lang-select")).toHaveValue("es"); // panel select mirrors - 29 | await expect(page.locator("html")).toHaveAttribute("lang", "es"); - 30 | - 31 | // Japanese - 32 | await sel.selectOption("ja"); - 33 | await expect(page.locator('[data-i18n="knobs.think"]')).toHaveText("考える"); - 34 | - 35 | // reload returns to English (session-only, no persistence) - 36 | await page.reload(); - 37 | await expect(page.locator("#welcome-lang")).toHaveValue("en"); - 38 | await expect(page.locator('[data-i18n="output.legend"]')).toHaveText("Output"); - 39 | }); - 40 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/loop-recovery-loop-recover-556bc-ed-no-freeze-on-last-frame-/error-context.md b/simulator/e2e/test-results/loop-recovery-loop-recover-556bc-ed-no-freeze-on-last-frame-/error-context.md deleted file mode 100644 index 63f3234..0000000 --- a/simulator/e2e/test-results/loop-recovery-loop-recover-556bc-ed-no-freeze-on-last-frame-/error-context.md +++ /dev/null @@ -1,63 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: loop-recovery.spec.ts >> loop recovers when a clip reaches `ended` (no freeze on last frame) -- Location: tests/loop-recovery.spec.ts:7:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [ref=e2]: "{\"detail\":\"Not Found\"}" -``` - -# Test source - -```ts - 1 | import { test, expect } from "@playwright/test"; - 2 | - 3 | // Regression: the steady-state loop (#vid-loop) restarts from the morph-tail - 4 | // offset via a narrow timeupdate window. If that window is ever missed the clip - 5 | // fires `ended` and — without a safety net — freezes on its last frame ("videos - 6 | // aren't looping"). This asserts the `ended` safety net recovers playback. - 7 | test("loop recovers when a clip reaches `ended` (no freeze on last frame)", async ({ page }) => { - 8 | await page.goto("/"); - 9 | // Enter via the welcome screen (Video defaults on → the loop video loads). -> 10 | await page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 11 | await page.locator("#welcome-launch").click(); - 12 | await page.waitForSelector("#welcome", { state: "detached", timeout: 10_000 }); - 13 | // Wait for the loop video to actually have media before forcing the stuck state. - 14 | await page.waitForFunction( - 15 | () => { const v = document.getElementById("vid-loop") as HTMLVideoElement; return v && v.duration > 0; }, - 16 | null, - 17 | { timeout: 20_000 }, - 18 | ); - 19 | - 20 | const r = await page.evaluate(async () => { - 21 | const lv = document.getElementById("vid-loop") as HTMLVideoElement; - 22 | // Reproduce the stuck state: parked on the last frame, as after a missed wrap. - 23 | lv.pause(); - 24 | lv.currentTime = Math.max(0, lv.duration - 0.001); - 25 | // The browser emits `ended` here in real life; emit it deterministically. - 26 | lv.dispatchEvent(new Event("ended")); - 27 | await new Promise((res) => setTimeout(res, 800)); - 28 | return { dur: +lv.duration.toFixed(2), ct: +lv.currentTime.toFixed(2), paused: lv.paused }; - 29 | }); - 30 | - 31 | // Recovered: playing again, and seeked back near the loop tail (not stuck at end). - 32 | expect(r.paused).toBe(false); - 33 | expect(r.ct).toBeLessThan(5); - 34 | }); - 35 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/welcome-Launch-when-alread-e343d-ectly-and-reveals-the-panel/error-context.md b/simulator/e2e/test-results/welcome-Launch-when-alread-e343d-ectly-and-reveals-the-panel/error-context.md deleted file mode 100644 index ee68697..0000000 --- a/simulator/e2e/test-results/welcome-Launch-when-alread-e343d-ectly-and-reveals-the-panel/error-context.md +++ /dev/null @@ -1,138 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: welcome.spec.ts >> Launch when already loaded enters the simulation directly and reveals the panel -- Location: tests/welcome.spec.ts:61:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [ref=e2]: "{\"detail\":\"Not Found\"}" -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // The Welcome screen is the universal entry gate (see - 4 | // docs/superpowers/specs/2026-06-30-welcome-screen-design.md). It carries the - 5 | // photosensitivity heads-up, the four output controls (Video on / Reduce motion - 6 | // off / Audio 2 / English), a "Launch Simulator" button, and the "Loading - 7 | // Universe" progress. The right panel is hidden until the visitor enters. - 8 | - 9 | const ready = (page: Page) => -> 10 | page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 11 | - 12 | // Set a range input's value + fire `input` (Playwright's fill() doesn't drive ranges). - 13 | async function setRange(page: Page, id: string, value: string) { - 14 | await page.evaluate(({ id, value }) => { - 15 | const el = document.getElementById(id) as HTMLInputElement; - 16 | el.value = value; - 17 | el.dispatchEvent(new Event("input", { bubbles: true })); - 18 | }, { id, value }); - 19 | } - 20 | - 21 | test("welcome screen shows on load with the expected defaults; the panel is hidden", async ({ page }) => { - 22 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 23 | await page.goto("/"); - 24 | - 25 | await expect(page.locator("#welcome")).toBeVisible(); - 26 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 27 | - 28 | // Defaults: Language English, Speed 1, Audio 2. - 29 | await expect(page.locator("#welcome-lang")).toHaveValue("en"); - 30 | await expect(page.locator("#welcome-play-speed")).toHaveValue("1"); - 31 | await expect(page.locator("#welcome-play-speed-val")).toHaveText("1.00×"); - 32 | await expect(page.locator("#welcome-audio")).toHaveValue("2"); - 33 | await expect(page.locator("#welcome-audio-val")).toHaveText("2"); - 34 | - 35 | // Heads-up + Launch button present; "Loading Universe" progress present. - 36 | await expect(page.locator("#welcome-title")).toBeVisible(); - 37 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 38 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 39 | - 40 | // The right panel is hidden behind the welcome screen. - 41 | await expect(page.locator(".panel")).toBeHidden(); - 42 | }); - 43 | - 44 | test("the language picker re-renders the welcome text live", async ({ page }) => { - 45 | await page.goto("/"); - 46 | const sel = page.locator("#welcome-lang"); - 47 | await expect(sel).toHaveValue("en"); - 48 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 49 | - 50 | await sel.selectOption("es"); - 51 | await expect(page.locator("#welcome-launch")).toHaveText("Iniciar simulador"); - 52 | await expect(page.locator("#welcome-title")).toHaveText(/Atención/); - 53 | await expect(page.locator("html")).toHaveAttribute("lang", "es"); - 54 | // the hidden panel selector mirrors the choice - 55 | await expect(page.locator("#lang-select")).toHaveValue("es"); - 56 | - 57 | await sel.selectOption("ja"); - 58 | await expect(page.locator("#welcome-launch")).toHaveText("シミュレーターを起動"); - 59 | }); - 60 | - 61 | test("Launch when already loaded enters the simulation directly and reveals the panel", async ({ page }) => { - 62 | await page.goto("/"); - 63 | await ready(page); - 64 | - 65 | await page.locator("#welcome-launch").click(); - 66 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 67 | await expect(page.locator(".panel")).toBeVisible(); - 68 | - 69 | // Welcome assumes Video on; Audio default 2 carried into the panel. - 70 | await expect(page.locator("#visual")).toBeChecked(); - 71 | await expect(page.locator("#audio")).toHaveValue("2"); - 72 | }); - 73 | - 74 | test("welcome control values carry into the panel on entry", async ({ page }) => { - 75 | await page.goto("/"); - 76 | await ready(page); - 77 | - 78 | await setRange(page, "welcome-audio", "5"); - 79 | await setRange(page, "welcome-play-speed", "2"); // mirrors into the panel slider live - 80 | await page.locator("#welcome-launch").click(); - 81 | - 82 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 83 | await expect(page.locator("#audio")).toHaveValue("5"); - 84 | await expect(page.locator("#play-speed")).toHaveValue("2"); - 85 | await expect(page.locator("#visual")).toBeChecked(); // welcome assumes video on - 86 | }); - 87 | - 88 | test("Launch while still loading shows the centered loader with controls, then auto-enters", async ({ page }) => { - 89 | // Stall media so phase-1 preload hasn't finished when Launch is pressed. - 90 | let release!: () => void; - 91 | const gate = new Promise((r) => (release = r)); - 92 | await page.route("**/media/**", async (route) => { - 93 | await gate; - 94 | await route.continue(); - 95 | }); - 96 | - 97 | await page.goto("/"); - 98 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 99 | - 100 | await page.locator("#welcome-launch").click(); - 101 | - 102 | // State B: messaging + button hidden, controls remain, loader shown (centered). - 103 | await expect(page.locator("#welcome")).toHaveClass(/loading/); - 104 | await expect(page.locator("#welcome-title")).toBeHidden(); - 105 | await expect(page.locator("#welcome-launch")).toBeHidden(); - 106 | await expect(page.locator(".welcome-controls")).toBeVisible(); - 107 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 108 | - 109 | // Let media through → universe becomes ready → auto-enter. - 110 | release(); -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/welcome-Launch-while-still-51a6a-h-controls-then-auto-enters/error-context.md b/simulator/e2e/test-results/welcome-Launch-while-still-51a6a-h-controls-then-auto-enters/error-context.md deleted file mode 100644 index 1e23aaa..0000000 --- a/simulator/e2e/test-results/welcome-Launch-while-still-51a6a-h-controls-then-auto-enters/error-context.md +++ /dev/null @@ -1,192 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: welcome.spec.ts >> Launch while still loading shows the centered loader with controls, then auto-enters -- Location: tests/welcome.spec.ts:88:5 - -# Error details - -``` -Error: expect(locator).toHaveClass(expected) failed - -Locator: locator('#welcome') -Expected pattern: /welcoming/ -Timeout: 5000ms -Error: element(s) not found - -Call log: - - Expect "toHaveClass" with timeout 5000ms - - waiting for locator('#welcome') - -``` - -```yaml -- text: Loading Universe... -- banner: - - heading "Human Experience Filter — Alteration Preview" [level=1] - - link "About": - - /url: about.html - - link "ⓘ Credits & licenses": - - /url: credits.html -- main: - - status: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output": - - text: Output - - checkbox "Video" - - text: Video Audio - - slider "Audio 0 /10": "0" - - text: 0 /10 Speed - - slider "Speed 1.00×": "1" - - text: 1.00× 🌐 - - combobox "Language": - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude": - - text: Altitude - - slider "Altitude — turn or use arrow keys to change scale": - - button "cosmos" - - button "orbit" - - button "sky" - - button "coast" - - button "reef" - - button "abyss" - - text: ALTITUDE - - text: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)": - - text: Experience knobs (0–4) Think - - slider "Think": "0" - - text: Feel - - slider "Feel": "0" - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light": "0" - - checkbox "Dev Mode" - - text: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // The Welcome screen is the universal entry gate (see - 4 | // docs/superpowers/specs/2026-06-30-welcome-screen-design.md). It carries the - 5 | // photosensitivity heads-up, the four output controls (Video on / Reduce motion - 6 | // off / Audio 2 / English), a "Launch Simulator" button, and the "Loading - 7 | // Universe" progress. The right panel is hidden until the visitor enters. - 8 | - 9 | const ready = (page: Page) => - 10 | page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - 11 | - 12 | // Set a range input's value + fire `input` (Playwright's fill() doesn't drive ranges). - 13 | async function setRange(page: Page, id: string, value: string) { - 14 | await page.evaluate(({ id, value }) => { - 15 | const el = document.getElementById(id) as HTMLInputElement; - 16 | el.value = value; - 17 | el.dispatchEvent(new Event("input", { bubbles: true })); - 18 | }, { id, value }); - 19 | } - 20 | - 21 | test("welcome screen shows on load with the expected defaults; the panel is hidden", async ({ page }) => { - 22 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 23 | await page.goto("/"); - 24 | - 25 | await expect(page.locator("#welcome")).toBeVisible(); - 26 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 27 | - 28 | // Defaults: Language English, Speed 1, Audio 2. - 29 | await expect(page.locator("#welcome-lang")).toHaveValue("en"); - 30 | await expect(page.locator("#welcome-play-speed")).toHaveValue("1"); - 31 | await expect(page.locator("#welcome-play-speed-val")).toHaveText("1.00×"); - 32 | await expect(page.locator("#welcome-audio")).toHaveValue("2"); - 33 | await expect(page.locator("#welcome-audio-val")).toHaveText("2"); - 34 | - 35 | // Heads-up + Launch button present; "Loading Universe" progress present. - 36 | await expect(page.locator("#welcome-title")).toBeVisible(); - 37 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 38 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 39 | - 40 | // The right panel is hidden behind the welcome screen. - 41 | await expect(page.locator(".panel")).toBeHidden(); - 42 | }); - 43 | - 44 | test("the language picker re-renders the welcome text live", async ({ page }) => { - 45 | await page.goto("/"); - 46 | const sel = page.locator("#welcome-lang"); - 47 | await expect(sel).toHaveValue("en"); - 48 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 49 | - 50 | await sel.selectOption("es"); - 51 | await expect(page.locator("#welcome-launch")).toHaveText("Iniciar simulador"); - 52 | await expect(page.locator("#welcome-title")).toHaveText(/Atención/); - 53 | await expect(page.locator("html")).toHaveAttribute("lang", "es"); - 54 | // the hidden panel selector mirrors the choice - 55 | await expect(page.locator("#lang-select")).toHaveValue("es"); - 56 | - 57 | await sel.selectOption("ja"); - 58 | await expect(page.locator("#welcome-launch")).toHaveText("シミュレーターを起動"); - 59 | }); - 60 | - 61 | test("Launch when already loaded enters the simulation directly and reveals the panel", async ({ page }) => { - 62 | await page.goto("/"); - 63 | await ready(page); - 64 | - 65 | await page.locator("#welcome-launch").click(); - 66 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 67 | await expect(page.locator(".panel")).toBeVisible(); - 68 | - 69 | // Welcome assumes Video on; Audio default 2 carried into the panel. - 70 | await expect(page.locator("#visual")).toBeChecked(); - 71 | await expect(page.locator("#audio")).toHaveValue("2"); - 72 | }); - 73 | - 74 | test("welcome control values carry into the panel on entry", async ({ page }) => { - 75 | await page.goto("/"); - 76 | await ready(page); - 77 | - 78 | await setRange(page, "welcome-audio", "5"); - 79 | await setRange(page, "welcome-play-speed", "2"); // mirrors into the panel slider live - 80 | await page.locator("#welcome-launch").click(); - 81 | - 82 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 83 | await expect(page.locator("#audio")).toHaveValue("5"); - 84 | await expect(page.locator("#play-speed")).toHaveValue("2"); - 85 | await expect(page.locator("#visual")).toBeChecked(); // welcome assumes video on - 86 | }); - 87 | - 88 | test("Launch while still loading shows the centered loader with controls, then auto-enters", async ({ page }) => { - 89 | // Stall media so phase-1 preload hasn't finished when Launch is pressed. - 90 | let release!: () => void; - 91 | const gate = new Promise((r) => (release = r)); - 92 | await page.route("**/media/**", async (route) => { - 93 | await gate; - 94 | await route.continue(); - 95 | }); - 96 | - 97 | await page.goto("/"); -> 98 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - | ^ Error: expect(locator).toHaveClass(expected) failed - 99 | - 100 | await page.locator("#welcome-launch").click(); - 101 | - 102 | // State B: messaging + button hidden, controls remain, loader shown (centered). - 103 | await expect(page.locator("#welcome")).toHaveClass(/loading/); - 104 | await expect(page.locator("#welcome-title")).toBeHidden(); - 105 | await expect(page.locator("#welcome-launch")).toBeHidden(); - 106 | await expect(page.locator(".welcome-controls")).toBeVisible(); - 107 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 108 | - 109 | // Let media through → universe becomes ready → auto-enter. - 110 | release(); - 111 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 30_000 }); - 112 | await expect(page.locator(".panel")).toBeVisible(); - 113 | }); - 114 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/welcome-the-language-picker-re-renders-the-welcome-text-live/error-context.md b/simulator/e2e/test-results/welcome-the-language-picker-re-renders-the-welcome-text-live/error-context.md deleted file mode 100644 index a4ef302..0000000 --- a/simulator/e2e/test-results/welcome-the-language-picker-re-renders-the-welcome-text-live/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: welcome.spec.ts >> the language picker re-renders the welcome text live -- Location: tests/welcome.spec.ts:44:5 - -# Error details - -``` -Error: expect(locator).toHaveValue(expected) failed - -Locator: locator('#welcome-lang') -Expected: "en" -Timeout: 5000ms -Error: element(s) not found - -Call log: - - Expect "toHaveValue" with timeout 5000ms - - waiting for locator('#welcome-lang') - -``` - -```yaml -- text: "{\"detail\":\"Not Found\"}" -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // The Welcome screen is the universal entry gate (see - 4 | // docs/superpowers/specs/2026-06-30-welcome-screen-design.md). It carries the - 5 | // photosensitivity heads-up, the four output controls (Video on / Reduce motion - 6 | // off / Audio 2 / English), a "Launch Simulator" button, and the "Loading - 7 | // Universe" progress. The right panel is hidden until the visitor enters. - 8 | - 9 | const ready = (page: Page) => - 10 | page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - 11 | - 12 | // Set a range input's value + fire `input` (Playwright's fill() doesn't drive ranges). - 13 | async function setRange(page: Page, id: string, value: string) { - 14 | await page.evaluate(({ id, value }) => { - 15 | const el = document.getElementById(id) as HTMLInputElement; - 16 | el.value = value; - 17 | el.dispatchEvent(new Event("input", { bubbles: true })); - 18 | }, { id, value }); - 19 | } - 20 | - 21 | test("welcome screen shows on load with the expected defaults; the panel is hidden", async ({ page }) => { - 22 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 23 | await page.goto("/"); - 24 | - 25 | await expect(page.locator("#welcome")).toBeVisible(); - 26 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 27 | - 28 | // Defaults: Language English, Speed 1, Audio 2. - 29 | await expect(page.locator("#welcome-lang")).toHaveValue("en"); - 30 | await expect(page.locator("#welcome-play-speed")).toHaveValue("1"); - 31 | await expect(page.locator("#welcome-play-speed-val")).toHaveText("1.00×"); - 32 | await expect(page.locator("#welcome-audio")).toHaveValue("2"); - 33 | await expect(page.locator("#welcome-audio-val")).toHaveText("2"); - 34 | - 35 | // Heads-up + Launch button present; "Loading Universe" progress present. - 36 | await expect(page.locator("#welcome-title")).toBeVisible(); - 37 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 38 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 39 | - 40 | // The right panel is hidden behind the welcome screen. - 41 | await expect(page.locator(".panel")).toBeHidden(); - 42 | }); - 43 | - 44 | test("the language picker re-renders the welcome text live", async ({ page }) => { - 45 | await page.goto("/"); - 46 | const sel = page.locator("#welcome-lang"); -> 47 | await expect(sel).toHaveValue("en"); - | ^ Error: expect(locator).toHaveValue(expected) failed - 48 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 49 | - 50 | await sel.selectOption("es"); - 51 | await expect(page.locator("#welcome-launch")).toHaveText("Iniciar simulador"); - 52 | await expect(page.locator("#welcome-title")).toHaveText(/Atención/); - 53 | await expect(page.locator("html")).toHaveAttribute("lang", "es"); - 54 | // the hidden panel selector mirrors the choice - 55 | await expect(page.locator("#lang-select")).toHaveValue("es"); - 56 | - 57 | await sel.selectOption("ja"); - 58 | await expect(page.locator("#welcome-launch")).toHaveText("シミュレーターを起動"); - 59 | }); - 60 | - 61 | test("Launch when already loaded enters the simulation directly and reveals the panel", async ({ page }) => { - 62 | await page.goto("/"); - 63 | await ready(page); - 64 | - 65 | await page.locator("#welcome-launch").click(); - 66 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 67 | await expect(page.locator(".panel")).toBeVisible(); - 68 | - 69 | // Welcome assumes Video on; Audio default 2 carried into the panel. - 70 | await expect(page.locator("#visual")).toBeChecked(); - 71 | await expect(page.locator("#audio")).toHaveValue("2"); - 72 | }); - 73 | - 74 | test("welcome control values carry into the panel on entry", async ({ page }) => { - 75 | await page.goto("/"); - 76 | await ready(page); - 77 | - 78 | await setRange(page, "welcome-audio", "5"); - 79 | await setRange(page, "welcome-play-speed", "2"); // mirrors into the panel slider live - 80 | await page.locator("#welcome-launch").click(); - 81 | - 82 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 83 | await expect(page.locator("#audio")).toHaveValue("5"); - 84 | await expect(page.locator("#play-speed")).toHaveValue("2"); - 85 | await expect(page.locator("#visual")).toBeChecked(); // welcome assumes video on - 86 | }); - 87 | - 88 | test("Launch while still loading shows the centered loader with controls, then auto-enters", async ({ page }) => { - 89 | // Stall media so phase-1 preload hasn't finished when Launch is pressed. - 90 | let release!: () => void; - 91 | const gate = new Promise((r) => (release = r)); - 92 | await page.route("**/media/**", async (route) => { - 93 | await gate; - 94 | await route.continue(); - 95 | }); - 96 | - 97 | await page.goto("/"); - 98 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 99 | - 100 | await page.locator("#welcome-launch").click(); - 101 | - 102 | // State B: messaging + button hidden, controls remain, loader shown (centered). - 103 | await expect(page.locator("#welcome")).toHaveClass(/loading/); - 104 | await expect(page.locator("#welcome-title")).toBeHidden(); - 105 | await expect(page.locator("#welcome-launch")).toBeHidden(); - 106 | await expect(page.locator(".welcome-controls")).toBeVisible(); - 107 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 108 | - 109 | // Let media through → universe becomes ready → auto-enter. - 110 | release(); - 111 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 30_000 }); - 112 | await expect(page.locator(".panel")).toBeVisible(); - 113 | }); - 114 | -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/welcome-welcome-control-values-carry-into-the-panel-on-entry/error-context.md b/simulator/e2e/test-results/welcome-welcome-control-values-carry-into-the-panel-on-entry/error-context.md deleted file mode 100644 index e659a27..0000000 --- a/simulator/e2e/test-results/welcome-welcome-control-values-carry-into-the-panel-on-entry/error-context.md +++ /dev/null @@ -1,203 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: welcome.spec.ts >> welcome control values carry into the panel on entry -- Location: tests/welcome.spec.ts:74:5 - -# Error details - -``` -TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. -``` - -# Page snapshot - -```yaml -- generic [active] [ref=e1]: - - banner [ref=e2]: - - heading "Human Experience Filter — Alteration Preview" [level=1] [ref=e3] - - link "About" [ref=e4] [cursor=pointer]: - - /url: about.html - - link "ⓘ Credits & licenses" [ref=e5] [cursor=pointer]: - - /url: credits.html - - main [ref=e6]: - - generic [ref=e8]: - - img - - img - - button "Run simulation" [ref=e12] [cursor=pointer] - - dialog "Heads up — motion & flashing" [ref=e13]: - - generic [ref=e14]: - - heading "Heads up — motion & flashing" [level=2] [ref=e15] - - paragraph [ref=e16]: This experience contains continuous motion, flashing, and shifting imagery. If you are sensitive to motion or flashing light, turn on your device’s “Reduce motion” setting before you begin — the experience honours it automatically. - - button "Continue" [ref=e17] [cursor=pointer] - - generic [ref=e18]: - - status [ref=e19]: cosmos. Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) - - group "Output" [ref=e20]: - - generic [ref=e21]: Output - - generic [ref=e22] [cursor=pointer]: - - checkbox "Video" - - generic [ref=e25]: Video - - generic [ref=e26]: - - generic [ref=e27]: Audio - - slider "Audio 0 /10" [ref=e28]: "0" - - generic [ref=e29]: "0" - - text: /10 - - generic [ref=e30]: - - generic [ref=e31]: Speed - - slider "Speed 1.00×" [ref=e32]: "1" - - generic [ref=e33]: 1.00× - - generic [ref=e34]: - - text: 🌐 - - combobox "Language" [ref=e35]: - - option "English" [selected] - - option "Español" - - option "Français" - - option "日本語" - - group "Altitude" [ref=e36]: - - generic [ref=e37]: Altitude - - slider "Altitude — turn or use arrow keys to change scale" [ref=e39]: - - button "cosmos" [ref=e42] [cursor=pointer] - - button "orbit" [ref=e44] [cursor=pointer] - - button "sky" [ref=e46] [cursor=pointer] - - button "coast" [ref=e47] [cursor=pointer] - - button "reef" [ref=e49] [cursor=pointer] - - button "abyss" [ref=e51] [cursor=pointer] - - generic [ref=e52]: ALTITUDE - - generic [ref=e56]: cosmos · Cosmic Cliffs — Carina Nebula flythrough (NASA/ESA/CSA/STScI) (1/6 · pool 7) - - paragraph [ref=e57]: "Turn the knob (drag it, or scroll) to change altitude — endless: past the deepest it wraps back up to the highest. Click a label to jump there." - - group "Experience knobs (0–4)" [ref=e58]: - - generic [ref=e59]: Experience knobs (0–4) - - generic [ref=e60]: - - text: Think - - slider "Think" [ref=e61]: "0" - - generic [ref=e62]: - - text: Feel - - slider "Feel" [ref=e63]: "0" - - generic [ref=e64]: - - text: Mood — dark ◀ 0 ▶ light - - slider "Mood — dark ◀ 0 ▶ light" [ref=e65]: "0" - - generic [ref=e66] [cursor=pointer]: - - checkbox "Dev Mode" - - generic [ref=e69]: Dev Mode -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // The Welcome screen is the universal entry gate (see - 4 | // docs/superpowers/specs/2026-06-30-welcome-screen-design.md). It carries the - 5 | // photosensitivity heads-up, the four output controls (Video on / Reduce motion - 6 | // off / Audio 2 / English), a "Launch Simulator" button, and the "Loading - 7 | // Universe" progress. The right panel is hidden until the visitor enters. - 8 | - 9 | const ready = (page: Page) => -> 10 | page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - | ^ TimeoutError: page.waitForFunction: Timeout 45000ms exceeded. - 11 | - 12 | // Set a range input's value + fire `input` (Playwright's fill() doesn't drive ranges). - 13 | async function setRange(page: Page, id: string, value: string) { - 14 | await page.evaluate(({ id, value }) => { - 15 | const el = document.getElementById(id) as HTMLInputElement; - 16 | el.value = value; - 17 | el.dispatchEvent(new Event("input", { bubbles: true })); - 18 | }, { id, value }); - 19 | } - 20 | - 21 | test("welcome screen shows on load with the expected defaults; the panel is hidden", async ({ page }) => { - 22 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 23 | await page.goto("/"); - 24 | - 25 | await expect(page.locator("#welcome")).toBeVisible(); - 26 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 27 | - 28 | // Defaults: Language English, Speed 1, Audio 2. - 29 | await expect(page.locator("#welcome-lang")).toHaveValue("en"); - 30 | await expect(page.locator("#welcome-play-speed")).toHaveValue("1"); - 31 | await expect(page.locator("#welcome-play-speed-val")).toHaveText("1.00×"); - 32 | await expect(page.locator("#welcome-audio")).toHaveValue("2"); - 33 | await expect(page.locator("#welcome-audio-val")).toHaveText("2"); - 34 | - 35 | // Heads-up + Launch button present; "Loading Universe" progress present. - 36 | await expect(page.locator("#welcome-title")).toBeVisible(); - 37 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 38 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 39 | - 40 | // The right panel is hidden behind the welcome screen. - 41 | await expect(page.locator(".panel")).toBeHidden(); - 42 | }); - 43 | - 44 | test("the language picker re-renders the welcome text live", async ({ page }) => { - 45 | await page.goto("/"); - 46 | const sel = page.locator("#welcome-lang"); - 47 | await expect(sel).toHaveValue("en"); - 48 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 49 | - 50 | await sel.selectOption("es"); - 51 | await expect(page.locator("#welcome-launch")).toHaveText("Iniciar simulador"); - 52 | await expect(page.locator("#welcome-title")).toHaveText(/Atención/); - 53 | await expect(page.locator("html")).toHaveAttribute("lang", "es"); - 54 | // the hidden panel selector mirrors the choice - 55 | await expect(page.locator("#lang-select")).toHaveValue("es"); - 56 | - 57 | await sel.selectOption("ja"); - 58 | await expect(page.locator("#welcome-launch")).toHaveText("シミュレーターを起動"); - 59 | }); - 60 | - 61 | test("Launch when already loaded enters the simulation directly and reveals the panel", async ({ page }) => { - 62 | await page.goto("/"); - 63 | await ready(page); - 64 | - 65 | await page.locator("#welcome-launch").click(); - 66 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 67 | await expect(page.locator(".panel")).toBeVisible(); - 68 | - 69 | // Welcome assumes Video on; Audio default 2 carried into the panel. - 70 | await expect(page.locator("#visual")).toBeChecked(); - 71 | await expect(page.locator("#audio")).toHaveValue("2"); - 72 | }); - 73 | - 74 | test("welcome control values carry into the panel on entry", async ({ page }) => { - 75 | await page.goto("/"); - 76 | await ready(page); - 77 | - 78 | await setRange(page, "welcome-audio", "5"); - 79 | await setRange(page, "welcome-play-speed", "2"); // mirrors into the panel slider live - 80 | await page.locator("#welcome-launch").click(); - 81 | - 82 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 83 | await expect(page.locator("#audio")).toHaveValue("5"); - 84 | await expect(page.locator("#play-speed")).toHaveValue("2"); - 85 | await expect(page.locator("#visual")).toBeChecked(); // welcome assumes video on - 86 | }); - 87 | - 88 | test("Launch while still loading shows the centered loader with controls, then auto-enters", async ({ page }) => { - 89 | // Stall media so phase-1 preload hasn't finished when Launch is pressed. - 90 | let release!: () => void; - 91 | const gate = new Promise((r) => (release = r)); - 92 | await page.route("**/media/**", async (route) => { - 93 | await gate; - 94 | await route.continue(); - 95 | }); - 96 | - 97 | await page.goto("/"); - 98 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 99 | - 100 | await page.locator("#welcome-launch").click(); - 101 | - 102 | // State B: messaging + button hidden, controls remain, loader shown (centered). - 103 | await expect(page.locator("#welcome")).toHaveClass(/loading/); - 104 | await expect(page.locator("#welcome-title")).toBeHidden(); - 105 | await expect(page.locator("#welcome-launch")).toBeHidden(); - 106 | await expect(page.locator(".welcome-controls")).toBeVisible(); - 107 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 108 | - 109 | // Let media through → universe becomes ready → auto-enter. - 110 | release(); -``` \ No newline at end of file diff --git a/simulator/e2e/test-results/welcome-welcome-screen-sho-74d80-efaults-the-panel-is-hidden/error-context.md b/simulator/e2e/test-results/welcome-welcome-screen-sho-74d80-efaults-the-panel-is-hidden/error-context.md deleted file mode 100644 index 934372a..0000000 --- a/simulator/e2e/test-results/welcome-welcome-screen-sho-74d80-efaults-the-panel-is-hidden/error-context.md +++ /dev/null @@ -1,150 +0,0 @@ -# Instructions - -- Following Playwright test failed. -- Explain why, be concise, respect Playwright best practices. -- Provide a snippet of code with the fix, if possible. - -# Test info - -- Name: welcome.spec.ts >> welcome screen shows on load with the expected defaults; the panel is hidden -- Location: tests/welcome.spec.ts:21:5 - -# Error details - -``` -Error: expect(locator).toBeVisible() failed - -Locator: locator('#welcome') -Expected: visible -Timeout: 5000ms -Error: element(s) not found - -Call log: - - Expect "toBeVisible" with timeout 5000ms - - waiting for locator('#welcome') - -``` - -```yaml -- text: "{\"detail\":\"Not Found\"}" -``` - -# Test source - -```ts - 1 | import { test, expect, Page } from "@playwright/test"; - 2 | - 3 | // The Welcome screen is the universal entry gate (see - 4 | // docs/superpowers/specs/2026-06-30-welcome-screen-design.md). It carries the - 5 | // photosensitivity heads-up, the four output controls (Video on / Reduce motion - 6 | // off / Audio 2 / English), a "Launch Simulator" button, and the "Loading - 7 | // Universe" progress. The right panel is hidden until the visitor enters. - 8 | - 9 | const ready = (page: Page) => - 10 | page.waitForFunction(() => (window as any).__hefReady === true, null, { timeout: 45_000 }); - 11 | - 12 | // Set a range input's value + fire `input` (Playwright's fill() doesn't drive ranges). - 13 | async function setRange(page: Page, id: string, value: string) { - 14 | await page.evaluate(({ id, value }) => { - 15 | const el = document.getElementById(id) as HTMLInputElement; - 16 | el.value = value; - 17 | el.dispatchEvent(new Event("input", { bubbles: true })); - 18 | }, { id, value }); - 19 | } - 20 | - 21 | test("welcome screen shows on load with the expected defaults; the panel is hidden", async ({ page }) => { - 22 | await page.emulateMedia({ reducedMotion: "no-preference" }); - 23 | await page.goto("/"); - 24 | -> 25 | await expect(page.locator("#welcome")).toBeVisible(); - | ^ Error: expect(locator).toBeVisible() failed - 26 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 27 | - 28 | // Defaults: Language English, Speed 1, Audio 2. - 29 | await expect(page.locator("#welcome-lang")).toHaveValue("en"); - 30 | await expect(page.locator("#welcome-play-speed")).toHaveValue("1"); - 31 | await expect(page.locator("#welcome-play-speed-val")).toHaveText("1.00×"); - 32 | await expect(page.locator("#welcome-audio")).toHaveValue("2"); - 33 | await expect(page.locator("#welcome-audio-val")).toHaveText("2"); - 34 | - 35 | // Heads-up + Launch button present; "Loading Universe" progress present. - 36 | await expect(page.locator("#welcome-title")).toBeVisible(); - 37 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 38 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 39 | - 40 | // The right panel is hidden behind the welcome screen. - 41 | await expect(page.locator(".panel")).toBeHidden(); - 42 | }); - 43 | - 44 | test("the language picker re-renders the welcome text live", async ({ page }) => { - 45 | await page.goto("/"); - 46 | const sel = page.locator("#welcome-lang"); - 47 | await expect(sel).toHaveValue("en"); - 48 | await expect(page.locator("#welcome-launch")).toHaveText("Launch Simulator"); - 49 | - 50 | await sel.selectOption("es"); - 51 | await expect(page.locator("#welcome-launch")).toHaveText("Iniciar simulador"); - 52 | await expect(page.locator("#welcome-title")).toHaveText(/Atención/); - 53 | await expect(page.locator("html")).toHaveAttribute("lang", "es"); - 54 | // the hidden panel selector mirrors the choice - 55 | await expect(page.locator("#lang-select")).toHaveValue("es"); - 56 | - 57 | await sel.selectOption("ja"); - 58 | await expect(page.locator("#welcome-launch")).toHaveText("シミュレーターを起動"); - 59 | }); - 60 | - 61 | test("Launch when already loaded enters the simulation directly and reveals the panel", async ({ page }) => { - 62 | await page.goto("/"); - 63 | await ready(page); - 64 | - 65 | await page.locator("#welcome-launch").click(); - 66 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 67 | await expect(page.locator(".panel")).toBeVisible(); - 68 | - 69 | // Welcome assumes Video on; Audio default 2 carried into the panel. - 70 | await expect(page.locator("#visual")).toBeChecked(); - 71 | await expect(page.locator("#audio")).toHaveValue("2"); - 72 | }); - 73 | - 74 | test("welcome control values carry into the panel on entry", async ({ page }) => { - 75 | await page.goto("/"); - 76 | await ready(page); - 77 | - 78 | await setRange(page, "welcome-audio", "5"); - 79 | await setRange(page, "welcome-play-speed", "2"); // mirrors into the panel slider live - 80 | await page.locator("#welcome-launch").click(); - 81 | - 82 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 5_000 }); - 83 | await expect(page.locator("#audio")).toHaveValue("5"); - 84 | await expect(page.locator("#play-speed")).toHaveValue("2"); - 85 | await expect(page.locator("#visual")).toBeChecked(); // welcome assumes video on - 86 | }); - 87 | - 88 | test("Launch while still loading shows the centered loader with controls, then auto-enters", async ({ page }) => { - 89 | // Stall media so phase-1 preload hasn't finished when Launch is pressed. - 90 | let release!: () => void; - 91 | const gate = new Promise((r) => (release = r)); - 92 | await page.route("**/media/**", async (route) => { - 93 | await gate; - 94 | await route.continue(); - 95 | }); - 96 | - 97 | await page.goto("/"); - 98 | await expect(page.locator("#welcome")).toHaveClass(/welcoming/); - 99 | - 100 | await page.locator("#welcome-launch").click(); - 101 | - 102 | // State B: messaging + button hidden, controls remain, loader shown (centered). - 103 | await expect(page.locator("#welcome")).toHaveClass(/loading/); - 104 | await expect(page.locator("#welcome-title")).toBeHidden(); - 105 | await expect(page.locator("#welcome-launch")).toBeHidden(); - 106 | await expect(page.locator(".welcome-controls")).toBeVisible(); - 107 | await expect(page.locator(".welcome-loading")).toBeVisible(); - 108 | - 109 | // Let media through → universe becomes ready → auto-enter. - 110 | release(); - 111 | await expect(page.locator("#welcome")).toHaveCount(0, { timeout: 30_000 }); - 112 | await expect(page.locator(".panel")).toBeVisible(); - 113 | }); - 114 | -``` \ No newline at end of file