306 lines
9.9 KiB
TypeScript
306 lines
9.9 KiB
TypeScript
import { expect, expectInsetFocusIndicator, test } from "../helpers/serve-dist";
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await page.addInitScript(() => {
|
|
const OriginalAudioContext = window.AudioContext;
|
|
(window as unknown as { __audioConstructed: number }).__audioConstructed =
|
|
0;
|
|
(window as unknown as { __audioContexts: AudioContext[] }).__audioContexts =
|
|
[];
|
|
(window as unknown as { __audioResumeCalls: number }).__audioResumeCalls = 0;
|
|
(window as unknown as { __audioSuspendCalls: number }).__audioSuspendCalls = 0;
|
|
if (!OriginalAudioContext) return;
|
|
class TrackingAudioContext extends OriginalAudioContext {
|
|
constructor(options?: AudioContextOptions) {
|
|
super(options);
|
|
const state = window as unknown as {
|
|
__audioConstructed: number;
|
|
__audioContexts: AudioContext[];
|
|
};
|
|
state.__audioConstructed += 1;
|
|
state.__audioContexts.push(this);
|
|
}
|
|
override resume(): Promise<void> {
|
|
(window as unknown as { __audioResumeCalls: number }).__audioResumeCalls += 1;
|
|
return super.resume();
|
|
}
|
|
override suspend(): Promise<void> {
|
|
(window as unknown as { __audioSuspendCalls: number }).__audioSuspendCalls += 1;
|
|
return super.suspend();
|
|
}
|
|
}
|
|
Object.defineProperty(window, "AudioContext", {
|
|
configurable: true,
|
|
value: TrackingAudioContext,
|
|
});
|
|
});
|
|
});
|
|
|
|
test("makes the required soundtrack gesture explicit while the visual drive is already running", async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
|
|
|
await expect(page.locator(".journey-canvas")).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Play soundtrack" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Start the drive" }),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("starts Web Audio synchronously from the visitor gesture and exposes accessible controls", async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
|
await page.evaluate(() => {
|
|
(
|
|
window as unknown as { __constructedDuringGesture?: number }
|
|
).__constructedDuringGesture = -1;
|
|
window.addEventListener("click", (event) => {
|
|
if (
|
|
(event.target as HTMLElement | null)?.textContent?.includes(
|
|
"PLAY SOUNDTRACK",
|
|
)
|
|
) {
|
|
const state = window as unknown as {
|
|
__audioConstructed: number;
|
|
__constructedDuringGesture: number;
|
|
};
|
|
state.__constructedDuringGesture = state.__audioConstructed;
|
|
}
|
|
});
|
|
});
|
|
|
|
await expect(
|
|
page.getByRole("button", { name: "Play soundtrack" }),
|
|
).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Mute sound" })).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Sound volume" })).toHaveCount(
|
|
0,
|
|
);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
(window as unknown as { __audioConstructed: number })
|
|
.__audioConstructed,
|
|
),
|
|
)
|
|
.toBe(0);
|
|
|
|
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
|
|
|
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Sound volume" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Pause motion" }),
|
|
).toBeVisible();
|
|
await expect(
|
|
page.getByRole("button", { name: "Drive settings" }),
|
|
).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
(window as unknown as { __constructedDuringGesture: number })
|
|
.__constructedDuringGesture,
|
|
),
|
|
)
|
|
.toBe(1);
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
(window as unknown as { __audioConstructed: number })
|
|
.__audioConstructed,
|
|
),
|
|
)
|
|
.toBe(1);
|
|
|
|
await page.getByRole("button", { name: "Mute sound" }).click();
|
|
await expect(
|
|
page.getByRole("button", { name: "Unmute sound" }),
|
|
).toBeVisible();
|
|
});
|
|
|
|
test("keeps the compact controls usable and dismisses the upward panels", async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
|
|
|
const controls = page
|
|
.getByRole("group", { name: "Drive controls" })
|
|
.getByRole("button");
|
|
const targets = await controls.evaluateAll((buttons) =>
|
|
buttons.map((button) => {
|
|
const rect = button.getBoundingClientRect();
|
|
return { width: rect.width, height: rect.height };
|
|
}),
|
|
);
|
|
expect(
|
|
targets.every((target) => target.width >= 44 && target.height >= 44),
|
|
).toBe(true);
|
|
|
|
const settings = page.getByRole("button", { name: "Drive settings" });
|
|
await settings.focus();
|
|
await expectInsetFocusIndicator(page, 'button[aria-label="Drive settings"]');
|
|
await settings.click();
|
|
await expect(
|
|
page.getByRole("dialog", { name: "Drive settings" }),
|
|
).toBeVisible();
|
|
const surfaces = await page
|
|
.locator(".journey-hud, .drive-controls__dock, .drive-popover")
|
|
.evaluateAll((elements) =>
|
|
elements.map((element) => {
|
|
const style = getComputedStyle(element);
|
|
const alpha = Number.parseFloat(
|
|
style.backgroundColor.match(/[\d.]+(?=\))/u)?.[0] ?? "1",
|
|
);
|
|
return { alpha, backdropFilter: style.backdropFilter };
|
|
}),
|
|
);
|
|
expect(
|
|
surfaces.every(
|
|
(surface) => surface.alpha >= 0.95 && surface.backdropFilter === "none",
|
|
),
|
|
).toBe(true);
|
|
await page.keyboard.press("Escape");
|
|
await expect(
|
|
page.getByRole("dialog", { name: "Drive settings" }),
|
|
).toHaveCount(0);
|
|
await expect(settings).toBeFocused();
|
|
|
|
await settings.click();
|
|
await page
|
|
.locator(".hero-journey__content")
|
|
.click({ position: { x: 8, y: 8 } });
|
|
await expect(
|
|
page.getByRole("dialog", { name: "Drive settings" }),
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
test("persists native volume and keeps sound independent from Static motion", async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
|
|
|
await page.getByRole("button", { name: "Sound volume" }).click();
|
|
const volume = page.getByLabel("Sound volume level");
|
|
await volume.fill("0.65");
|
|
await expect(volume).toHaveValue("0.65");
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(() => localStorage.getItem("portfolio.audio.volume")),
|
|
)
|
|
.toBe("0.65");
|
|
|
|
await page.getByRole("button", { name: "Drive settings" }).click();
|
|
await page.getByLabel("Quality control").selectOption("static");
|
|
await expect(page.getByRole("button", { name: "Unmute sound" })).toHaveCount(
|
|
0,
|
|
);
|
|
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
|
await expect(volume).toHaveCount(0);
|
|
await page.getByRole("button", { name: "Sound volume" }).click();
|
|
await expect(volume).toBeVisible();
|
|
});
|
|
|
|
test("requires a direct visitor action after browser interruption without creating another graph", async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
|
|
|
await page.evaluate(() => {
|
|
const context = (window as unknown as { __audioContexts: AudioContext[] })
|
|
.__audioContexts[0];
|
|
Object.defineProperty(context, "state", {
|
|
configurable: true,
|
|
value: "interrupted",
|
|
});
|
|
context?.dispatchEvent(new Event("statechange"));
|
|
});
|
|
|
|
await expect(
|
|
page.getByRole("button", { name: "Resume sound" }),
|
|
).toBeVisible();
|
|
await page.getByRole("button", { name: "Resume sound" }).click();
|
|
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
|
await expect
|
|
.poll(() =>
|
|
page.evaluate(
|
|
() =>
|
|
(window as unknown as { __audioConstructed: number })
|
|
.__audioConstructed,
|
|
),
|
|
)
|
|
.toBe(1);
|
|
});
|
|
|
|
test("automatically suspends and resumes the same graph across ordinary visibility changes", async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
|
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
|
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(document, "visibilityState", { configurable: true, value: "hidden" });
|
|
document.dispatchEvent(new Event("visibilitychange"));
|
|
});
|
|
await expect.poll(() => page.evaluate(() => (
|
|
window as unknown as { __audioSuspendCalls: number }
|
|
).__audioSuspendCalls)).toBe(1);
|
|
|
|
await page.evaluate(() => {
|
|
Object.defineProperty(document, "visibilityState", { configurable: true, value: "visible" });
|
|
document.dispatchEvent(new Event("visibilitychange"));
|
|
});
|
|
await expect.poll(() => page.evaluate(() => (
|
|
window as unknown as { __audioResumeCalls: number }
|
|
).__audioResumeCalls)).toBe(2);
|
|
expect(await page.evaluate(() => (
|
|
window as unknown as { __audioConstructed: number }
|
|
).__audioConstructed)).toBe(1);
|
|
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
|
});
|
|
|
|
test("shows a concise non-blocking message when initial audio start is rejected", async ({
|
|
page,
|
|
distUrl,
|
|
}) => {
|
|
await page.addInitScript(() => {
|
|
const OriginalAudioContext = window.AudioContext;
|
|
if (!OriginalAudioContext) return;
|
|
class RejectingAudioContext extends OriginalAudioContext {
|
|
override resume(): Promise<void> {
|
|
return Promise.reject(new DOMException("blocked", "NotAllowedError"));
|
|
}
|
|
}
|
|
Object.defineProperty(window, "AudioContext", {
|
|
configurable: true,
|
|
value: RejectingAudioContext,
|
|
});
|
|
});
|
|
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
|
|
|
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
|
|
|
await expect(
|
|
page.getByText("Audio unavailable. Continue without sound."),
|
|
).toBeVisible();
|
|
await expect(page.locator("main#portfolio-content")).toBeVisible();
|
|
});
|