fix: close final portfolio quality gaps
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
import { clickNavigationLink, expect, test } from "../helpers/serve-dist";
|
||||
|
||||
test("forced colors preserves visible focus, controls, content, and the Contact action", async ({
|
||||
page,
|
||||
@@ -45,7 +45,7 @@ test("forced colors preserves visible focus, controls, content, and the Contact
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
await page.locator('nav a[href="#contact"]').click();
|
||||
await clickNavigationLink(page, "#contact");
|
||||
await expect(page.locator("#contact-heading")).toBeFocused();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
expect(
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
import { expect, revealNavigationLinks, test } from "../helpers/serve-dist";
|
||||
|
||||
const navigationHrefs = [
|
||||
"#start",
|
||||
"#profile",
|
||||
"#experience",
|
||||
"#infrastructure",
|
||||
"#selected-builds",
|
||||
"#additional-work",
|
||||
"#toolkit",
|
||||
"#contact",
|
||||
] as const;
|
||||
|
||||
interface FocusGeometry {
|
||||
readonly description: string;
|
||||
@@ -74,6 +85,7 @@ test("every enabled author pointer target is 44 by 44 CSS pixels or a valid inli
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await revealNavigationLinks(page);
|
||||
|
||||
const failures = await page
|
||||
.locator("a[href], button, input, select, textarea")
|
||||
@@ -131,6 +143,10 @@ test("keyboard traversal reaches Contact in document order with visible unobscur
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
const compactNavigation = await page
|
||||
.locator(".site-navigation summary")
|
||||
.isVisible();
|
||||
await revealNavigationLinks(page);
|
||||
|
||||
const visited: string[] = [];
|
||||
for (let index = 0; index < 64; index += 1) {
|
||||
@@ -154,17 +170,12 @@ test("keyboard traversal reaches Contact in document order with visible unobscur
|
||||
if (geometry.description.startsWith("mailto:")) break;
|
||||
}
|
||||
|
||||
expect(visited.at(0)).toBe("#portfolio-content");
|
||||
expect(visited.slice(1, 9)).toEqual([
|
||||
"#start",
|
||||
"#profile",
|
||||
"#experience",
|
||||
"#infrastructure",
|
||||
"#selected-builds",
|
||||
"#additional-work",
|
||||
"#toolkit",
|
||||
"#contact",
|
||||
]);
|
||||
const expectedPrefix = [
|
||||
"#portfolio-content",
|
||||
...(compactNavigation ? ["Navigate"] : []),
|
||||
...navigationHrefs,
|
||||
];
|
||||
expect(visited.slice(0, expectedPrefix.length)).toEqual(expectedPrefix);
|
||||
expect(visited.some((description) => description.startsWith("mailto:"))).toBe(
|
||||
true,
|
||||
);
|
||||
@@ -175,8 +186,14 @@ test("explicit keyboard navigation focuses a section heading without a fixed-sur
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
const compactNavigation = await page
|
||||
.locator(".site-navigation summary")
|
||||
.isVisible();
|
||||
await revealNavigationLinks(page);
|
||||
|
||||
for (let index = 0; index < 9; index += 1) await page.keyboard.press("Tab");
|
||||
const contactTabIndex = compactNavigation ? 10 : 9;
|
||||
for (let index = 0; index < contactTabIndex; index += 1)
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.locator('nav a[href="#contact"]')).toBeFocused();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page.locator("#contact-heading")).toBeFocused();
|
||||
@@ -194,17 +211,31 @@ test("responsive navigation exposes the exact visible label as each link's acces
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.getByRole("link", { name: "Career", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "More work", exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Experience", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Career", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "More work", exact: true }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Experience", exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Additional work", exact: true }),
|
||||
).toHaveCount(0);
|
||||
|
||||
await page.setViewportSize({ width: 320, height: 800 });
|
||||
const career = page.getByRole("link", { name: "Career", exact: true });
|
||||
await expect(career).toHaveText("Career");
|
||||
await expect(page.getByRole("link", { name: "More work", exact: true })).toHaveText("More work");
|
||||
await expect(page.getByRole("link", { name: "Experience", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("link", { name: "Additional work", exact: true })).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("link", { name: "More work", exact: true }),
|
||||
).toHaveText("More work");
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Experience", exact: true }),
|
||||
).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("link", { name: "Additional work", exact: true }),
|
||||
).toHaveCount(0);
|
||||
|
||||
await career.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
import { clickNavigationLink, expect, test } from "../helpers/serve-dist";
|
||||
|
||||
const textSpacingOverride = `
|
||||
* {
|
||||
@@ -62,7 +62,7 @@ async function assertReflowGeometry(page: Page): Promise<void> {
|
||||
expect(result.horizontalFailures).toEqual([]);
|
||||
expect(result.clippedText).toEqual([]);
|
||||
|
||||
await page.locator('nav a[href="#contact"]').click();
|
||||
await clickNavigationLink(page, "#contact");
|
||||
await expect(page.locator("#contact-heading")).toBeFocused();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -1,133 +1,251 @@
|
||||
import { expect, expectInsetFocusIndicator, test } from '../helpers/serve-dist';
|
||||
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 { __audioConstructed: number }).__audioConstructed =
|
||||
0;
|
||||
(window as unknown as { __audioContexts: AudioContext[] }).__audioContexts =
|
||||
[];
|
||||
if (!OriginalAudioContext) return;
|
||||
class TrackingAudioContext extends OriginalAudioContext {
|
||||
constructor(options?: AudioContextOptions) {
|
||||
super(options);
|
||||
const state = window as unknown as { __audioConstructed: number; __audioContexts: AudioContext[] };
|
||||
const state = window as unknown as {
|
||||
__audioConstructed: number;
|
||||
__audioContexts: AudioContext[];
|
||||
};
|
||||
state.__audioConstructed += 1;
|
||||
state.__audioContexts.push(this);
|
||||
}
|
||||
}
|
||||
Object.defineProperty(window, 'AudioContext', { configurable: true, value: TrackingAudioContext });
|
||||
Object.defineProperty(window, "AudioContext", {
|
||||
configurable: true,
|
||||
value: TrackingAudioContext,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('starts Web Audio synchronously from the visitor gesture and exposes accessible controls', async ({ page, distUrl }) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
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('START THE DRIVE')) {
|
||||
const state = window as unknown as { __audioConstructed: number; __constructedDuringGesture: number };
|
||||
(
|
||||
window as unknown as { __constructedDuringGesture?: number }
|
||||
).__constructedDuringGesture = -1;
|
||||
window.addEventListener("click", (event) => {
|
||||
if (
|
||||
(event.target as HTMLElement | null)?.textContent?.includes(
|
||||
"START THE DRIVE",
|
||||
)
|
||||
) {
|
||||
const state = window as unknown as {
|
||||
__audioConstructed: number;
|
||||
__constructedDuringGesture: number;
|
||||
};
|
||||
state.__constructedDuringGesture = state.__audioConstructed;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Start the drive' })).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 expect(
|
||||
page.getByRole("button", { name: "Start the drive" }),
|
||||
).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: 'Start the drive' }).click();
|
||||
await page.getByRole("button", { name: "Start the drive" }).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 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();
|
||||
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: 'Start the drive' }).click();
|
||||
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: "Start the drive" }).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 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' });
|
||||
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(
|
||||
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('main#portfolio-content').click({ position: { x: 8, y: 8 } });
|
||||
await expect(page.getByRole('dialog', { name: 'Drive settings' })).toHaveCount(0);
|
||||
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: 'Start the drive' }).click();
|
||||
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: "Start the drive" }).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: "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 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 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: 'Start the drive' }).click();
|
||||
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: "Start the drive" }).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'));
|
||||
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);
|
||||
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('shows a concise non-blocking message when initial audio start is rejected', async ({ page, distUrl }) => {
|
||||
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')); }
|
||||
override resume(): Promise<void> {
|
||||
return Promise.reject(new DOMException("blocked", "NotAllowedError"));
|
||||
}
|
||||
}
|
||||
Object.defineProperty(window, 'AudioContext', { configurable: true, value: RejectingAudioContext });
|
||||
Object.defineProperty(window, "AudioContext", {
|
||||
configurable: true,
|
||||
value: RejectingAudioContext,
|
||||
});
|
||||
});
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await page.getByRole('button', { name: 'Start the drive' }).click();
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
|
||||
await expect(page.getByText('Audio unavailable. Continue without sound.')).toBeVisible();
|
||||
await expect(page.locator('main#portfolio-content')).toBeVisible();
|
||||
await expect(
|
||||
page.getByText("Audio unavailable. Continue without sound."),
|
||||
).toBeVisible();
|
||||
await expect(page.locator("main#portfolio-content")).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,57 +1,87 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
import {
|
||||
dispatchPersistedPageTransition,
|
||||
installSceneInstrumentation,
|
||||
readSceneInstrumentation,
|
||||
} from '../helpers/scene-instrumentation';
|
||||
} from "../helpers/scene-instrumentation";
|
||||
|
||||
test.beforeEach(async ({ page }) => installSceneInstrumentation(page));
|
||||
|
||||
test('synthetic persisted freeze and restore keeps at most one canvas, RAF loop, and audio context', async ({
|
||||
test("synthetic persisted freeze and restore keeps at most one canvas, RAF loop, and audio context", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
|
||||
await page.getByRole('button', { name: 'START THE DRIVE' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Mute sound' })).toBeVisible();
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-state",
|
||||
"running",
|
||||
);
|
||||
await page.getByRole("button", { name: "START THE DRIVE" }).click();
|
||||
await expect(page.getByRole("button", { name: "Mute sound" })).toBeVisible();
|
||||
|
||||
for (let cycle = 0; cycle < 3; cycle += 1) {
|
||||
await dispatchPersistedPageTransition(page, 'pagehide');
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).activeAnimationFrames).toBe(0);
|
||||
await dispatchPersistedPageTransition(page, 'pageshow');
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).activeAnimationFrames).toBeLessThanOrEqual(1);
|
||||
await dispatchPersistedPageTransition(page, "pagehide");
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await readSceneInstrumentation(page)).activeAnimationFrames,
|
||||
)
|
||||
.toBe(0);
|
||||
await dispatchPersistedPageTransition(page, "pageshow");
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-state",
|
||||
"running",
|
||||
);
|
||||
await expect
|
||||
.poll(
|
||||
async () =>
|
||||
(await readSceneInstrumentation(page)).activeAnimationFrames,
|
||||
)
|
||||
.toBeLessThanOrEqual(1);
|
||||
}
|
||||
|
||||
const state = await readSceneInstrumentation(page);
|
||||
expect(state.maximumCanvasCount).toBe(1);
|
||||
expect(await page.locator('.journey-canvas').count()).toBe(1);
|
||||
expect(await page.locator(".journey-canvas").count()).toBe(1);
|
||||
expect(state.audioContextsConstructed).toBe(1);
|
||||
expect(state.liveAudioContexts).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test('persisted lifecycle events preserve paused and Static states without duplicate resources', async ({
|
||||
test("persisted lifecycle events preserve paused and Static states without duplicate resources", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-state",
|
||||
"running",
|
||||
);
|
||||
|
||||
await page.getByRole('button', { name: 'Pause motion' }).click();
|
||||
await dispatchPersistedPageTransition(page, 'pagehide');
|
||||
await dispatchPersistedPageTransition(page, 'pageshow');
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(1);
|
||||
await expect(page.getByRole('button', { name: 'Resume motion' })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Start the drive" }).click();
|
||||
await page.getByRole("button", { name: "Pause motion" }).click();
|
||||
await dispatchPersistedPageTransition(page, "pagehide");
|
||||
await dispatchPersistedPageTransition(page, "pageshow");
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(1);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Resume motion" }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Quality control').selectOption('static');
|
||||
await dispatchPersistedPageTransition(page, 'pagehide');
|
||||
await dispatchPersistedPageTransition(page, 'pageshow');
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await expect(page.getByRole('button', { name: 'Enable motion' })).toBeVisible();
|
||||
await page.getByRole("button", { name: "Drive settings" }).click();
|
||||
await page.getByLabel("Quality control").selectOption("static");
|
||||
await dispatchPersistedPageTransition(page, "pagehide");
|
||||
await dispatchPersistedPageTransition(page, "pageshow");
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Enable motion" }),
|
||||
).toBeVisible();
|
||||
|
||||
const state = await readSceneInstrumentation(page);
|
||||
expect(state.maximumCanvasCount).toBeLessThanOrEqual(1);
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).activeAnimationFrames).toBe(0);
|
||||
expect(state.liveAudioContexts).toBe(0);
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await readSceneInstrumentation(page)).activeAnimationFrames,
|
||||
)
|
||||
.toBe(0);
|
||||
expect(state.audioContextsConstructed).toBe(1);
|
||||
expect(state.liveAudioContexts).toBe(1);
|
||||
});
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { expect, expectInsetFocusIndicator, test } from '../helpers/serve-dist';
|
||||
import { expect, expectInsetFocusIndicator, test } from "../helpers/serve-dist";
|
||||
|
||||
const navHrefs = [
|
||||
'#start',
|
||||
'#profile',
|
||||
'#experience',
|
||||
'#infrastructure',
|
||||
'#selected-builds',
|
||||
'#additional-work',
|
||||
'#toolkit',
|
||||
'#contact',
|
||||
"#start",
|
||||
"#profile",
|
||||
"#experience",
|
||||
"#infrastructure",
|
||||
"#selected-builds",
|
||||
"#additional-work",
|
||||
"#toolkit",
|
||||
"#contact",
|
||||
] as const;
|
||||
|
||||
const assertVisibleUnobscuredFocus = async (
|
||||
page: import('@playwright/test').Page,
|
||||
page: import("@playwright/test").Page,
|
||||
): Promise<void> => {
|
||||
await expect
|
||||
.poll(() =>
|
||||
@@ -34,7 +34,7 @@ const assertVisibleUnobscuredFocus = async (
|
||||
(active === topElement ||
|
||||
active.contains(topElement) ||
|
||||
topElement.contains(active)) &&
|
||||
style.outlineStyle !== 'none' &&
|
||||
style.outlineStyle !== "none" &&
|
||||
Number.parseFloat(style.outlineWidth) >= 2
|
||||
);
|
||||
}),
|
||||
@@ -42,119 +42,142 @@ const assertVisibleUnobscuredFocus = async (
|
||||
.toBe(true);
|
||||
};
|
||||
|
||||
test('tabs from the skip link through navigation and every contact action', async ({
|
||||
test("tabs from the skip link through navigation and every contact action", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name === "mobile-chromium",
|
||||
"The compact disclosure path is covered by the adaptive keyboard traversal suite.",
|
||||
);
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
const expectedTargets = [
|
||||
{ selector: '.skip-link', href: '#portfolio-content' },
|
||||
{ selector: ".skip-link", href: "#portfolio-content" },
|
||||
...navHrefs.map((href) => ({ selector: `nav a[href="${href}"]`, href })),
|
||||
{ selector: ".hero-journey__action", href: "#selected-builds" },
|
||||
{
|
||||
selector: 'a[href="https://github.com/LucaPisl/footy-tracker"]',
|
||||
href: "https://github.com/LucaPisl/footy-tracker",
|
||||
},
|
||||
{
|
||||
selector:
|
||||
'a[href="https://github.com/LucaPisl/opinion-dynamics-simulation"]',
|
||||
href: "https://github.com/LucaPisl/opinion-dynamics-simulation",
|
||||
},
|
||||
{
|
||||
selector:
|
||||
'a[href="https://github.com/LucaPisl/CurrencyConverterAvalonia"]',
|
||||
href: "https://github.com/LucaPisl/CurrencyConverterAvalonia",
|
||||
},
|
||||
{
|
||||
selector: 'a[href="https://github.com/LucaPisl/LinuxModdingGuide"]',
|
||||
href: "https://github.com/LucaPisl/LinuxModdingGuide",
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href^="mailto:"]',
|
||||
href: 'mailto:luca.pislaru@gmail.com',
|
||||
href: "mailto:luca.pislaru@gmail.com",
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href="https://github.com/LucaPisl"]',
|
||||
href: 'https://github.com/LucaPisl',
|
||||
href: "https://github.com/LucaPisl",
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href="https://gitlab.com/luca_pisl"]',
|
||||
href: 'https://gitlab.com/luca_pisl',
|
||||
href: "https://gitlab.com/luca_pisl",
|
||||
},
|
||||
{
|
||||
selector: '#contact a[href="https://linkedin.com/in/luca-pisl"]',
|
||||
href: 'https://linkedin.com/in/luca-pisl',
|
||||
href: "https://linkedin.com/in/luca-pisl",
|
||||
},
|
||||
];
|
||||
|
||||
for (const target of expectedTargets) {
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press("Tab");
|
||||
const locator = page.locator(target.selector);
|
||||
await expect(locator).toBeFocused();
|
||||
await expect(locator).toHaveAttribute('href', target.href);
|
||||
await expect(locator).toHaveAttribute("href", target.href);
|
||||
await assertVisibleUnobscuredFocus(page);
|
||||
}
|
||||
});
|
||||
|
||||
test('activating the skip link focuses the visible unobscured portfolio content', async ({
|
||||
test("activating the skip link focuses the visible unobscured portfolio content", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.locator('.skip-link')).toBeFocused();
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.locator(".skip-link")).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
await expect(page).toHaveURL(`${distUrl}/#portfolio-content`);
|
||||
await expect(page.locator('#portfolio-content')).toBeFocused();
|
||||
await expect(page.locator('#portfolio-content')).toHaveAttribute(
|
||||
'tabindex',
|
||||
'-1',
|
||||
await expect(page.locator("#portfolio-content")).toBeFocused();
|
||||
await expect(page.locator("#portfolio-content")).toHaveAttribute(
|
||||
"tabindex",
|
||||
"-1",
|
||||
);
|
||||
await expectInsetFocusIndicator(page, '#portfolio-content');
|
||||
await expectInsetFocusIndicator(page, "#portfolio-content");
|
||||
});
|
||||
|
||||
test('explicit section activation scrolls to and focuses its heading', async ({
|
||||
test("explicit section activation scrolls to and focuses its heading", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name === 'mobile-chromium',
|
||||
'The full keyboard path already covers the mobile project.',
|
||||
testInfo.project.name === "mobile-chromium",
|
||||
"The full keyboard path already covers the mobile project.",
|
||||
);
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
for (let index = 0; index < 5; index += 1) await page.keyboard.press('Tab');
|
||||
for (let index = 0; index < 5; index += 1) await page.keyboard.press("Tab");
|
||||
await expect(page.locator('nav a[href="#infrastructure"]')).toBeFocused();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
await expect(page).toHaveURL(`${distUrl}/#infrastructure`);
|
||||
await expect(page.locator('#infrastructure')).toBeInViewport();
|
||||
await expect(page.locator('#infrastructure-heading')).toHaveAttribute(
|
||||
'tabindex',
|
||||
'-1',
|
||||
await expect(page.locator("#infrastructure")).toBeInViewport();
|
||||
await expect(page.locator("#infrastructure-heading")).toHaveAttribute(
|
||||
"tabindex",
|
||||
"-1",
|
||||
);
|
||||
await expect(page.locator('#infrastructure-heading')).toBeFocused();
|
||||
await expect(page.locator("#infrastructure-heading")).toBeFocused();
|
||||
await assertVisibleUnobscuredFocus(page);
|
||||
});
|
||||
|
||||
test('manual wheel and PageDown scrolling never move focus', async ({
|
||||
test("manual wheel and PageDown scrolling never move focus", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name === 'mobile-chromium',
|
||||
'The focus invariant is engine-level rather than viewport-specific.',
|
||||
testInfo.project.name === "mobile-chromium",
|
||||
"The focus invariant is engine-level rather than viewport-specific.",
|
||||
);
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.keyboard.press('Tab');
|
||||
await page.keyboard.press('Tab');
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Tab");
|
||||
const startLink = page.locator('nav a[href="#start"]');
|
||||
await expect(startLink).toBeFocused();
|
||||
|
||||
await page.mouse.wheel(0, 900);
|
||||
await expect(startLink).toBeFocused();
|
||||
await page.keyboard.press('PageDown');
|
||||
await page.keyboard.press("PageDown");
|
||||
await expect(startLink).toBeFocused();
|
||||
});
|
||||
|
||||
test('direct fragments do not steal focus while reduced-motion navigation remains accessible', async ({
|
||||
test("direct fragments do not steal focus while reduced-motion navigation remains accessible", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name === 'mobile-chromium',
|
||||
'The full keyboard path already covers the mobile project.',
|
||||
testInfo.project.name === "mobile-chromium",
|
||||
"The full keyboard path already covers the mobile project.",
|
||||
);
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(`${distUrl}/#toolkit`, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('#toolkit-heading')).not.toBeFocused();
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await page.goto(`${distUrl}/#toolkit`, { waitUntil: "domcontentloaded" });
|
||||
await expect(page.locator("#toolkit-heading")).not.toBeFocused();
|
||||
|
||||
await page.locator('nav a[href="#contact"]').click();
|
||||
await expect(page).toHaveURL(`${distUrl}/#contact`);
|
||||
await expect(page.locator('#contact-heading')).toBeFocused();
|
||||
await expect(page.locator('#contact')).toBeInViewport();
|
||||
await expect(page.locator("#contact-heading")).toBeFocused();
|
||||
await expect(page.locator("#contact")).toBeInViewport();
|
||||
});
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { expect, test } from "../helpers/serve-dist";
|
||||
import {
|
||||
clickNavigationLink,
|
||||
expect,
|
||||
revealNavigationLinks,
|
||||
test,
|
||||
} from "../helpers/serve-dist";
|
||||
|
||||
test("keeps canonical hrefs while creating exactly one or zero explicit history entries", async ({
|
||||
page,
|
||||
@@ -6,20 +11,18 @@ test("keeps canonical hrefs while creating exactly one or zero explicit history
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
const initialLength = await page.evaluate(() => history.length);
|
||||
const start = page.locator('nav a[href="#start"]');
|
||||
const profile = page.locator('nav a[href="#profile"]');
|
||||
|
||||
await start.click();
|
||||
await clickNavigationLink(page, "#start");
|
||||
await expect(page).toHaveURL(`${distUrl}/`);
|
||||
expect(await page.evaluate(() => history.length)).toBe(initialLength);
|
||||
await expect(page.locator("#start-heading")).toBeFocused();
|
||||
|
||||
await profile.click();
|
||||
await clickNavigationLink(page, "#profile");
|
||||
await expect(page).toHaveURL(`${distUrl}/#profile`);
|
||||
expect(await page.evaluate(() => history.length)).toBe(initialLength + 1);
|
||||
await expect(page.locator("#profile-heading")).toBeFocused();
|
||||
|
||||
await profile.click();
|
||||
await clickNavigationLink(page, "#profile");
|
||||
expect(await page.evaluate(() => history.length)).toBe(initialLength + 1);
|
||||
await expect(page.locator("#profile-heading")).toBeFocused();
|
||||
});
|
||||
@@ -27,7 +30,11 @@ test("keeps canonical hrefs while creating exactly one or zero explicit history
|
||||
test("cancels pending explicit focus when wheel intent intervenes", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
}, testInfo) => {
|
||||
test.skip(
|
||||
testInfo.project.name === "mobile-chromium",
|
||||
"Hidden-focus cancellation is covered once in desktop Chromium.",
|
||||
);
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
const link = page.locator('nav a[href="#infrastructure"]');
|
||||
await link.focus();
|
||||
@@ -56,6 +63,7 @@ test("passive scrolling replaces a stable fragment without stealing focus", asyn
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await revealNavigationLinks(page);
|
||||
const link = page.locator('nav a[href="#start"]');
|
||||
await link.focus();
|
||||
|
||||
@@ -80,8 +88,8 @@ test("back and forward restore sections without another history mutation or focu
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
await page.locator('nav a[href="#profile"]').click();
|
||||
await page.locator('nav a[href="#experience"]').click();
|
||||
await clickNavigationLink(page, "#profile");
|
||||
await clickNavigationLink(page, "#experience");
|
||||
const historyLength = await page.evaluate(() => history.length);
|
||||
await expect(page.locator("#experience-heading")).toBeFocused();
|
||||
|
||||
|
||||
@@ -1,49 +1,50 @@
|
||||
import type { PortfolioContent } from '@/content/types';
|
||||
import { expect, expectInsetFocusIndicator, test } from '../helpers/serve-dist';
|
||||
import type { PortfolioContent } from "@/content/types";
|
||||
import { expect, expectInsetFocusIndicator, test } from "../helpers/serve-dist";
|
||||
|
||||
test.use({ javaScriptEnabled: false });
|
||||
|
||||
const sectionIds = [
|
||||
'start',
|
||||
'profile',
|
||||
'experience',
|
||||
'infrastructure',
|
||||
'selected-builds',
|
||||
'additional-work',
|
||||
'toolkit',
|
||||
'contact',
|
||||
"start",
|
||||
"profile",
|
||||
"experience",
|
||||
"infrastructure",
|
||||
"selected-builds",
|
||||
"additional-work",
|
||||
"toolkit",
|
||||
"contact",
|
||||
] as const;
|
||||
|
||||
const sectionHeadings = [
|
||||
'Profile',
|
||||
'Experience',
|
||||
'Infrastructure',
|
||||
'Selected builds',
|
||||
'Additional work',
|
||||
'Toolkit',
|
||||
'Contact',
|
||||
"Profile",
|
||||
"Experience",
|
||||
"Infrastructure",
|
||||
"Selected builds",
|
||||
"Additional work",
|
||||
"Toolkit",
|
||||
"Contact",
|
||||
] as const;
|
||||
|
||||
const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> = {
|
||||
start: 'Start',
|
||||
profile: 'Profile',
|
||||
experience: 'Career',
|
||||
infrastructure: 'Infra',
|
||||
'selected-builds': 'Builds',
|
||||
'additional-work': 'More work',
|
||||
toolkit: 'Toolkit',
|
||||
contact: 'Contact',
|
||||
};
|
||||
const navigationLabels: Readonly<Record<(typeof sectionIds)[number], string>> =
|
||||
{
|
||||
start: "Start",
|
||||
profile: "Profile",
|
||||
experience: "Career",
|
||||
infrastructure: "Infra",
|
||||
"selected-builds": "Builds",
|
||||
"additional-work": "More work",
|
||||
toolkit: "Toolkit",
|
||||
contact: "Contact",
|
||||
};
|
||||
|
||||
const visibleTextLeafSelector = 'h1,h2,h3,p,dt,dd,li,figcaption,a,svg text';
|
||||
const visibleTextLeafSelector = "h1,h2,h3,p,dt,dd,li,figcaption,a,svg text";
|
||||
const visibleTextSelector = [
|
||||
'.site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)',
|
||||
'.portfolio-main :is(h1,h2,h3,p,dt,dd,li,figcaption,a)',
|
||||
'.portfolio-main svg text',
|
||||
].join(',');
|
||||
".site-header :is(h1,h2,h3,p,dt,dd,li,figcaption,a)",
|
||||
".portfolio-main :is(h1,h2,h3,p,dt,dd,li,figcaption,a)",
|
||||
".portfolio-main svg text",
|
||||
].join(",");
|
||||
|
||||
const normalizedText = (value: string): string =>
|
||||
value.replace(/\s+/gu, ' ').trim();
|
||||
value.replace(/\s+/gu, " ").trim();
|
||||
|
||||
type TextMultiset = Array<[text: string, count: number]>;
|
||||
|
||||
@@ -69,14 +70,14 @@ const missingText = (
|
||||
};
|
||||
|
||||
const portfolioContentFromSnapshot = async (
|
||||
page: import('@playwright/test').Page,
|
||||
page: import("@playwright/test").Page,
|
||||
rawHtml: string,
|
||||
): Promise<PortfolioContent> =>
|
||||
page.evaluate((html) => {
|
||||
const document = new DOMParser().parseFromString(html, 'text/html');
|
||||
const snapshot = document.querySelector('#portfolio-inputs');
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
const snapshot = document.querySelector("#portfolio-inputs");
|
||||
if (snapshot?.textContent === null || snapshot?.textContent === undefined) {
|
||||
throw new Error('Missing #portfolio-inputs snapshot');
|
||||
throw new Error("Missing #portfolio-inputs snapshot");
|
||||
}
|
||||
return (JSON.parse(snapshot.textContent) as { content: PortfolioContent })
|
||||
.content;
|
||||
@@ -89,7 +90,7 @@ const expectedVisibleTextSegments = (
|
||||
content.navigation.map((item) => [item.id, item.label]),
|
||||
);
|
||||
const section = (
|
||||
id: Exclude<(typeof sectionIds)[number], 'start'>,
|
||||
id: Exclude<(typeof sectionIds)[number], "start">,
|
||||
body: readonly string[],
|
||||
eyebrow?: string,
|
||||
): string[] => {
|
||||
@@ -100,60 +101,65 @@ const expectedVisibleTextSegments = (
|
||||
);
|
||||
};
|
||||
const projectLinkText = (
|
||||
project: PortfolioContent['projects']['selected'][number],
|
||||
project: PortfolioContent["projects"]["selected"][number],
|
||||
): string[] =>
|
||||
project.visibility === 'public' && project.url ? [project.title] : [];
|
||||
project.visibility === "public" && project.url ? [project.title] : [];
|
||||
|
||||
return [
|
||||
...sectionIds.map((id) => navigationLabels[id]),
|
||||
content.identity.name,
|
||||
content.identity.role,
|
||||
content.identity.valueProposition,
|
||||
'Explore work',
|
||||
...section('profile', [
|
||||
"Explore work",
|
||||
...section("profile", [
|
||||
content.profile.introduction,
|
||||
...content.profile.screeningFacts.flatMap((fact) => [
|
||||
fact.label,
|
||||
fact.value,
|
||||
]),
|
||||
]),
|
||||
...section('experience', [
|
||||
...section("experience", [
|
||||
...content.experience.flatMap((record) => [
|
||||
record.role,
|
||||
`${record.organization} · ${record.dates}${record.location === undefined ? '' : ` · ${record.location}`}`,
|
||||
`${record.organization} · ${record.dates}${record.location === undefined ? "" : ` · ${record.location}`}`,
|
||||
...record.highlights,
|
||||
]),
|
||||
content.systemsOperations.title,
|
||||
content.systemsOperations.summary,
|
||||
...content.systemsOperations.evidence,
|
||||
]),
|
||||
...section('infrastructure', [
|
||||
...section("infrastructure", [
|
||||
content.infrastructure.title,
|
||||
content.infrastructure.summary,
|
||||
content.infrastructure.title,
|
||||
"Trust boundary",
|
||||
"Where trust or access rules change",
|
||||
"Request flow",
|
||||
"The direction traffic moves through the system",
|
||||
"Legend: Boundary labels mark changes in trust or access rules. Connectors show request flow direction.",
|
||||
...content.infrastructure.bands.map(
|
||||
(band) => `${band.title}: ${band.summary}`,
|
||||
),
|
||||
]),
|
||||
...section(
|
||||
'selected-builds',
|
||||
"selected-builds",
|
||||
content.projects.selected.flatMap((project) => [
|
||||
project.title,
|
||||
project.summary,
|
||||
'Problem',
|
||||
"Problem",
|
||||
project.problem,
|
||||
'Contribution',
|
||||
"Contribution",
|
||||
project.contribution,
|
||||
'Technical decisions',
|
||||
"Technical decisions",
|
||||
...project.technicalDecisions,
|
||||
'Outcome',
|
||||
"Outcome",
|
||||
project.outcome,
|
||||
...projectLinkText(project),
|
||||
]),
|
||||
'Selected work',
|
||||
"Selected work",
|
||||
),
|
||||
...section(
|
||||
'additional-work',
|
||||
"additional-work",
|
||||
content.projects.additional.flatMap((project) => [
|
||||
project.title,
|
||||
project.summary,
|
||||
@@ -163,29 +169,33 @@ const expectedVisibleTextSegments = (
|
||||
]),
|
||||
),
|
||||
...section(
|
||||
'toolkit',
|
||||
"toolkit",
|
||||
content.toolkit.flatMap((group) => [
|
||||
group.title,
|
||||
...group.technologies.map((technology) => technology.name),
|
||||
]),
|
||||
),
|
||||
...section('contact', [
|
||||
"Thanks for making the trip. The road's open if you'd like to build something together.",
|
||||
'Start a conversation',
|
||||
content.contact.email,
|
||||
content.contact.cvMessage,
|
||||
...content.contact.social.map((link) => link.service),
|
||||
], 'Destination'),
|
||||
...section(
|
||||
"contact",
|
||||
[
|
||||
"Thanks for making the trip. The road's open if you'd like to build something together.",
|
||||
"Start a conversation",
|
||||
content.contact.email,
|
||||
content.contact.cvMessage,
|
||||
...content.contact.social.map((link) => link.service),
|
||||
],
|
||||
"Destination",
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
const rawVisibleTextSegments = async (
|
||||
page: import('@playwright/test').Page,
|
||||
page: import("@playwright/test").Page,
|
||||
rawHtml: string,
|
||||
): Promise<string[]> =>
|
||||
page.evaluate(
|
||||
({ html, leafSelector, rootSelector }) => {
|
||||
const document = new DOMParser().parseFromString(html, 'text/html');
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
const isHidden = (element: Element): boolean => {
|
||||
for (
|
||||
let current: Element | null = element;
|
||||
@@ -193,21 +203,21 @@ const rawVisibleTextSegments = async (
|
||||
current = current.parentElement
|
||||
) {
|
||||
if (
|
||||
current.matches('template,script,style,noscript') ||
|
||||
current.hasAttribute('hidden') ||
|
||||
current.hasAttribute('inert')
|
||||
current.matches("template,script,style,noscript") ||
|
||||
current.hasAttribute("hidden") ||
|
||||
current.hasAttribute("inert")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const ariaHidden = current.getAttribute('aria-hidden');
|
||||
const ariaHidden = current.getAttribute("aria-hidden");
|
||||
if (
|
||||
ariaHidden !== null &&
|
||||
ariaHidden.trim().toLowerCase() !== 'false'
|
||||
ariaHidden.trim().toLowerCase() !== "false"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const hiddenClass =
|
||||
current.getAttribute('class')?.toLowerCase() ?? '';
|
||||
current.getAttribute("class")?.toLowerCase() ?? "";
|
||||
if (
|
||||
/(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test(
|
||||
hiddenClass,
|
||||
@@ -216,7 +226,7 @@ const rawVisibleTextSegments = async (
|
||||
return true;
|
||||
}
|
||||
const inlineStyle =
|
||||
current.getAttribute('style')?.toLowerCase() ?? '';
|
||||
current.getAttribute("style")?.toLowerCase() ?? "";
|
||||
if (
|
||||
/(?:^|;)\s*display\s*:\s*none\b/u.test(inlineStyle) ||
|
||||
/(?:^|;)\s*visibility\s*:\s*hidden\b/u.test(inlineStyle) ||
|
||||
@@ -233,7 +243,7 @@ const rawVisibleTextSegments = async (
|
||||
!isHidden(element) && element.querySelector(leafSelector) === null,
|
||||
)
|
||||
.map((element) =>
|
||||
(element.textContent ?? '').replace(/\s+/gu, ' ').trim(),
|
||||
(element.textContent ?? "").replace(/\s+/gu, " ").trim(),
|
||||
)
|
||||
.filter((text) => text.length > 0);
|
||||
},
|
||||
@@ -245,7 +255,7 @@ const rawVisibleTextSegments = async (
|
||||
);
|
||||
|
||||
const browserVisibleTextSegments = async (
|
||||
page: import('@playwright/test').Page,
|
||||
page: import("@playwright/test").Page,
|
||||
): Promise<string[]> =>
|
||||
page.locator(visibleTextSelector).evaluateAll(
|
||||
(elements, leafSelector) =>
|
||||
@@ -257,21 +267,21 @@ const browserVisibleTextSegments = async (
|
||||
current = current.parentElement
|
||||
) {
|
||||
if (
|
||||
current.matches('template,script,style,noscript') ||
|
||||
current.hasAttribute('hidden') ||
|
||||
current.hasAttribute('inert')
|
||||
current.matches("template,script,style,noscript") ||
|
||||
current.hasAttribute("hidden") ||
|
||||
current.hasAttribute("inert")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const ariaHidden = current.getAttribute('aria-hidden');
|
||||
const ariaHidden = current.getAttribute("aria-hidden");
|
||||
if (
|
||||
ariaHidden !== null &&
|
||||
ariaHidden.trim().toLowerCase() !== 'false'
|
||||
ariaHidden.trim().toLowerCase() !== "false"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
const hiddenClass =
|
||||
current.getAttribute('class')?.toLowerCase() ?? '';
|
||||
current.getAttribute("class")?.toLowerCase() ?? "";
|
||||
if (
|
||||
/(?:^|\s|-)print-only(?:\s|-|$)|print-portfolio-label|visually-hidden|sr-only|screen-reader-only/u.test(
|
||||
hiddenClass,
|
||||
@@ -281,9 +291,9 @@ const browserVisibleTextSegments = async (
|
||||
}
|
||||
const style = getComputedStyle(current);
|
||||
if (
|
||||
style.display === 'none' ||
|
||||
style.visibility === 'hidden' ||
|
||||
style.opacity === '0'
|
||||
style.display === "none" ||
|
||||
style.visibility === "hidden" ||
|
||||
style.opacity === "0"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
@@ -294,13 +304,13 @@ const browserVisibleTextSegments = async (
|
||||
);
|
||||
})
|
||||
.map((element) =>
|
||||
(element.textContent ?? '').replace(/\s+/gu, ' ').trim(),
|
||||
(element.textContent ?? "").replace(/\s+/gu, " ").trim(),
|
||||
)
|
||||
.filter((text) => text.length > 0),
|
||||
visibleTextLeafSelector,
|
||||
);
|
||||
|
||||
test('keeps every raw SSR reading-path segment visible without JavaScript', async ({
|
||||
test("keeps every raw SSR reading-path segment visible without JavaScript", async ({
|
||||
page,
|
||||
distUrl,
|
||||
request,
|
||||
@@ -308,14 +318,14 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
|
||||
const pageErrors: string[] = [];
|
||||
const consoleErrors: string[] = [];
|
||||
const failedRequests: string[] = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') consoleErrors.push(message.text());
|
||||
page.on("pageerror", (error) => pageErrors.push(error.message));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") consoleErrors.push(message.text());
|
||||
});
|
||||
page.on('requestfailed', (request) => {
|
||||
if (request.resourceType() !== 'script') {
|
||||
page.on("requestfailed", (request) => {
|
||||
if (request.resourceType() !== "script") {
|
||||
failedRequests.push(
|
||||
`${request.url()}: ${request.failure()?.errorText ?? 'failed'}`,
|
||||
`${request.url()}: ${request.failure()?.errorText ?? "failed"}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -331,7 +341,7 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
|
||||
await rawVisibleTextSegments(page, rawHtml),
|
||||
);
|
||||
|
||||
await page.goto(distUrl, { waitUntil: 'load' });
|
||||
await page.goto(distUrl, { waitUntil: "load" });
|
||||
const actualVisibleText = textMultiset(
|
||||
await browserVisibleTextSegments(page),
|
||||
);
|
||||
@@ -340,37 +350,37 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
|
||||
expect(rawVisibleText).toEqual(expectedVisibleText);
|
||||
expect(actualVisibleText).toEqual(expectedVisibleText);
|
||||
expect(normalizedText(JSON.stringify(actualVisibleText))).not.toContain(
|
||||
'preview-semantic-v1',
|
||||
"preview-semantic-v1",
|
||||
);
|
||||
|
||||
await expect(page.locator('h1:visible')).toHaveCount(1);
|
||||
await expect(page.locator('h1')).toHaveText('Luca Pîslaru');
|
||||
await expect(page.locator('.hero-journey__role')).toHaveText(
|
||||
'System Administrator & Full-Stack Developer',
|
||||
await expect(page.locator("h1:visible")).toHaveCount(1);
|
||||
await expect(page.locator("h1")).toHaveText("Luca Pîslaru");
|
||||
await expect(page.locator(".hero-journey__role")).toHaveText(
|
||||
"System Administrator & Full-Stack Developer",
|
||||
);
|
||||
await expect(page.locator('main')).toBeVisible();
|
||||
await expect(page.locator("main")).toBeVisible();
|
||||
await expect(
|
||||
page.locator('nav[aria-label="Portfolio sections"]'),
|
||||
).toBeVisible();
|
||||
await expect(page.locator('footer')).toBeVisible();
|
||||
await expect(page.locator("footer")).toBeVisible();
|
||||
expect(
|
||||
await page
|
||||
.locator('main > section')
|
||||
.locator("main > section")
|
||||
.evaluateAll((sections) => sections.map((section) => section.id)),
|
||||
).toEqual(sectionIds);
|
||||
expect(await page.locator('main > section > h2').allTextContents()).toEqual(
|
||||
expect(await page.locator("main > section > h2").allTextContents()).toEqual(
|
||||
sectionHeadings,
|
||||
);
|
||||
|
||||
for (const evidence of [
|
||||
'Professional systems operations',
|
||||
'Secure self-hosted platform and lifecycle',
|
||||
'Private self-hosted medication tracker',
|
||||
'Footy Tracker',
|
||||
'Linux',
|
||||
'Docker',
|
||||
'Python',
|
||||
'Vue.js',
|
||||
"Professional systems operations",
|
||||
"Secure self-hosted platform and lifecycle",
|
||||
"Private self-hosted medication tracker",
|
||||
"Footy Tracker",
|
||||
"Linux",
|
||||
"Docker",
|
||||
"Python",
|
||||
"Vue.js",
|
||||
]) {
|
||||
await expect(
|
||||
page.getByText(evidence, { exact: true }).first(),
|
||||
@@ -379,30 +389,30 @@ test('keeps every raw SSR reading-path segment visible without JavaScript', asyn
|
||||
|
||||
const email = page.locator('a[href="mailto:luca.pislaru@gmail.com"]');
|
||||
await expect(email).toBeVisible();
|
||||
await expect(email).toHaveText('Start a conversation');
|
||||
await expect(email).toHaveText("Start a conversation");
|
||||
await expect(
|
||||
page.getByText(
|
||||
'For a current CV or role-specific information, contact me directly.',
|
||||
"For a current CV or role-specific information, contact me directly.",
|
||||
{ exact: true },
|
||||
),
|
||||
).toBeVisible();
|
||||
|
||||
for (const title of [
|
||||
'Private self-hosted medication tracker',
|
||||
'Private healthcare platform',
|
||||
'Private game prototype',
|
||||
"Private self-hosted medication tracker",
|
||||
"Private healthcare platform",
|
||||
"Private game prototype",
|
||||
]) {
|
||||
await expect(
|
||||
page.locator('article').filter({ hasText: title }).locator('a'),
|
||||
page.locator("article").filter({ hasText: title }).locator("a"),
|
||||
).toHaveCount(0);
|
||||
}
|
||||
await expect(page.locator('a[download]')).toHaveCount(0);
|
||||
await expect(page.locator("a[download]")).toHaveCount(0);
|
||||
expect(pageErrors).toEqual([]);
|
||||
expect(consoleErrors).toEqual([]);
|
||||
expect(failedRequests).toEqual([]);
|
||||
});
|
||||
|
||||
test('content oracle detects a removed highlight and an inert reading path', async ({
|
||||
test("content oracle detects a removed highlight and an inert reading path", async ({
|
||||
page,
|
||||
distUrl,
|
||||
request,
|
||||
@@ -417,31 +427,31 @@ test('content oracle detects a removed highlight and an inert reading path', asy
|
||||
|
||||
const withoutHighlight = await page.evaluate(
|
||||
({ html, highlight }) => {
|
||||
const document = new DOMParser().parseFromString(html, 'text/html');
|
||||
const target = [...document.querySelectorAll('li')].find(
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
const target = [...document.querySelectorAll("li")].find(
|
||||
(element) =>
|
||||
(element.textContent ?? '').replace(/\s+/gu, ' ').trim() ===
|
||||
(element.textContent ?? "").replace(/\s+/gu, " ").trim() ===
|
||||
highlight,
|
||||
);
|
||||
if (target === undefined) throw new Error('Expected highlight is absent');
|
||||
if (target === undefined) throw new Error("Expected highlight is absent");
|
||||
target.remove();
|
||||
return `<!doctype html>${document.documentElement.outerHTML}`;
|
||||
},
|
||||
{ html: rawHtml, highlight: firstHighlight ?? '' },
|
||||
{ html: rawHtml, highlight: firstHighlight ?? "" },
|
||||
);
|
||||
const withoutHighlightText = textMultiset(
|
||||
await rawVisibleTextSegments(page, withoutHighlight),
|
||||
);
|
||||
expect(withoutHighlightText).not.toEqual(expected);
|
||||
const missingHighlight = missingText(expected, withoutHighlightText);
|
||||
expect(missingHighlight).toEqual([[normalizedText(firstHighlight ?? ''), 1]]);
|
||||
expect(missingHighlight).toEqual([[normalizedText(firstHighlight ?? ""), 1]]);
|
||||
expect(missingText(withoutHighlightText, expected)).toEqual([]);
|
||||
|
||||
const inertMain = await page.evaluate((html) => {
|
||||
const document = new DOMParser().parseFromString(html, 'text/html');
|
||||
const main = document.querySelector('main');
|
||||
if (main === null) throw new Error('Expected main is absent');
|
||||
main.setAttribute('inert', '');
|
||||
const document = new DOMParser().parseFromString(html, "text/html");
|
||||
const main = document.querySelector("main");
|
||||
if (main === null) throw new Error("Expected main is absent");
|
||||
main.setAttribute("inert", "");
|
||||
return `<!doctype html>${document.documentElement.outerHTML}`;
|
||||
}, rawHtml);
|
||||
const inertMainText = textMultiset(
|
||||
@@ -457,33 +467,33 @@ test('content oracle detects a removed highlight and an inert reading path', asy
|
||||
expect(missingText(inertMainText, expected)).toEqual([]);
|
||||
});
|
||||
|
||||
test('activating the skip link focuses visible portfolio content without JavaScript', async ({
|
||||
test("activating the skip link focuses visible portfolio content without JavaScript", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'load' });
|
||||
await page.keyboard.press('Tab');
|
||||
await expect(page.locator('.skip-link')).toBeFocused();
|
||||
await page.goto(distUrl, { waitUntil: "load" });
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.locator(".skip-link")).toBeFocused();
|
||||
|
||||
await page.keyboard.press('Enter');
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
await expect(page).toHaveURL(`${distUrl}/#portfolio-content`);
|
||||
await expect(page.locator('#portfolio-content')).toBeFocused();
|
||||
await expect(page.locator('#portfolio-content')).toHaveAttribute(
|
||||
'tabindex',
|
||||
'-1',
|
||||
await expect(page.locator("#portfolio-content")).toBeFocused();
|
||||
await expect(page.locator("#portfolio-content")).toHaveAttribute(
|
||||
"tabindex",
|
||||
"-1",
|
||||
);
|
||||
await expectInsetFocusIndicator(page, '#portfolio-content');
|
||||
await expectInsetFocusIndicator(page, "#portfolio-content");
|
||||
});
|
||||
|
||||
test('retains native section navigation when scripts are disabled', async ({
|
||||
test("retains native section navigation when scripts are disabled", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'load' });
|
||||
await page.goto(distUrl, { waitUntil: "load" });
|
||||
await page.locator('nav a[href="#contact"]').click();
|
||||
|
||||
await expect(page).toHaveURL(`${distUrl}/#contact`);
|
||||
await expect(page.locator('#contact')).toBeInViewport();
|
||||
await expect(page.locator("#contact")).toBeInViewport();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,68 +1,111 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import { expect, revealNavigationLinks, test } from "../helpers/serve-dist";
|
||||
import {
|
||||
environmentChunkPattern,
|
||||
installSceneInstrumentation,
|
||||
readSceneInstrumentation,
|
||||
} from '../helpers/scene-instrumentation';
|
||||
} from "../helpers/scene-instrumentation";
|
||||
|
||||
test.beforeEach(async ({ page }) => installSceneInstrumentation(page));
|
||||
|
||||
test('falls back to one poster and no scene loop when the deferred scene chunk is unavailable', async ({
|
||||
test("falls back to one poster and no scene loop when the deferred scene chunk is unavailable", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
const pageErrors: string[] = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
await page.route(environmentChunkPattern, (route) => route.abort('failed'));
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
page.on("pageerror", (error) => pageErrors.push(error.message));
|
||||
await page.route(environmentChunkPattern, (route) => route.abort("failed"));
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'failed');
|
||||
await expect(page.locator('.journey-poster')).toHaveCount(1);
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await expect(page.locator('main > section')).toHaveCount(8);
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-state",
|
||||
"failed",
|
||||
);
|
||||
await expect(page.locator(".journey-poster")).toHaveCount(1);
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await expect(page.locator("main > section")).toHaveCount(8);
|
||||
await revealNavigationLinks(page);
|
||||
await expect(page.locator('nav a[href="#contact"]')).toBeVisible();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).activeAnimationFrames).toBe(0);
|
||||
expect((await readSceneInstrumentation(page)).unhandledRejections).toEqual([]);
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await readSceneInstrumentation(page)).activeAnimationFrames,
|
||||
)
|
||||
.toBe(0);
|
||||
expect((await readSceneInstrumentation(page)).unhandledRejections).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(pageErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test('contains a WebGL initialization failure without hiding the portfolio', async ({ page, distUrl }) => {
|
||||
test("contains a WebGL initialization failure without hiding the portfolio", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
const pageErrors: string[] = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
page.on("pageerror", (error) => pageErrors.push(error.message));
|
||||
await page.addInitScript(() => {
|
||||
const nativeGetContext = HTMLCanvasElement.prototype.getContext;
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, "getContext", {
|
||||
configurable: true,
|
||||
value(this: HTMLCanvasElement, contextId: string, options?: unknown): RenderingContext | null {
|
||||
if (contextId === 'webgl' || contextId === 'webgl2' || contextId === 'experimental-webgl') return null;
|
||||
return Reflect.apply(nativeGetContext, this, [contextId, options]) as RenderingContext | null;
|
||||
value(
|
||||
this: HTMLCanvasElement,
|
||||
contextId: string,
|
||||
options?: unknown,
|
||||
): RenderingContext | null {
|
||||
if (
|
||||
contextId === "webgl" ||
|
||||
contextId === "webgl2" ||
|
||||
contextId === "experimental-webgl"
|
||||
)
|
||||
return null;
|
||||
return Reflect.apply(nativeGetContext, this, [
|
||||
contextId,
|
||||
options,
|
||||
]) as RenderingContext | null;
|
||||
},
|
||||
});
|
||||
});
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'failed');
|
||||
await expect(page.locator('.journey-poster')).toHaveCount(1);
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await expect(page.locator('#portfolio-content')).toBeVisible();
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-state",
|
||||
"failed",
|
||||
);
|
||||
await expect(page.locator(".journey-poster")).toHaveCount(1);
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await expect(page.locator("#portfolio-content")).toBeVisible();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).activeAnimationFrames).toBe(0);
|
||||
expect((await readSceneInstrumentation(page)).unhandledRejections).toEqual([]);
|
||||
await expect
|
||||
.poll(
|
||||
async () => (await readSceneInstrumentation(page)).activeAnimationFrames,
|
||||
)
|
||||
.toBe(0);
|
||||
expect((await readSceneInstrumentation(page)).unhandledRejections).toEqual(
|
||||
[],
|
||||
);
|
||||
expect(pageErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test('allows an explicit Static visitor to re-enable motion when the system permits it', async ({ page, distUrl }) => {
|
||||
test("allows an explicit Static visitor to re-enable motion when the system permits it", async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
const requests: string[] = [];
|
||||
page.on('request', (request) => requests.push(request.url()));
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await page.getByLabel('Quality control').selectOption('static');
|
||||
page.on("request", (request) => requests.push(request.url()));
|
||||
await page.goto(distUrl, { waitUntil: "domcontentloaded" });
|
||||
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 page.getByRole('button', { name: 'Enable motion' }).click();
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Enable motion" }),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Enable motion" }).click();
|
||||
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(1);
|
||||
await expect(page.locator(".journey-world")).toHaveAttribute(
|
||||
"data-scene-state",
|
||||
"running",
|
||||
);
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(1);
|
||||
expect(requests.some((url) => environmentChunkPattern.test(url))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -116,7 +116,9 @@ const captureWorldSequence = async (
|
||||
for (let index = 0; index < frameCount; index += 1) {
|
||||
frames.push(
|
||||
await sampleFrame(
|
||||
await world.page().screenshot({ animations: "allow", clip, scale: "css" }),
|
||||
await world
|
||||
.page()
|
||||
.screenshot({ animations: "allow", clip, scale: "css" }),
|
||||
),
|
||||
);
|
||||
if (index + 1 < frameCount) await world.page().waitForTimeout(intervalMs);
|
||||
@@ -352,7 +354,9 @@ test.describe("production visual composition", () => {
|
||||
await expect(page.locator(".journey-canvas")).toHaveCount(0);
|
||||
await expect(page.locator(".journey-poster")).toHaveCount(1);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
|
||||
await page.evaluate(() =>
|
||||
(document.activeElement as HTMLElement | null)?.blur(),
|
||||
);
|
||||
await page.mouse.move(1, 1);
|
||||
|
||||
const first = await captureViewport(page, testInfo, "visual-static-a");
|
||||
@@ -378,7 +382,9 @@ test.describe("production visual composition", () => {
|
||||
"reduced motion requested",
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
|
||||
await page.evaluate(() =>
|
||||
(document.activeElement as HTMLElement | null)?.blur(),
|
||||
);
|
||||
await page.mouse.move(1, 1);
|
||||
await captureViewport(page, testInfo, "visual-reduced-motion");
|
||||
});
|
||||
@@ -482,7 +488,7 @@ test.describe("three-flash safety", () => {
|
||||
page,
|
||||
distUrl,
|
||||
}, testInfo) => {
|
||||
test.setTimeout(30_000);
|
||||
test.setTimeout(60_000);
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await waitForRenderedPortfolio(page, distUrl);
|
||||
await page.mouse.wheel(0, 900);
|
||||
@@ -513,7 +519,9 @@ test.describe("three-flash safety", () => {
|
||||
"static",
|
||||
);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.evaluate(() => (document.activeElement as HTMLElement | null)?.blur());
|
||||
await page.evaluate(() =>
|
||||
(document.activeElement as HTMLElement | null)?.blur(),
|
||||
);
|
||||
await page.mouse.move(1, 1);
|
||||
const staticResult = analyzeFlashSequence(
|
||||
await captureWorldSequence(page.locator(".journey-world"), 8, 100),
|
||||
|
||||
Reference in New Issue
Block a user