fix: synchronize responsive navigation and soundtrack cue
This commit is contained in:
@@ -56,7 +56,7 @@ async function enterState(
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
if (state === "audio-running") {
|
||||
await page.getByRole("button", { name: "START THE DRIVE" }).click();
|
||||
await page.getByRole("button", { name: "PLAY SOUNDTRACK" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Mute sound" }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -225,6 +225,7 @@ test("responsive navigation exposes the exact visible label as each link's acces
|
||||
).toHaveCount(0);
|
||||
|
||||
await page.setViewportSize({ width: 320, height: 800 });
|
||||
await revealNavigationLinks(page);
|
||||
const career = page.getByRole("link", { name: "Career", exact: true });
|
||||
await expect(career).toHaveText("Career");
|
||||
await expect(
|
||||
|
||||
@@ -26,6 +26,21 @@ test.beforeEach(async ({ page }) => {
|
||||
});
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -38,7 +53,7 @@ test("starts Web Audio synchronously from the visitor gesture and exposes access
|
||||
window.addEventListener("click", (event) => {
|
||||
if (
|
||||
(event.target as HTMLElement | null)?.textContent?.includes(
|
||||
"START THE DRIVE",
|
||||
"PLAY SOUNDTRACK",
|
||||
)
|
||||
) {
|
||||
const state = window as unknown as {
|
||||
@@ -51,7 +66,7 @@ test("starts Web Audio synchronously from the visitor gesture and exposes access
|
||||
});
|
||||
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Start the drive" }),
|
||||
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(
|
||||
@@ -67,7 +82,7 @@ test("starts Web Audio synchronously from the visitor gesture and exposes access
|
||||
)
|
||||
.toBe(0);
|
||||
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
||||
|
||||
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
||||
await expect(
|
||||
@@ -109,7 +124,7 @@ test("keeps the compact controls usable and dismisses the upward panels", async
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
||||
|
||||
const controls = page
|
||||
.getByRole("group", { name: "Drive controls" })
|
||||
@@ -167,7 +182,7 @@ test("persists native volume and keeps sound independent from Static motion", as
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Sound volume" }).click();
|
||||
const volume = page.getByLabel("Sound volume level");
|
||||
@@ -195,7 +210,7 @@ test("requires a direct visitor action after browser interruption without creati
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
||||
|
||||
await page.evaluate(() => {
|
||||
const context = (window as unknown as { __audioContexts: AudioContext[] })
|
||||
@@ -242,7 +257,7 @@ test("shows a concise non-blocking message when initial audio start is rejected"
|
||||
});
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByText("Audio unavailable. Continue without sound."),
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
import {
|
||||
assertSemanticFaultOracle,
|
||||
authorizedFaultUrl,
|
||||
collectBrowserErrors,
|
||||
readUnhandledRejections,
|
||||
triggerFaultAction,
|
||||
} from '../helpers/test-build-auth';
|
||||
import { installSceneInstrumentation, readSceneInstrumentation } from '../helpers/scene-instrumentation';
|
||||
} from "../helpers/test-build-auth";
|
||||
import {
|
||||
installSceneInstrumentation,
|
||||
readSceneInstrumentation,
|
||||
} from "../helpers/scene-instrumentation";
|
||||
|
||||
test.beforeEach(async ({ page }, testInfo) => {
|
||||
test.skip(testInfo.project.name !== 'desktop-chromium', 'Audio fault matrix runs once on current desktop Chromium.');
|
||||
test.skip(
|
||||
testInfo.project.name !== "desktop-chromium",
|
||||
"Audio fault matrix runs once on current desktop Chromium.",
|
||||
);
|
||||
await installSceneInstrumentation(page);
|
||||
});
|
||||
|
||||
const startAudio = async (page: import('@playwright/test').Page): Promise<void> => {
|
||||
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
|
||||
const startAudio = async (
|
||||
page: import("@playwright/test").Page,
|
||||
): Promise<void> => {
|
||||
await page.getByRole("button", { name: "PLAY SOUNDTRACK" }).click();
|
||||
};
|
||||
|
||||
const assertAudioContained = async (
|
||||
page: import('@playwright/test').Page,
|
||||
page: import("@playwright/test").Page,
|
||||
pageErrors: readonly string[],
|
||||
expectedLiveContexts: number,
|
||||
): Promise<void> => {
|
||||
@@ -31,59 +39,104 @@ const assertAudioContained = async (
|
||||
expect(resources.liveAudioContexts).toBe(expectedLiveContexts);
|
||||
};
|
||||
|
||||
test('F10 contains initial Web Audio start rejection and creates no reusable graph', async ({ page, distUrl }) => {
|
||||
test("F10 contains initial Web Audio start rejection and creates no reusable graph", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
const errors = await collectBrowserErrors(page);
|
||||
await page.goto(authorizedFaultUrl(distUrl, 'F10-audio-start-rejection'), { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(authorizedFaultUrl(distUrl, "F10-audio-start-rejection"), {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await startAudio(page);
|
||||
|
||||
await expect(page.getByText('Audio unavailable. Continue without sound.')).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'START THE DRIVE' })).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByText("Audio unavailable. Continue without sound."),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "PLAY SOUNDTRACK" }),
|
||||
).toHaveCount(0);
|
||||
await assertSemanticFaultOracle(page);
|
||||
expect(errors.pageErrors).toEqual([]);
|
||||
expect(await readUnhandledRejections(page)).toEqual([]);
|
||||
expect((await readSceneInstrumentation(page)).audioContextsConstructed).toBe(0);
|
||||
expect((await readSceneInstrumentation(page)).audioContextsConstructed).toBe(
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
test('F11 keeps an interrupted intact graph suspended after resume rejection', async ({ page, distUrl }) => {
|
||||
test("F11 keeps an interrupted intact graph suspended after resume rejection", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
const errors = await collectBrowserErrors(page);
|
||||
await page.goto(authorizedFaultUrl(distUrl, 'F11-audio-resume-rejection'), { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(authorizedFaultUrl(distUrl, "F11-audio-resume-rejection"), {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await startAudio(page);
|
||||
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
||||
|
||||
await triggerFaultAction(page, 'audio-interruption');
|
||||
await expect(page.getByRole('button', { name: 'Resume sound' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Resume sound' }).click();
|
||||
await triggerFaultAction(page, "audio-interruption");
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Resume sound" }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "Resume sound" }).click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Resume sound' })).toBeVisible();
|
||||
await expect(page.getByText('Audio unavailable. Continue without sound.')).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Resume sound" }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Audio unavailable. Continue without sound."),
|
||||
).toHaveCount(0);
|
||||
await assertAudioContained(page, errors.pageErrors, 1);
|
||||
});
|
||||
|
||||
test('F12 tears down a graph that becomes invalid during resume', async ({ page, distUrl }) => {
|
||||
test("F12 tears down a graph that becomes invalid during resume", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
const errors = await collectBrowserErrors(page);
|
||||
await page.goto(authorizedFaultUrl(distUrl, 'F12-audio-graph-invalid'), { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(authorizedFaultUrl(distUrl, "F12-audio-graph-invalid"), {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await startAudio(page);
|
||||
await triggerFaultAction(page, 'audio-interruption');
|
||||
await expect(page.getByRole('button', { name: 'Resume sound' })).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Resume sound' }).click();
|
||||
await triggerFaultAction(page, "audio-interruption");
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Resume sound" }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "Resume sound" }).click();
|
||||
|
||||
await expect(page.getByText('Audio unavailable. Continue without sound.')).toBeVisible();
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).liveAudioContexts).toBe(0);
|
||||
await expect(
|
||||
page.getByText("Audio unavailable. Continue without sound."),
|
||||
).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => (await readSceneInstrumentation(page)).liveAudioContexts)
|
||||
.toBe(0);
|
||||
await assertAudioContained(page, errors.pageErrors, 0);
|
||||
});
|
||||
|
||||
test('F13 synchronously silences and closes the graph when hide-time suspend rejects', async ({ page, distUrl }) => {
|
||||
test("F13 synchronously silences and closes the graph when hide-time suspend rejects", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
const errors = await collectBrowserErrors(page);
|
||||
await page.goto(authorizedFaultUrl(distUrl, 'F13-audio-suspend-rejection'), { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(authorizedFaultUrl(distUrl, "F13-audio-suspend-rejection"), {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await startAudio(page);
|
||||
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
|
||||
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'));
|
||||
Object.defineProperty(document, "visibilityState", {
|
||||
configurable: true,
|
||||
value: "hidden",
|
||||
});
|
||||
document.dispatchEvent(new Event("visibilitychange"));
|
||||
});
|
||||
|
||||
await expect(page.getByText('Audio unavailable. Continue without sound.')).toBeVisible();
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).liveAudioContexts).toBe(0);
|
||||
await expect(
|
||||
page.getByText("Audio unavailable. Continue without sound."),
|
||||
).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => (await readSceneInstrumentation(page)).liveAudioContexts)
|
||||
.toBe(0);
|
||||
await assertAudioContained(page, errors.pageErrors, 0);
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ test("synthetic persisted freeze and restore keeps at most one canvas, RAF loop,
|
||||
"data-scene-state",
|
||||
"running",
|
||||
);
|
||||
await page.getByRole("button", { name: "START THE DRIVE" }).click();
|
||||
await page.getByRole("button", { name: "PLAY SOUNDTRACK" }).click();
|
||||
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
||||
|
||||
for (let cycle = 0; cycle < 3; cycle += 1) {
|
||||
@@ -57,7 +57,7 @@ test("persisted lifecycle events preserve paused and Static states without dupli
|
||||
"running",
|
||||
);
|
||||
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
||||
await page.getByRole("button", { name: "Pause motion" }).click();
|
||||
await dispatchPersistedPageTransition(page, "pagehide");
|
||||
await dispatchPersistedPageTransition(page, "pageshow");
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
|
||||
test('keeps the decorative world separate from the semantic reading path', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
test("keeps the decorative world separate from the semantic reading path", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.locator('#journey-scene-description')).toContainText('night');
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('aria-hidden', 'true');
|
||||
await expect(page.locator('.journey-poster')).toHaveAttribute('alt', '');
|
||||
await expect(page.locator('.journey-poster')).toHaveAttribute('width', '1600');
|
||||
await expect(page.locator('main#portfolio-content')).toBeVisible();
|
||||
await expect(page.locator("#journey-scene-description")).toContainText(
|
||||
"night",
|
||||
);
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"aria-hidden",
|
||||
"true",
|
||||
);
|
||||
await expect(page.locator(".journey-poster")).toHaveAttribute("alt", "");
|
||||
await expect(page.locator(".journey-poster")).toHaveAttribute(
|
||||
"width",
|
||||
"1600",
|
||||
);
|
||||
await expect(page.locator("main#portfolio-content")).toBeVisible();
|
||||
});
|
||||
|
||||
test('keeps the hero and navigation in paint layers above the world', async ({ page, distUrl }) => {
|
||||
test("keeps the hero and navigation in paint layers above the world", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
const layers = await page.evaluate(() => {
|
||||
const world = document.querySelector<HTMLElement>('.journey-world');
|
||||
const shell = document.querySelector<HTMLElement>('.portfolio-shell');
|
||||
const hero = document.querySelector<HTMLElement>('.hero-journey__content');
|
||||
const heading = document.querySelector<HTMLElement>('.hero-journey h1');
|
||||
const navigation = document.querySelector<HTMLElement>('.site-navigation');
|
||||
if (!world || !shell || !hero || !heading || !navigation) throw new Error('Missing top-screen structure');
|
||||
const world = document.querySelector<HTMLElement>(".journey-world");
|
||||
const shell = document.querySelector<HTMLElement>(".portfolio-shell");
|
||||
const hero = document.querySelector<HTMLElement>(".hero-journey__content");
|
||||
const heading = document.querySelector<HTMLElement>(".hero-journey h1");
|
||||
const navigation = document.querySelector<HTMLElement>(".site-navigation");
|
||||
if (!world || !shell || !hero || !heading || !navigation)
|
||||
throw new Error("Missing top-screen structure");
|
||||
const worldStyle = getComputedStyle(world);
|
||||
const heroStyle = getComputedStyle(hero);
|
||||
const navigationStyle = getComputedStyle(navigation);
|
||||
@@ -29,14 +44,20 @@ test('keeps the hero and navigation in paint layers above the world', async ({ p
|
||||
shellZ: Number(getComputedStyle(shell).zIndex),
|
||||
navigationZ: Number(navigationStyle.zIndex),
|
||||
navigationPosition: navigationStyle.position,
|
||||
headingVisible: heading.getBoundingClientRect().width > 0 && heading.getBoundingClientRect().height > 0,
|
||||
navigationVisible: navigation.getBoundingClientRect().width > 0 && navigation.getBoundingClientRect().height > 0,
|
||||
heroOpacity: Number.parseFloat(heroStyle.backgroundColor.match(/[\d.]+(?=\))/gu)?.at(-1) ?? '1'),
|
||||
headingVisible:
|
||||
heading.getBoundingClientRect().width > 0 &&
|
||||
heading.getBoundingClientRect().height > 0,
|
||||
navigationVisible:
|
||||
navigation.getBoundingClientRect().width > 0 &&
|
||||
navigation.getBoundingClientRect().height > 0,
|
||||
heroOpacity: Number.parseFloat(
|
||||
heroStyle.backgroundColor.match(/[\d.]+(?=\))/gu)?.at(-1) ?? "1",
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
expect(layers).toMatchObject({
|
||||
navigationPosition: 'fixed',
|
||||
navigationPosition: "fixed",
|
||||
headingVisible: true,
|
||||
navigationVisible: true,
|
||||
});
|
||||
@@ -45,21 +66,52 @@ test('keeps the hero and navigation in paint layers above the world', async ({ p
|
||||
expect(layers.heroOpacity).toBeGreaterThanOrEqual(0.94);
|
||||
});
|
||||
|
||||
test('keeps a scrolled content panel in a geometry-safe paint layer above the world', async ({ page, distUrl }) => {
|
||||
test("resets the navigation disclosure when crossing the compact breakpoint", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
const disclosure = page.locator(".site-navigation details");
|
||||
const summary = page.locator(".site-navigation summary");
|
||||
const startLink = page.locator('.site-navigation a[href="#start"]');
|
||||
await expect(disclosure).toHaveAttribute("open", "");
|
||||
await expect(startLink).toBeVisible();
|
||||
|
||||
await page.setViewportSize({ width: 630, height: 900 });
|
||||
await expect(summary).toBeVisible();
|
||||
await expect(disclosure).not.toHaveAttribute("open", "");
|
||||
await expect(startLink).toBeHidden();
|
||||
|
||||
await summary.click();
|
||||
await expect(disclosure).toHaveAttribute("open", "");
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await expect(summary).toBeHidden();
|
||||
await expect(disclosure).toHaveAttribute("open", "");
|
||||
await expect(startLink).toBeVisible();
|
||||
});
|
||||
|
||||
test("keeps a scrolled content panel in a geometry-safe paint layer above the world", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
const profileLink = page.locator('nav a[href="#profile"]');
|
||||
if (!(await profileLink.isVisible())) await page.locator('.site-navigation summary').click();
|
||||
if (!(await profileLink.isVisible()))
|
||||
await page.locator(".site-navigation summary").click();
|
||||
await profileLink.click();
|
||||
await expect(page).toHaveURL(`${distUrl}/#profile`);
|
||||
await expect(page.locator('#profile-heading')).toBeInViewport();
|
||||
await expect(page.locator("#profile-heading")).toBeInViewport();
|
||||
|
||||
const layers = await page.evaluate(() => {
|
||||
const world = document.querySelector<HTMLElement>('.journey-world');
|
||||
const main = document.querySelector<HTMLElement>('.portfolio-main');
|
||||
const panel = document.querySelector<HTMLElement>('#profile');
|
||||
const heading = document.querySelector<HTMLElement>('#profile-heading');
|
||||
if (!world || !main || !panel || !heading) throw new Error('Missing scrolled panel structure');
|
||||
const world = document.querySelector<HTMLElement>(".journey-world");
|
||||
const main = document.querySelector<HTMLElement>(".portfolio-main");
|
||||
const panel = document.querySelector<HTMLElement>("#profile");
|
||||
const heading = document.querySelector<HTMLElement>("#profile-heading");
|
||||
if (!world || !main || !panel || !heading)
|
||||
throw new Error("Missing scrolled panel structure");
|
||||
const mainStyle = getComputedStyle(main);
|
||||
const panelStyle = getComputedStyle(panel);
|
||||
const headingRect = heading.getBoundingClientRect();
|
||||
@@ -68,73 +120,107 @@ test('keeps a scrolled content panel in a geometry-safe paint layer above the wo
|
||||
mainIsolation: mainStyle.isolation,
|
||||
mainPosition: mainStyle.position,
|
||||
headingVisible:
|
||||
headingRect.width > 0
|
||||
&& headingRect.height > 0
|
||||
&& headingRect.bottom > 0
|
||||
&& headingRect.top < window.innerHeight,
|
||||
panelOpacity: Number.parseFloat(panelStyle.backgroundColor.match(/[\d.]+(?=\))/gu)?.at(-1) ?? '1'),
|
||||
headingRect.width > 0 &&
|
||||
headingRect.height > 0 &&
|
||||
headingRect.bottom > 0 &&
|
||||
headingRect.top < window.innerHeight,
|
||||
panelOpacity: Number.parseFloat(
|
||||
panelStyle.backgroundColor.match(/[\d.]+(?=\))/gu)?.at(-1) ?? "1",
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
expect(layers).toMatchObject({
|
||||
worldZ: 0,
|
||||
mainIsolation: 'isolate',
|
||||
mainPosition: 'static',
|
||||
mainIsolation: "isolate",
|
||||
mainPosition: "static",
|
||||
headingVisible: true,
|
||||
});
|
||||
expect(layers.panelOpacity).toBeGreaterThanOrEqual(0.94);
|
||||
});
|
||||
|
||||
test('provides native motion and quality controls with visible state', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
test("provides native motion and quality controls with visible state", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await page.getByRole('button', { name: 'Drive settings' }).click();
|
||||
const quality = page.getByLabel('Quality control');
|
||||
await page.getByRole("button", { name: "Drive settings" }).click();
|
||||
const quality = page.getByLabel("Quality control");
|
||||
await expect(quality).toBeVisible();
|
||||
await expect(page.getByRole('status')).toHaveCount(0);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Pause motion' })).toBeVisible();
|
||||
const canvas = page.locator('.journey-canvas');
|
||||
await expect(page.getByRole("status")).toHaveCount(0);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.getByRole("button", { name: "Play soundtrack" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Pause motion" }),
|
||||
).toBeVisible();
|
||||
const canvas = page.locator(".journey-canvas");
|
||||
const originalCanvas = await canvas.elementHandle();
|
||||
const originalGeneration = await page.locator('.journey-world').getAttribute('data-scene-generation');
|
||||
const originalGeneration = await page
|
||||
.locator(".journey-world")
|
||||
.getAttribute("data-scene-generation");
|
||||
|
||||
await page.getByRole('button', { name: 'Pause motion' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Resume motion' })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Pause motion" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Resume motion" }),
|
||||
).toBeVisible();
|
||||
await expect(canvas).toHaveCount(1);
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-generation', originalGeneration ?? '');
|
||||
expect(await originalCanvas?.evaluate((element) => element.isConnected)).toBe(true);
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-generation",
|
||||
originalGeneration ?? "",
|
||||
);
|
||||
expect(await originalCanvas?.evaluate((element) => element.isConnected)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Resume motion' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Pause motion' })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Resume motion" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Pause motion" }),
|
||||
).toBeVisible();
|
||||
await expect(canvas).toHaveCount(1);
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-generation', originalGeneration ?? '');
|
||||
expect(await originalCanvas?.evaluate((element) => element.isConnected)).toBe(true);
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-generation",
|
||||
originalGeneration ?? "",
|
||||
);
|
||||
expect(await originalCanvas?.evaluate((element) => element.isConnected)).toBe(
|
||||
true,
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Drive settings' }).click();
|
||||
await page.getByLabel('Quality control').selectOption('static');
|
||||
await expect(page.getByRole('button', { name: 'Enable motion' })).toBeVisible();
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await expect(page.getByRole('status')).toContainText('Motion: static');
|
||||
await page.getByRole("button", { name: "Drive settings" }).click();
|
||||
await page.getByLabel("Quality control").selectOption("static");
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Enable motion" }),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await expect(page.getByRole("status")).toContainText("Motion: static");
|
||||
});
|
||||
|
||||
test('never creates a canvas when the system requests reduced motion', async ({ page, distUrl }) => {
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
test("never creates a canvas when the system requests reduced motion", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await page.getByRole('button', { name: 'Drive settings' }).click();
|
||||
await expect(page.getByRole('status')).toContainText('reduced motion requested');
|
||||
await expect(page.getByLabel('Quality control')).toBeVisible();
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Drive settings" }).click();
|
||||
await expect(page.getByRole("status")).toContainText(
|
||||
"reduced motion requested",
|
||||
);
|
||||
await expect(page.getByLabel("Quality control")).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows ARRIVED from the authoritative section snapshot at Contact', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
test("shows ARRIVED from the authoritative section snapshot at Contact", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
const contactLink = page.locator('nav a[href="#contact"]');
|
||||
if (!(await contactLink.isVisible())) await page.locator('.site-navigation summary').click();
|
||||
if (!(await contactLink.isVisible()))
|
||||
await page.locator(".site-navigation summary").click();
|
||||
await contactLink.click();
|
||||
|
||||
await expect(page.locator('.journey-hud')).toContainText('Contact');
|
||||
await expect(page.locator('.journey-hud')).toContainText('ARRIVED');
|
||||
await expect(page.locator('.journey-hud')).toContainText('100%');
|
||||
await expect(page.locator(".journey-hud")).toContainText("Contact");
|
||||
await expect(page.locator(".journey-hud")).toContainText("ARRIVED");
|
||||
await expect(page.locator(".journey-hud")).toContainText("100%");
|
||||
});
|
||||
|
||||
@@ -395,7 +395,7 @@ test.describe("production visual composition", () => {
|
||||
}, testInfo) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await waitForRenderedPortfolio(page, distUrl);
|
||||
await page.getByRole("button", { name: "START THE DRIVE" }).click();
|
||||
await page.getByRole("button", { name: "PLAY SOUNDTRACK" }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Mute sound" }),
|
||||
).toBeVisible();
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { performance as nodePerformance } from 'node:perf_hooks';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import type { CDPSession, Page } from '@playwright/test';
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import { authorizedPerformanceUrl } from '../helpers/test-build-auth';
|
||||
import { loadAuthenticatedReferenceRun, validateObservedReferenceEnvironment } from './reference-run';
|
||||
import { calculatePeakRequestConcurrency, evaluateRuntimeTraceBudgetEvidence } from '../../scripts/lib/bundle-budgets.mjs';
|
||||
import { performance as nodePerformance } from "node:perf_hooks";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import type { CDPSession, Page } from "@playwright/test";
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
import { authorizedPerformanceUrl } from "../helpers/test-build-auth";
|
||||
import {
|
||||
loadAuthenticatedReferenceRun,
|
||||
validateObservedReferenceEnvironment,
|
||||
} from "./reference-run";
|
||||
import {
|
||||
calculatePeakRequestConcurrency,
|
||||
evaluateRuntimeTraceBudgetEvidence,
|
||||
} from "../../scripts/lib/bundle-budgets.mjs";
|
||||
import {
|
||||
calculateCls,
|
||||
calculateLabTbt,
|
||||
@@ -16,24 +22,46 @@ import {
|
||||
postRunningLongTasks,
|
||||
unexpectedLateShifts,
|
||||
type ScriptedActionSample,
|
||||
} from '../../src/testing/performance-instrumentation';
|
||||
} from "../../src/testing/performance-instrumentation";
|
||||
|
||||
type NamedProfile = 'desktop-high' | 'mobile-low';
|
||||
type NamedProfile = "desktop-high" | "mobile-low";
|
||||
|
||||
const profileForProject = (name: string): {
|
||||
const profileForProject = (
|
||||
name: string,
|
||||
): {
|
||||
name: NamedProfile;
|
||||
tier: 'high' | 'low';
|
||||
tier: "high" | "low";
|
||||
viewport: { width: number; height: number };
|
||||
latency: number;
|
||||
download: number;
|
||||
upload: number;
|
||||
sceneCpuP95: number;
|
||||
lostShare: number;
|
||||
} => name === 'performance-mobile'
|
||||
? { name: 'mobile-low', tier: 'low', viewport: { width: 390, height: 844 }, latency: 150, download: 200_000, upload: 93_750, sceneCpuP95: 24, lostShare: 0.1 }
|
||||
: { name: 'desktop-high', tier: 'high', viewport: { width: 1_440, height: 900 }, latency: 40, download: 1_250_000, upload: 1_250_000, sceneCpuP95: 12, lostShare: 0.05 };
|
||||
} =>
|
||||
name === "performance-mobile"
|
||||
? {
|
||||
name: "mobile-low",
|
||||
tier: "low",
|
||||
viewport: { width: 390, height: 844 },
|
||||
latency: 150,
|
||||
download: 200_000,
|
||||
upload: 93_750,
|
||||
sceneCpuP95: 24,
|
||||
lostShare: 0.1,
|
||||
}
|
||||
: {
|
||||
name: "desktop-high",
|
||||
tier: "high",
|
||||
viewport: { width: 1_440, height: 900 },
|
||||
latency: 40,
|
||||
download: 1_250_000,
|
||||
upload: 1_250_000,
|
||||
sceneCpuP95: 12,
|
||||
lostShare: 0.05,
|
||||
};
|
||||
|
||||
const sleep = (duration: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, duration));
|
||||
const sleep = (duration: number): Promise<void> =>
|
||||
new Promise((resolve) => setTimeout(resolve, duration));
|
||||
|
||||
const stableTargetCenter = async (page: Page, selector: string) => {
|
||||
const locator = page.locator(selector).filter({ visible: true });
|
||||
@@ -55,19 +83,42 @@ const dispatchPointerAction = async (
|
||||
await at(offset - 50);
|
||||
const center = await stableTargetCenter(page, selector);
|
||||
const markerSelector = `[data-drive-target="${id}"]`;
|
||||
await page.locator(selector).filter({ visible: true }).evaluate((element, actionId) => {
|
||||
element.setAttribute('data-drive-target', actionId);
|
||||
}, id);
|
||||
await page
|
||||
.locator(selector)
|
||||
.filter({ visible: true })
|
||||
.evaluate((element, actionId) => {
|
||||
element.setAttribute("data-drive-target", actionId);
|
||||
}, id);
|
||||
await at(offset - 20);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', ...center });
|
||||
await cdp.send("Input.dispatchMouseEvent", { type: "mouseMoved", ...center });
|
||||
await at(offset);
|
||||
await page.evaluate(({ actionId, target }) => window.__PORTFOLIO_PERFORMANCE__?.beginAction(actionId, target), { actionId: id, target: markerSelector });
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', button: 'left', clickCount: 1, ...center });
|
||||
await page.evaluate(
|
||||
({ actionId, target }) =>
|
||||
window.__PORTFOLIO_PERFORMANCE__?.beginAction(actionId, target),
|
||||
{ actionId: id, target: markerSelector },
|
||||
);
|
||||
await cdp.send("Input.dispatchMouseEvent", {
|
||||
type: "mousePressed",
|
||||
button: "left",
|
||||
clickCount: 1,
|
||||
...center,
|
||||
});
|
||||
await at(offset + 40);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', button: 'left', clickCount: 1, ...center });
|
||||
await cdp.send("Input.dispatchMouseEvent", {
|
||||
type: "mouseReleased",
|
||||
button: "left",
|
||||
clickCount: 1,
|
||||
...center,
|
||||
});
|
||||
};
|
||||
|
||||
const dispatchWheelBurst = async (page: Page, cdp: CDPSession, at: (offset: number) => Promise<void>, offset: number, deltaY: number) => {
|
||||
const dispatchWheelBurst = async (
|
||||
page: Page,
|
||||
cdp: CDPSession,
|
||||
at: (offset: number) => Promise<void>,
|
||||
offset: number,
|
||||
deltaY: number,
|
||||
) => {
|
||||
const center = await page.evaluate(() => {
|
||||
const viewport = window.visualViewport;
|
||||
return {
|
||||
@@ -77,64 +128,103 @@ const dispatchWheelBurst = async (page: Page, cdp: CDPSession, at: (offset: numb
|
||||
});
|
||||
for (const deltaOffset of [0, 40, 80, 120, 160, 200]) {
|
||||
await at(offset + deltaOffset);
|
||||
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseWheel', deltaX: 0, deltaY, ...center });
|
||||
await cdp.send("Input.dispatchMouseEvent", {
|
||||
type: "mouseWheel",
|
||||
deltaX: 0,
|
||||
deltaY,
|
||||
...center,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
test('drive-trace.v1 runs on current Chromium with fail-closed reference authentication', async ({ browser, page, distUrl }, testInfo) => {
|
||||
test("drive-trace.v1 runs on current Chromium with fail-closed reference authentication", async ({
|
||||
browser,
|
||||
page,
|
||||
distUrl,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(75_000);
|
||||
const profile = profileForProject(testInfo.project.name);
|
||||
const referenceRun = await loadAuthenticatedReferenceRun(profile.name);
|
||||
await page.setViewportSize(profile.viewport);
|
||||
const cdp = await page.context().newCDPSession(page);
|
||||
await cdp.send('Network.enable');
|
||||
await cdp.send('Network.clearBrowserCache');
|
||||
await cdp.send('Network.emulateNetworkConditions', {
|
||||
await cdp.send("Network.enable");
|
||||
await cdp.send("Network.clearBrowserCache");
|
||||
await cdp.send("Network.emulateNetworkConditions", {
|
||||
offline: false,
|
||||
latency: profile.latency,
|
||||
downloadThroughput: profile.download,
|
||||
uploadThroughput: profile.upload,
|
||||
});
|
||||
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
||||
await page.goto('about:blank');
|
||||
const startedFromAboutBlank = page.url() === 'about:blank';
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await page.goto("about:blank");
|
||||
const startedFromAboutBlank = page.url() === "about:blank";
|
||||
const loadStarted = nodePerformance.now();
|
||||
await page.goto(authorizedPerformanceUrl(distUrl, profile.tier), { waitUntil: 'load', timeout: 10_000 });
|
||||
await page.goto(authorizedPerformanceUrl(distUrl, profile.tier), {
|
||||
waitUntil: "load",
|
||||
timeout: 10_000,
|
||||
});
|
||||
const loadDuration = nodePerformance.now() - loadStarted;
|
||||
expect(loadDuration).toBeLessThanOrEqual(10_000);
|
||||
await expect.poll(() => page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.snapshot().lcp.length ?? 0), { timeout: 10_000 }).toBeGreaterThan(0);
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running', { timeout: 5_000 });
|
||||
await expect(page.locator('.journey-hud__quality')).toHaveText(profile.tier === 'high' ? 'High' : 'Low');
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
() => window.__PORTFOLIO_PERFORMANCE__?.snapshot().lcp.length ?? 0,
|
||||
),
|
||||
{ timeout: 10_000 },
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-state",
|
||||
"running",
|
||||
{ timeout: 5_000 },
|
||||
);
|
||||
await expect(page.locator(".journey-hud__quality")).toHaveText(
|
||||
profile.tier === "high" ? "High" : "Low",
|
||||
);
|
||||
if (referenceRun !== null) {
|
||||
const observed = await page.evaluate(() => ({
|
||||
viewport: { width: innerWidth, height: innerHeight },
|
||||
visible: document.visibilityState === 'visible',
|
||||
reducedMotion: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' as const : 'no-preference' as const,
|
||||
saveData: Boolean((navigator as Navigator & { connection?: { saveData?: boolean } }).connection?.saveData),
|
||||
visible: document.visibilityState === "visible",
|
||||
reducedMotion: matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||
? ("reduce" as const)
|
||||
: ("no-preference" as const),
|
||||
saveData: Boolean(
|
||||
(navigator as Navigator & { connection?: { saveData?: boolean } })
|
||||
.connection?.saveData,
|
||||
),
|
||||
pageZoom: visualViewport?.scale ?? 1,
|
||||
devicePixelRatio,
|
||||
}));
|
||||
validateObservedReferenceEnvironment(referenceRun, {
|
||||
...observed,
|
||||
browserName: 'Chromium',
|
||||
browserName: "Chromium",
|
||||
browserVersion: browser.version(),
|
||||
startedFromAboutBlank,
|
||||
});
|
||||
}
|
||||
|
||||
const warmupRafs = await page.evaluate(() => new Promise<number[]>((resolve) => {
|
||||
const samples: number[] = [];
|
||||
const frame = (now: number): void => {
|
||||
samples.push(now);
|
||||
if (samples.length === 121) resolve(samples);
|
||||
else requestAnimationFrame(frame);
|
||||
};
|
||||
requestAnimationFrame(frame);
|
||||
}));
|
||||
const warmupIntervals = warmupRafs.slice(1).map((stamp, index) => stamp - warmupRafs[index]!);
|
||||
const warmupMedian = [...warmupIntervals].sort((a, b) => a - b)[Math.floor(warmupIntervals.length / 2)]!;
|
||||
const warmupRafs = await page.evaluate(
|
||||
() =>
|
||||
new Promise<number[]>((resolve) => {
|
||||
const samples: number[] = [];
|
||||
const frame = (now: number): void => {
|
||||
samples.push(now);
|
||||
if (samples.length === 121) resolve(samples);
|
||||
else requestAnimationFrame(frame);
|
||||
};
|
||||
requestAnimationFrame(frame);
|
||||
}),
|
||||
);
|
||||
const warmupIntervals = warmupRafs
|
||||
.slice(1)
|
||||
.map((stamp, index) => stamp - warmupRafs[index]!);
|
||||
const warmupMedian = [...warmupIntervals].sort((a, b) => a - b)[
|
||||
Math.floor(warmupIntervals.length / 2)
|
||||
]!;
|
||||
const validReferenceCadence = Math.abs(warmupMedian - 16.667) <= 1;
|
||||
if (process.env.PORTFOLIO_REFERENCE_LAB === '1') expect(validReferenceCadence).toBe(true);
|
||||
if (process.env.PORTFOLIO_REFERENCE_LAB === "1")
|
||||
expect(validReferenceCadence).toBe(true);
|
||||
|
||||
await page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.startTrace());
|
||||
const nodeTraceStart = nodePerformance.now();
|
||||
@@ -143,68 +233,207 @@ test('drive-trace.v1 runs on current Chromium with fail-closed reference authent
|
||||
if (remaining > 0) await sleep(remaining);
|
||||
};
|
||||
|
||||
await dispatchPointerAction(page, cdp, at, 1_000, 'start', 'button:has-text("START THE DRIVE")');
|
||||
await dispatchPointerAction(
|
||||
page,
|
||||
cdp,
|
||||
at,
|
||||
1_000,
|
||||
"start",
|
||||
'button:has-text("PLAY SOUNDTRACK")',
|
||||
);
|
||||
await dispatchWheelBurst(page, cdp, at, 1_500, 100);
|
||||
await at(2_350);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
|
||||
await cdp.send("Input.dispatchKeyEvent", {
|
||||
type: "keyDown",
|
||||
key: "Tab",
|
||||
code: "Tab",
|
||||
windowsVirtualKeyCode: 9,
|
||||
});
|
||||
await at(2_370);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'Tab', code: 'Tab', windowsVirtualKeyCode: 9 });
|
||||
await expect(page.getByRole('slider', { name: 'Sound volume' })).toBeFocused();
|
||||
await cdp.send("Input.dispatchKeyEvent", {
|
||||
type: "keyUp",
|
||||
key: "Tab",
|
||||
code: "Tab",
|
||||
windowsVirtualKeyCode: 9,
|
||||
});
|
||||
await expect(
|
||||
page.getByRole("slider", { name: "Sound volume" }),
|
||||
).toBeFocused();
|
||||
await at(2_400);
|
||||
await page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.beginAction('volume', 'input[aria-label="Sound volume"]'));
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', key: 'ArrowLeft', code: 'ArrowLeft', windowsVirtualKeyCode: 37 });
|
||||
await page.evaluate(() =>
|
||||
window.__PORTFOLIO_PERFORMANCE__?.beginAction(
|
||||
"volume",
|
||||
'input[aria-label="Sound volume"]',
|
||||
),
|
||||
);
|
||||
await cdp.send("Input.dispatchKeyEvent", {
|
||||
type: "keyDown",
|
||||
key: "ArrowLeft",
|
||||
code: "ArrowLeft",
|
||||
windowsVirtualKeyCode: 37,
|
||||
});
|
||||
await at(2_440);
|
||||
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', key: 'ArrowLeft', code: 'ArrowLeft', windowsVirtualKeyCode: 37 });
|
||||
await expect(page.getByRole('slider', { name: 'Sound volume' })).toHaveValue('0.35');
|
||||
await cdp.send("Input.dispatchKeyEvent", {
|
||||
type: "keyUp",
|
||||
key: "ArrowLeft",
|
||||
code: "ArrowLeft",
|
||||
windowsVirtualKeyCode: 37,
|
||||
});
|
||||
await expect(page.getByRole("slider", { name: "Sound volume" })).toHaveValue(
|
||||
"0.35",
|
||||
);
|
||||
await dispatchWheelBurst(page, cdp, at, 2_800, 100);
|
||||
await dispatchPointerAction(page, cdp, at, 3_700, 'mute', 'button:has-text("Mute sound")');
|
||||
await expect(page.getByRole('button', { name: 'Unmute sound' })).toBeVisible();
|
||||
await dispatchPointerAction(
|
||||
page,
|
||||
cdp,
|
||||
at,
|
||||
3_700,
|
||||
"mute",
|
||||
'button:has-text("Mute sound")',
|
||||
);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Unmute sound" }),
|
||||
).toBeVisible();
|
||||
await dispatchWheelBurst(page, cdp, at, 4_100, 100);
|
||||
await dispatchPointerAction(page, cdp, at, 5_000, 'unmute', 'button:has-text("Unmute sound")');
|
||||
await dispatchPointerAction(
|
||||
page,
|
||||
cdp,
|
||||
at,
|
||||
5_000,
|
||||
"unmute",
|
||||
'button:has-text("Unmute sound")',
|
||||
);
|
||||
await dispatchWheelBurst(page, cdp, at, 5_400, 100);
|
||||
await dispatchPointerAction(page, cdp, at, 6_300, 'pause', 'button:has-text("Pause motion")');
|
||||
await dispatchPointerAction(
|
||||
page,
|
||||
cdp,
|
||||
at,
|
||||
6_300,
|
||||
"pause",
|
||||
'button:has-text("Pause motion")',
|
||||
);
|
||||
await dispatchWheelBurst(page, cdp, at, 6_700, 100);
|
||||
await dispatchPointerAction(page, cdp, at, 7_600, 'resume', 'button:has-text("Resume motion")');
|
||||
await dispatchPointerAction(
|
||||
page,
|
||||
cdp,
|
||||
at,
|
||||
7_600,
|
||||
"resume",
|
||||
'button:has-text("Resume motion")',
|
||||
);
|
||||
await dispatchWheelBurst(page, cdp, at, 8_000, 100);
|
||||
await at(8_800);
|
||||
await page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.armNavigationSettlementBarrier());
|
||||
await dispatchPointerAction(page, cdp, at, 8_900, 'toolkit', 'nav a[href="#toolkit"]');
|
||||
await page.evaluate(() =>
|
||||
window.__PORTFOLIO_PERFORMANCE__?.armNavigationSettlementBarrier(),
|
||||
);
|
||||
await dispatchPointerAction(
|
||||
page,
|
||||
cdp,
|
||||
at,
|
||||
8_900,
|
||||
"toolkit",
|
||||
'nav a[href="#toolkit"]',
|
||||
);
|
||||
await dispatchWheelBurst(page, cdp, at, 9_300, -100);
|
||||
await dispatchWheelBurst(page, cdp, at, 10_600, -100);
|
||||
await dispatchWheelBurst(page, cdp, at, 11_900, 100);
|
||||
await dispatchWheelBurst(page, cdp, at, 13_200, 100);
|
||||
await dispatchPointerAction(page, cdp, at, 14_500, 'contact', 'nav a[href="#contact"]');
|
||||
await dispatchPointerAction(
|
||||
page,
|
||||
cdp,
|
||||
at,
|
||||
14_500,
|
||||
"contact",
|
||||
'nav a[href="#contact"]',
|
||||
);
|
||||
await at(20_000);
|
||||
await page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.endTrace());
|
||||
|
||||
await expect(page).toHaveURL(/#contact$/u);
|
||||
await expect(page.locator('#contact > h2')).toBeFocused();
|
||||
await expect(page.locator('.journey-hud__quality')).toHaveText(profile.tier === 'high' ? 'High' : 'Low');
|
||||
const snapshot = await page.evaluate(() => window.__PORTFOLIO_PERFORMANCE__?.snapshot());
|
||||
if (snapshot === undefined || snapshot.traceStart === null || snapshot.traceEnd === null || snapshot.firstSceneFrame === null) {
|
||||
throw new Error('The authenticated performance snapshot is incomplete.');
|
||||
await expect(page.locator("#contact > h2")).toBeFocused();
|
||||
await expect(page.locator(".journey-hud__quality")).toHaveText(
|
||||
profile.tier === "high" ? "High" : "Low",
|
||||
);
|
||||
const snapshot = await page.evaluate(() =>
|
||||
window.__PORTFOLIO_PERFORMANCE__?.snapshot(),
|
||||
);
|
||||
if (
|
||||
snapshot === undefined ||
|
||||
snapshot.traceStart === null ||
|
||||
snapshot.traceEnd === null ||
|
||||
snapshot.firstSceneFrame === null
|
||||
) {
|
||||
throw new Error("The authenticated performance snapshot is incomplete.");
|
||||
}
|
||||
if (snapshot.runtimeAllocation === null) throw new Error('The production scene did not emit an actual allocation ledger.');
|
||||
if (snapshot.runtimeAllocation === null)
|
||||
throw new Error(
|
||||
"The production scene did not emit an actual allocation ledger.",
|
||||
);
|
||||
const expectedPeriod = warmupMedian;
|
||||
const fcp = await page.evaluate(() => performance.getEntriesByName('first-contentful-paint')[0]?.startTime ?? null);
|
||||
const fcp = await page.evaluate(
|
||||
() =>
|
||||
performance.getEntriesByName("first-contentful-paint")[0]?.startTime ??
|
||||
null,
|
||||
);
|
||||
const lcp = latestLcpBeforeCutoff(snapshot.lcp, snapshot.cutoff);
|
||||
const cls = calculateCls(snapshot.layoutShifts);
|
||||
const tbt = calculateLabTbt(snapshot.longTasks, fcp, snapshot.traceStart);
|
||||
const postRunning = postRunningLongTasks(snapshot.longTasks, snapshot.traceStart, snapshot.traceEnd);
|
||||
const eventToPaint = evaluateEventToNextPaint(snapshot.actions as ScriptedActionSample[]);
|
||||
const continuity = evaluateSceneContinuity(snapshot.sceneIntervals, snapshot.sceneFrames, expectedPeriod);
|
||||
const lost = calculateLostPresentationShare(snapshot.pageRafs, expectedPeriod, Math.floor(20_000 / expectedPeriod) - 3);
|
||||
const lateShifts = unexpectedLateShifts(snapshot.layoutShifts, snapshot.firstSceneFrame, snapshot.traceEnd);
|
||||
const sceneCpuP95 = nearestRankP95(snapshot.sceneFrames.map((frame) => frame.end - frame.start));
|
||||
const resourceTimings = await page.evaluate(() => performance.getEntriesByType('resource').map((entry) => {
|
||||
const resource = entry as PerformanceResourceTiming;
|
||||
return { name: resource.name, initiatorType: resource.initiatorType, startTime: resource.startTime, responseEnd: resource.responseEnd };
|
||||
}));
|
||||
const postRunning = postRunningLongTasks(
|
||||
snapshot.longTasks,
|
||||
snapshot.traceStart,
|
||||
snapshot.traceEnd,
|
||||
);
|
||||
const eventToPaint = evaluateEventToNextPaint(
|
||||
snapshot.actions as ScriptedActionSample[],
|
||||
);
|
||||
const continuity = evaluateSceneContinuity(
|
||||
snapshot.sceneIntervals,
|
||||
snapshot.sceneFrames,
|
||||
expectedPeriod,
|
||||
);
|
||||
const lost = calculateLostPresentationShare(
|
||||
snapshot.pageRafs,
|
||||
expectedPeriod,
|
||||
Math.floor(20_000 / expectedPeriod) - 3,
|
||||
);
|
||||
const lateShifts = unexpectedLateShifts(
|
||||
snapshot.layoutShifts,
|
||||
snapshot.firstSceneFrame,
|
||||
snapshot.traceEnd,
|
||||
);
|
||||
const sceneCpuP95 = nearestRankP95(
|
||||
snapshot.sceneFrames.map((frame) => frame.end - frame.start),
|
||||
);
|
||||
const resourceTimings = await page.evaluate(() =>
|
||||
performance.getEntriesByType("resource").map((entry) => {
|
||||
const resource = entry as PerformanceResourceTiming;
|
||||
return {
|
||||
name: resource.name,
|
||||
initiatorType: resource.initiatorType,
|
||||
startTime: resource.startTime,
|
||||
responseEnd: resource.responseEnd,
|
||||
};
|
||||
}),
|
||||
);
|
||||
const peakRequests = calculatePeakRequestConcurrency(resourceTimings);
|
||||
const runtimeBudgetEvidence = { schema: 'portfolio-runtime-trace-evidence/v1', peakRequests, allocation: snapshot.runtimeAllocation };
|
||||
const runtimeBudget = evaluateRuntimeTraceBudgetEvidence(runtimeBudgetEvidence);
|
||||
const runtimeResourcePass = !runtimeBudget.violations.some((item) => ['mobileTextureDimension', 'lowTierGpuEstimate', 'highTierGpuEstimate'].includes(item.budget));
|
||||
const requestConcurrencyPass = !runtimeBudget.violations.some((item) => ['imageSceneConcurrency', 'videoConcurrency'].includes(item.budget));
|
||||
const runtimeBudgetEvidence = {
|
||||
schema: "portfolio-runtime-trace-evidence/v1",
|
||||
peakRequests,
|
||||
allocation: snapshot.runtimeAllocation,
|
||||
};
|
||||
const runtimeBudget = evaluateRuntimeTraceBudgetEvidence(
|
||||
runtimeBudgetEvidence,
|
||||
);
|
||||
const runtimeResourcePass = !runtimeBudget.violations.some((item) =>
|
||||
[
|
||||
"mobileTextureDimension",
|
||||
"lowTierGpuEstimate",
|
||||
"highTierGpuEstimate",
|
||||
].includes(item.budget),
|
||||
);
|
||||
const requestConcurrencyPass = !runtimeBudget.violations.some((item) =>
|
||||
["imageSceneConcurrency", "videoConcurrency"].includes(item.budget),
|
||||
);
|
||||
|
||||
const budgetResults = {
|
||||
lcp: lcp <= 3_000,
|
||||
@@ -214,32 +443,59 @@ test('drive-trace.v1 runs on current Chromium with fail-closed reference authent
|
||||
eventToNextPaint: eventToPaint.p95 <= 200,
|
||||
sceneCpu: sceneCpuP95 <= profile.sceneCpuP95,
|
||||
continuity: continuity.passes,
|
||||
lostPresentation: lost.hasSufficientCoverage && lost.share <= profile.lostShare,
|
||||
lostPresentation:
|
||||
lost.hasSufficientCoverage && lost.share <= profile.lostShare,
|
||||
lateShifts: lateShifts.length === 0,
|
||||
runtimeResources: runtimeResourcePass,
|
||||
requestConcurrency: requestConcurrencyPass,
|
||||
};
|
||||
if (process.env.PORTFOLIO_REFERENCE_LAB === '1') {
|
||||
if (process.env.PORTFOLIO_REFERENCE_LAB === "1") {
|
||||
expect(Object.values(budgetResults).every(Boolean)).toBe(true);
|
||||
}
|
||||
expect(snapshot.navigationBarrier.cancelledEpoch).not.toBeNull();
|
||||
|
||||
const report = {
|
||||
schema: 'drive-trace.v1',
|
||||
evidenceKind: referenceRun === null ? 'current-chromium-developer-machine-non-reference' : 'validated-reference-candidate-unsigned',
|
||||
schema: "drive-trace.v1",
|
||||
evidenceKind:
|
||||
referenceRun === null
|
||||
? "current-chromium-developer-machine-non-reference"
|
||||
: "validated-reference-candidate-unsigned",
|
||||
gatingReferenceResult: false,
|
||||
referenceCandidateValidated: referenceRun !== null,
|
||||
...(referenceRun === null ? {} : { referenceRun: { runId: referenceRun.runId, runIndex: referenceRun.runIndex, attestationDigest: referenceRun.attestationDigest } }),
|
||||
...(referenceRun === null
|
||||
? {}
|
||||
: {
|
||||
referenceRun: {
|
||||
runId: referenceRun.runId,
|
||||
runIndex: referenceRun.runIndex,
|
||||
attestationDigest: referenceRun.attestationDigest,
|
||||
},
|
||||
}),
|
||||
profile: profile.name,
|
||||
tier: profile.tier,
|
||||
browser: testInfo.project.name,
|
||||
metrics: { warmupMedian, lcp, cls, tbt, postRunningLongTaskCount: postRunning.length, eventToNextPaintP95: eventToPaint.p95, sceneCpuP95, continuity, lostPresentationShare: lost.share, lateShiftCount: lateShifts.length },
|
||||
metrics: {
|
||||
warmupMedian,
|
||||
lcp,
|
||||
cls,
|
||||
tbt,
|
||||
postRunningLongTaskCount: postRunning.length,
|
||||
eventToNextPaintP95: eventToPaint.p95,
|
||||
sceneCpuP95,
|
||||
continuity,
|
||||
lostPresentationShare: lost.share,
|
||||
lateShiftCount: lateShifts.length,
|
||||
},
|
||||
runtimeBudgetEvidence,
|
||||
runtimeBudgetSummary: runtimeBudget.tier,
|
||||
budgetResults,
|
||||
referenceBudgetsPass: validReferenceCadence && Object.values(budgetResults).every(Boolean),
|
||||
referenceBudgetsPass:
|
||||
validReferenceCadence && Object.values(budgetResults).every(Boolean),
|
||||
validReferenceCadence,
|
||||
pendingPhysicalReferenceRuns: referenceRun === null ? 5 : 0,
|
||||
};
|
||||
await writeFile(testInfo.outputPath('drive-trace.v1.json'), `${JSON.stringify(report, null, 2)}\n`);
|
||||
await writeFile(
|
||||
testInfo.outputPath("drive-trace.v1.json"),
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -262,14 +262,14 @@ describe("DriveControls", () => {
|
||||
},
|
||||
});
|
||||
|
||||
it("presents one explicit drive action before audio activation", async () => {
|
||||
it("presents one explicit soundtrack action before audio activation", async () => {
|
||||
const wrapper = mountControls(policy());
|
||||
|
||||
expect(wrapper.get('[data-drive-state="idle"]').text()).toContain(
|
||||
"START THE DRIVE",
|
||||
"PLAY SOUNDTRACK",
|
||||
);
|
||||
expect(wrapper.find('[aria-label="Mute sound"]').exists()).toBe(false);
|
||||
await wrapper.get('[aria-label="Start the drive"]').trigger("click");
|
||||
await wrapper.get('[aria-label="Play soundtrack"]').trigger("click");
|
||||
expect(wrapper.emitted("audioStart")).toHaveLength(1);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user