{{ infrastructure.title }}
-{{ infrastructure.summary }}
++ {{ infrastructure.title }} +
++ {{ infrastructure.summary }} +
}>();
:aria-describedby="`${infrastructure.id}-diagram-description`"
>
-
+
Request
Visitor
Inbound traffic
-
- Public edge
+
+ Public edge
01 / edge
Nginx and TLS
@@ -32,35 +44,78 @@ defineProps<{ infrastructure: Readonly }>();
fail2ban guard
-
- Access boundary
+ {{ infrastructure.title }}
+ (
- ':scope > h2[tabindex="-1"]',
+ 'h1[tabindex="-1"], h2[tabindex="-1"]',
);
return heading === null ? null : { id, target };
}
@@ -94,7 +95,7 @@ const browserRuntime = (): NavigationRuntime => ({
target.scrollIntoView({ behavior, block: "start" }),
focusHeading: (target) => {
target
- .querySelector(':scope > h2[tabindex="-1"]')
+ .querySelector('h1[tabindex="-1"], h2[tabindex="-1"]')
?.focus({ preventScroll: true });
},
getScrollY: () => window.scrollY,
@@ -165,8 +166,13 @@ export function createNavigationController(
source,
destinationId,
};
- if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.noteNavigationEpoch(currentState.epoch);
- if (TEST_BUILD && source === 'explicit-navigation' && (destinationId === 'toolkit' || destinationId === 'contact')) {
+ if (TEST_BUILD)
+ window.__PORTFOLIO_PERFORMANCE__?.noteNavigationEpoch(currentState.epoch);
+ if (
+ TEST_BUILD &&
+ source === "explicit-navigation" &&
+ (destinationId === "toolkit" || destinationId === "contact")
+ ) {
window.__PORTFOLIO_PERFORMANCE__?.recordActionMutation(destinationId);
}
emit();
@@ -191,7 +197,11 @@ export function createNavigationController(
) {
return;
}
- if (TEST_BUILD && window.__PORTFOLIO_PERFORMANCE__?.holdNavigationSettlement(epoch)) return;
+ if (
+ TEST_BUILD &&
+ window.__PORTFOLIO_PERFORMANCE__?.holdNavigationSettlement(epoch)
+ )
+ return;
cancelSettle();
frameId = runtime.requestFrame(() => {
frameId = undefined;
@@ -210,7 +220,8 @@ export function createNavigationController(
id: SectionId,
target: HTMLElement,
): void => {
- if (TEST_BUILD) window.__PORTFOLIO_PERFORMANCE__?.registerNavigationSettlement(epoch);
+ if (TEST_BUILD)
+ window.__PORTFOLIO_PERFORMANCE__?.registerNavigationSettlement(epoch);
settleTimerId = runtime.setTimer(() => {
settleTimerId = undefined;
if (
diff --git a/src/scene/post-processing.ts b/src/scene/post-processing.ts
index 3c8ebda..13f767a 100644
--- a/src/scene/post-processing.ts
+++ b/src/scene/post-processing.ts
@@ -1,16 +1,29 @@
-import * as THREE from 'three';
-import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
-import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
-import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
-import type { SceneResourceBag } from '@/scene/resources';
+import * as THREE from "three";
+import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
+import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
+import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
+import type {
+ GpuRenderTargetAllocation,
+ GpuRenderTargetReporter,
+ SceneResourceBag,
+} from "@/scene/resources";
-export interface PostProcessor {
+export interface PostProcessor extends GpuRenderTargetReporter {
resize(width: number, height: number, pixelRatio: number): void;
render(scene: unknown, camera: unknown): void;
dispose(): void;
}
+interface RenderTargetLike {
+ width: number;
+ height: number;
+ samples?: number;
+ depthBuffer?: boolean;
+}
+
interface ComposerLike {
+ renderTarget1?: RenderTargetLike;
+ renderTarget2?: RenderTargetLike;
addPass(pass: unknown): void;
setPixelRatio(pixelRatio: number): void;
setSize(width: number, height: number): void;
@@ -25,6 +38,9 @@ interface RenderPassLike {
}
interface BloomPassLike {
+ renderTargetBright?: RenderTargetLike;
+ renderTargetsHorizontal?: readonly RenderTargetLike[];
+ renderTargetsVertical?: readonly RenderTargetLike[];
dispose(): void;
}
@@ -37,12 +53,8 @@ export interface PostProcessingFactories {
const productionFactories: PostProcessingFactories = {
createComposer: (renderer) => new EffectComposer(renderer),
createRenderPass: (scene, camera) => new RenderPass(scene, camera),
- createBloomPass: () => new UnrealBloomPass(
- new THREE.Vector2(1, 1),
- 0.46,
- 0.32,
- 0.84,
- ),
+ createBloomPass: () =>
+ new UnrealBloomPass(new THREE.Vector2(1, 1), 0.46, 0.32, 0.84),
};
export function createPostProcessor(
@@ -51,13 +63,19 @@ export function createPostProcessor(
factories: PostProcessingFactories = productionFactories,
): PostProcessor {
const composer = factories.createComposer(renderer);
- const renderPass = factories.createRenderPass(new THREE.Scene(), new THREE.Camera());
+ const renderPass = factories.createRenderPass(
+ new THREE.Scene(),
+ new THREE.Camera(),
+ );
const bloomPass = factories.createBloomPass();
composer.addPass(renderPass);
composer.addPass(bloomPass);
let disposed = false;
const processor: PostProcessor = {
+ gpuRenderTargets() {
+ return collectRenderTargetAllocations(composer, bloomPass);
+ },
resize(width, height, pixelRatio) {
if (disposed) return;
composer.setPixelRatio(clamp(pixelRatio, 0.5, 2));
@@ -80,7 +98,41 @@ export function createPostProcessor(
composer.dispose();
},
};
- return resources.own('pass', processor);
+ return resources.own("pass", processor);
+}
+
+function collectRenderTargetAllocations(
+ composer: ComposerLike,
+ bloomPass: BloomPassLike,
+): readonly GpuRenderTargetAllocation[] {
+ const candidates: ReadonlyArray<
+ readonly [string, RenderTargetLike | undefined]
+ > = [
+ ["composer-target-1", composer.renderTarget1],
+ ["composer-target-2", composer.renderTarget2],
+ ["bloom-bright", bloomPass.renderTargetBright],
+ ...(bloomPass.renderTargetsHorizontal ?? []).map(
+ (target, index) => [`bloom-horizontal-${index}`, target] as const,
+ ),
+ ...(bloomPass.renderTargetsVertical ?? []).map(
+ (target, index) => [`bloom-vertical-${index}`, target] as const,
+ ),
+ ];
+ const seen = new Set();
+ return candidates.flatMap(([id, target]) => {
+ if (target === undefined || seen.has(target)) return [];
+ seen.add(target);
+ return [
+ {
+ id,
+ width: Math.max(1, Math.floor(finiteOr(target.width, 1))),
+ height: Math.max(1, Math.floor(finiteOr(target.height, 1))),
+ bytesPerPixel: 4,
+ attachments: target.depthBuffer === false ? 1 : 2,
+ samples: Math.max(1, Math.floor(finiteOr(target.samples ?? 1, 1))),
+ },
+ ];
+ });
}
function finiteOr(value: number, fallback: number): number {
diff --git a/src/scene/resources.ts b/src/scene/resources.ts
index dbf1d66..1f1ff5c 100644
--- a/src/scene/resources.ts
+++ b/src/scene/resources.ts
@@ -1,11 +1,11 @@
export type SceneResourceKind =
- | 'geometry'
- | 'material'
- | 'texture'
- | 'target'
- | 'pass'
- | 'renderer'
- | 'other';
+ | "geometry"
+ | "material"
+ | "texture"
+ | "target"
+ | "pass"
+ | "renderer"
+ | "other";
export interface DisposableResource {
dispose(): void;
@@ -42,15 +42,25 @@ export interface GpuRenderTargetAllocation {
samples: number;
}
+export interface GpuRenderTargetReporter {
+ gpuRenderTargets(): readonly GpuRenderTargetAllocation[];
+}
+
export interface SceneGpuBudgetSnapshot {
- tier: 'low' | 'high';
+ tier: "low" | "high";
textures: readonly GpuTextureAllocation[];
geometryBytes: number;
renderTargets: readonly GpuRenderTargetAllocation[];
}
const resourceKinds: readonly SceneResourceKind[] = [
- 'geometry', 'material', 'texture', 'target', 'pass', 'renderer', 'other',
+ "geometry",
+ "material",
+ "texture",
+ "target",
+ "pass",
+ "renderer",
+ "other",
];
export class SceneResourceBag {
@@ -141,7 +151,9 @@ export class SceneResourceBag {
}
snapshot(): SceneResourceSnapshot {
- const owned = Object.fromEntries(resourceKinds.map((kind) => [kind, 0])) as Record;
+ const owned = Object.fromEntries(
+ resourceKinds.map((kind) => [kind, 0]),
+ ) as Record;
for (const kind of this.resources.values()) owned[kind] += 1;
return {
disposed: this.disposed,
@@ -154,23 +166,38 @@ export class SceneResourceBag {
}
gpuBudgetSnapshot(input: {
- tier: 'low' | 'high';
- defaultFramebuffer: Omit;
+ tier: "low" | "high";
+ defaultFramebuffer: Omit;
}): SceneGpuBudgetSnapshot {
let geometryBytes = 0;
const seenBuffers = new Set();
const textures: GpuTextureAllocation[] = [];
- const renderTargets: GpuRenderTargetAllocation[] = [{ id: 'default-framebuffer', ...input.defaultFramebuffer }];
+ const renderTargets: GpuRenderTargetAllocation[] = [
+ { id: "default-framebuffer", ...input.defaultFramebuffer },
+ ];
let textureIndex = 0;
let targetIndex = 0;
for (const [resource, kind] of this.resources) {
- if (kind === 'geometry') {
+ const reporter = resource as DisposableResource &
+ Partial;
+ if (typeof reporter.gpuRenderTargets === "function") {
+ for (const allocation of reporter.gpuRenderTargets()) {
+ validateRenderTargetAllocation(allocation);
+ renderTargets.push({ ...allocation });
+ }
+ }
+ if (kind === "geometry") {
const geometry = resource as DisposableResource & {
- attributes?: Record;
+ attributes?: Record<
+ string,
+ { array?: ArrayBufferView; data?: { array?: ArrayBufferView } }
+ >;
index?: { array?: ArrayBufferView } | null;
};
const arrays = [
- ...Object.values(geometry.attributes ?? {}).map((attribute) => attribute.array ?? attribute.data?.array),
+ ...Object.values(geometry.attributes ?? {}).map(
+ (attribute) => attribute.array ?? attribute.data?.array,
+ ),
geometry.index?.array,
].filter((array): array is ArrayBufferView => array !== undefined);
for (const array of arrays) {
@@ -179,28 +206,58 @@ export class SceneResourceBag {
geometryBytes += array.byteLength;
}
}
- if (kind === 'texture') {
+ if (kind === "texture") {
const texture = resource as DisposableResource & {
image?: { width?: number; height?: number; data?: ArrayBufferView };
generateMipmaps?: boolean;
};
const width = Number(texture.image?.width);
const height = Number(texture.image?.height);
- if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
- throw new Error('Owned production texture is missing measurable dimensions.');
+ if (
+ !Number.isInteger(width) ||
+ width <= 0 ||
+ !Number.isInteger(height) ||
+ height <= 0
+ ) {
+ throw new Error(
+ "Owned production texture is missing measurable dimensions.",
+ );
}
const dataBytes = texture.image?.data?.byteLength;
- const bytesPerPixel = dataBytes === undefined ? 4 : dataBytes / (width * height);
- if (!Number.isInteger(bytesPerPixel) || bytesPerPixel <= 0) throw new Error('Owned production texture has an unsupported pixel layout.');
- textures.push({ id: `owned-texture-${textureIndex}`, width, height, bytesPerPixel, layers: 1, mipmapped: texture.generateMipmaps !== false });
+ const bytesPerPixel =
+ dataBytes === undefined ? 4 : dataBytes / (width * height);
+ if (!Number.isInteger(bytesPerPixel) || bytesPerPixel <= 0)
+ throw new Error(
+ "Owned production texture has an unsupported pixel layout.",
+ );
+ textures.push({
+ id: `owned-texture-${textureIndex}`,
+ width,
+ height,
+ bytesPerPixel,
+ layers: 1,
+ mipmapped: texture.generateMipmaps !== false,
+ });
textureIndex += 1;
}
- if (kind === 'target') {
- const target = resource as DisposableResource & { width?: number; height?: number; samples?: number; depthBuffer?: boolean };
+ if (kind === "target") {
+ const target = resource as DisposableResource & {
+ width?: number;
+ height?: number;
+ samples?: number;
+ depthBuffer?: boolean;
+ };
const width = Number(target.width);
const height = Number(target.height);
- if (!Number.isInteger(width) || width <= 0 || !Number.isInteger(height) || height <= 0) {
- throw new Error('Owned production render target is missing measurable dimensions.');
+ if (
+ !Number.isInteger(width) ||
+ width <= 0 ||
+ !Number.isInteger(height) ||
+ height <= 0
+ ) {
+ throw new Error(
+ "Owned production render target is missing measurable dimensions.",
+ );
}
renderTargets.push({
id: `owned-render-target-${targetIndex}`,
@@ -216,3 +273,25 @@ export class SceneResourceBag {
return { tier: input.tier, textures, geometryBytes, renderTargets };
}
}
+
+function validateRenderTargetAllocation(
+ allocation: GpuRenderTargetAllocation,
+): void {
+ if (
+ allocation.id.length === 0 ||
+ !Number.isInteger(allocation.width) ||
+ allocation.width <= 0 ||
+ !Number.isInteger(allocation.height) ||
+ allocation.height <= 0 ||
+ !Number.isInteger(allocation.bytesPerPixel) ||
+ allocation.bytesPerPixel <= 0 ||
+ !Number.isInteger(allocation.attachments) ||
+ allocation.attachments <= 0 ||
+ !Number.isInteger(allocation.samples) ||
+ allocation.samples <= 0
+ ) {
+ throw new Error(
+ "Owned post-processing render target has an invalid allocation description.",
+ );
+ }
+}
diff --git a/src/styles/components.css b/src/styles/components.css
index b31299a..5471d53 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -44,7 +44,7 @@
}
.hero-journey__action::after {
- content: '→';
+ content: "→";
transition: transform var(--transition);
}
@@ -120,7 +120,7 @@
inset: 0 auto 0 0.23rem;
inline-size: 1px;
background: var(--line-strong);
- content: '';
+ content: "";
}
.experience-timeline > li {
@@ -136,7 +136,7 @@
block-size: 0.5rem;
border: 1px solid var(--cyan);
background: var(--canvas);
- content: '';
+ content: "";
transform: rotate(45deg);
}
@@ -251,7 +251,7 @@
inline-size: 0.65rem;
margin-inline-end: 0.4rem;
border-block-start: 1px dashed var(--line-strong);
- content: '';
+ content: "";
vertical-align: middle;
}
@@ -270,7 +270,7 @@
block-size: 0.36rem;
border-block-start: 1px solid var(--cyan);
border-inline-end: 1px solid var(--cyan);
- content: '';
+ content: "";
transform: rotate(45deg);
}
@@ -293,7 +293,63 @@
transform: rotate(135deg);
}
-[data-testid='infrastructure-text-equivalent'] {
+.infrastructure-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.75rem 1.75rem;
+ margin: var(--space-2) 0 0;
+ padding: 0;
+}
+
+.infrastructure-legend > div {
+ display: grid;
+ gap: 0.25rem;
+}
+
+.infrastructure-legend dt {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.55rem;
+ color: var(--text);
+ font: 600 0.68rem/1.2 var(--mono);
+ letter-spacing: 0.07em;
+ text-transform: uppercase;
+}
+
+.infrastructure-legend dd {
+ margin: 0;
+ color: var(--text-muted);
+ font-size: 0.74rem;
+}
+
+.infrastructure-legend__boundary,
+.infrastructure-legend__flow {
+ position: relative;
+ display: inline-block;
+ inline-size: 1.65rem;
+ block-size: 0.5rem;
+}
+
+.infrastructure-legend__boundary {
+ border-block-start: 1px dashed var(--line-strong);
+}
+
+.infrastructure-legend__flow {
+ border-block-start: 1px solid var(--cyan);
+}
+
+.infrastructure-legend__flow::after {
+ position: absolute;
+ inset: -0.2rem 0 auto auto;
+ inline-size: 0.36rem;
+ block-size: 0.36rem;
+ border-block-start: 1px solid var(--cyan);
+ border-inline-end: 1px solid var(--cyan);
+ content: "";
+ transform: rotate(45deg);
+}
+
+[data-testid="infrastructure-text-equivalent"] {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-2);
@@ -301,13 +357,13 @@
list-style: none;
}
-[data-testid='infrastructure-text-equivalent'] li {
+[data-testid="infrastructure-text-equivalent"] li {
margin: 0;
padding: var(--space-2);
border-block-start: 1px solid var(--line);
}
-[data-testid='infrastructure-text-equivalent'] strong {
+[data-testid="infrastructure-text-equivalent"] strong {
color: var(--text);
}
@@ -327,12 +383,12 @@
box-shadow: var(--panel-shadow);
}
-.project-case-study[data-project-id='footy-tracker'],
-.project-case-study[data-project-id='public-build-two'] {
+.project-case-study[data-project-id="footy-tracker"],
+.project-case-study[data-project-id="public-build-two"] {
margin-inline-start: 54%;
}
-.project-case-study[data-project-priority='supporting'] {
+.project-case-study[data-project-priority="supporting"] {
inline-size: min(38rem, 62%);
margin-inline: auto;
background: var(--surface-soft);
@@ -349,7 +405,9 @@
font-size: var(--step-2);
}
-.project-case-study[data-project-priority='supporting'] .project-case-study__header h3 {
+.project-case-study[data-project-priority="supporting"]
+ .project-case-study__header
+ h3 {
font-size: var(--step-1);
}
@@ -395,12 +453,12 @@
border-block-start: 1px solid var(--line);
}
-.project-card[data-maturity='prototype'],
-.project-card[data-maturity='active'] {
+.project-card[data-maturity="prototype"],
+.project-card[data-maturity="active"] {
margin-inline-end: 8%;
}
-.project-card[data-maturity='learning'] {
+.project-card[data-maturity="learning"] {
margin-inline-start: 12%;
}
@@ -576,8 +634,8 @@
inline-size: 62%;
}
- .project-case-study[data-project-id='footy-tracker'],
- .project-case-study[data-project-id='public-build-two'] {
+ .project-case-study[data-project-id="footy-tracker"],
+ .project-case-study[data-project-id="public-build-two"] {
margin-inline-start: 38%;
}
@@ -633,7 +691,7 @@
.profile-facts,
.project-evidence > div,
- [data-testid='infrastructure-text-equivalent'],
+ [data-testid="infrastructure-text-equivalent"],
.toolkit-grid {
grid-template-columns: 1fr;
}
diff --git a/src/styles/layout.css b/src/styles/layout.css
index a3cf710..9bc1796 100644
--- a/src/styles/layout.css
+++ b/src/styles/layout.css
@@ -49,7 +49,10 @@
font: 520 0.78rem/1 var(--mono);
letter-spacing: 0.06em;
text-decoration: none;
- transition: color var(--transition), background-color var(--transition), border-color var(--transition);
+ transition:
+ color var(--transition),
+ background-color var(--transition),
+ border-color var(--transition);
}
.site-navigation a:hover,
@@ -58,7 +61,7 @@
color: var(--text);
}
-.site-navigation a[aria-current='page'] {
+.site-navigation a[aria-current="page"] {
border-block-end-color: var(--cyan);
color: var(--cyan);
}
@@ -97,29 +100,31 @@
min-width: 0;
margin-block: clamp(6rem, 16vh, 11rem);
padding: clamp(1.5rem, 2.6vw, 2.7rem);
- scroll-margin-block-start: calc(var(--site-navigation-block-size) + var(--space-3));
+ scroll-margin-block-start: calc(
+ var(--site-navigation-block-size) + var(--space-3)
+ );
border: var(--panel-border);
border-radius: var(--radius-surface);
background: var(--surface);
box-shadow: var(--panel-shadow);
}
-.portfolio-section[data-layout='right-rail'] {
+.portfolio-section[data-layout="right-rail"] {
inline-size: 42%;
margin-inline: 54% 4%;
}
-.portfolio-section[data-layout='left-timeline'] {
+.portfolio-section[data-layout="left-timeline"] {
inline-size: 46%;
margin-inline: 4% 50%;
}
-.portfolio-section[data-layout='full-diagram'] {
+.portfolio-section[data-layout="full-diagram"] {
inline-size: 78%;
margin-inline: 11%;
}
-.portfolio-section[data-layout='project-stage'] {
+.portfolio-section[data-layout="project-stage"] {
inline-size: 92%;
margin-inline: 4%;
padding: 0;
@@ -128,25 +133,25 @@
box-shadow: none;
}
-.portfolio-section[data-layout='project-stage'] > h2,
-.portfolio-section[data-layout='project-stage'] > .section-eyebrow {
+.portfolio-section[data-layout="project-stage"] > h2,
+.portfolio-section[data-layout="project-stage"] > .section-eyebrow {
inline-size: fit-content;
padding: 0.55rem 0.8rem;
border-inline-start: 2px solid var(--cyan);
background: var(--surface-strong);
}
-.portfolio-section[data-layout='compact-rail'] {
+.portfolio-section[data-layout="compact-rail"] {
inline-size: 42%;
margin-inline: 54% 4%;
}
-.portfolio-section[data-layout='toolkit-field'] {
+.portfolio-section[data-layout="toolkit-field"] {
inline-size: 78%;
margin-inline: 11%;
}
-.portfolio-section[data-layout='destination'] {
+.portfolio-section[data-layout="destination"] {
inline-size: min(54rem, 62%);
margin-inline: auto;
}
@@ -164,33 +169,33 @@
inline-size: min(34rem, 62vw);
}
- .portfolio-section[data-layout='right-rail'] {
+ .portfolio-section[data-layout="right-rail"] {
inline-size: 60%;
margin-inline: 36% 4%;
}
- .portfolio-section[data-layout='left-timeline'] {
+ .portfolio-section[data-layout="left-timeline"] {
inline-size: 60%;
margin-inline: 4% 36%;
}
- .portfolio-section[data-layout='full-diagram'],
- .portfolio-section[data-layout='toolkit-field'] {
+ .portfolio-section[data-layout="full-diagram"],
+ .portfolio-section[data-layout="toolkit-field"] {
inline-size: 84%;
margin-inline: 8%;
}
- .portfolio-section[data-layout='project-stage'] {
+ .portfolio-section[data-layout="project-stage"] {
inline-size: 92%;
margin-inline: 4%;
}
- .portfolio-section[data-layout='compact-rail'] {
+ .portfolio-section[data-layout="compact-rail"] {
inline-size: 62%;
margin-inline: 34% 4%;
}
- .portfolio-section[data-layout='destination'] {
+ .portfolio-section[data-layout="destination"] {
inline-size: 70%;
}
}
@@ -219,18 +224,23 @@
.site-navigation summary::after {
color: var(--cyan);
- content: '+';
+ content: "+";
font-size: 1.1rem;
}
.site-navigation details[open] summary::after {
- content: '−';
+ content: "−";
}
.site-navigation summary::-webkit-details-marker {
display: none;
}
+ .site-navigation summary:focus-visible {
+ outline: none;
+ box-shadow: inset 0 0 0 2px var(--focus);
+ }
+
.site-navigation ul {
position: absolute;
inset: 100% 0 auto;
@@ -271,14 +281,14 @@
background: var(--surface-strong);
}
- .portfolio-section[data-layout='project-stage'] {
+ .portfolio-section[data-layout="project-stage"] {
padding: 0;
border: 0;
background: transparent;
}
- .portfolio-section[data-layout='project-stage'] > h2,
- .portfolio-section[data-layout='project-stage'] > .section-eyebrow {
+ .portfolio-section[data-layout="project-stage"] > h2,
+ .portfolio-section[data-layout="project-stage"] > .section-eyebrow {
margin-inline-start: 0;
background: var(--surface-strong);
}
diff --git a/tests/browser/accessibility-forced-colors.spec.ts b/tests/browser/accessibility-forced-colors.spec.ts
index 83eb87e..b0da6f0 100644
--- a/tests/browser/accessibility-forced-colors.spec.ts
+++ b/tests/browser/accessibility-forced-colors.spec.ts
@@ -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(
diff --git a/tests/browser/accessibility-keyboard.spec.ts b/tests/browser/accessibility-keyboard.spec.ts
index da342d6..bdf634a 100644
--- a/tests/browser/accessibility-keyboard.spec.ts
+++ b/tests/browser/accessibility-keyboard.spec.ts
@@ -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");
diff --git a/tests/browser/accessibility-reflow.spec.ts b/tests/browser/accessibility-reflow.spec.ts
index b87f592..1c1f691 100644
--- a/tests/browser/accessibility-reflow.spec.ts
+++ b/tests/browser/accessibility-reflow.spec.ts
@@ -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 {
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();
}
diff --git a/tests/browser/audio-controls.spec.ts b/tests/browser/audio-controls.spec.ts
index 088d354..df03fb0 100644
--- a/tests/browser/audio-controls.spec.ts
+++ b/tests/browser/audio-controls.spec.ts
@@ -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 { return Promise.reject(new DOMException('blocked', 'NotAllowedError')); }
+ override resume(): Promise {
+ 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();
});
diff --git a/tests/browser/bfcache-lifecycle.spec.ts b/tests/browser/bfcache-lifecycle.spec.ts
index 3fa68bd..71f85a7 100644
--- a/tests/browser/bfcache-lifecycle.spec.ts
+++ b/tests/browser/bfcache-lifecycle.spec.ts
@@ -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);
});
diff --git a/tests/browser/keyboard.spec.ts b/tests/browser/keyboard.spec.ts
index 77d9b89..80f893e 100644
--- a/tests/browser/keyboard.spec.ts
+++ b/tests/browser/keyboard.spec.ts
@@ -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 => {
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();
});
diff --git a/tests/browser/navigation.spec.ts b/tests/browser/navigation.spec.ts
index ab4574a..0ef3c2f 100644
--- a/tests/browser/navigation.spec.ts
+++ b/tests/browser/navigation.spec.ts
@@ -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();
diff --git a/tests/browser/no-javascript.spec.ts b/tests/browser/no-javascript.spec.ts
index e9746bb..d85aefa 100644
--- a/tests/browser/no-javascript.spec.ts
+++ b/tests/browser/no-javascript.spec.ts
@@ -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> = {
- start: 'Start',
- profile: 'Profile',
- experience: 'Career',
- infrastructure: 'Infra',
- 'selected-builds': 'Builds',
- 'additional-work': 'More work',
- toolkit: 'Toolkit',
- contact: 'Contact',
-};
+const navigationLabels: Readonly> =
+ {
+ 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 =>
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 =>
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 =>
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 `${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 `${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();
});
diff --git a/tests/browser/static-fallback.spec.ts b/tests/browser/static-fallback.spec.ts
index eb3ddc9..73281a0 100644
--- a/tests/browser/static-fallback.spec.ts
+++ b/tests/browser/static-fallback.spec.ts
@@ -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);
});
diff --git a/tests/browser/visual-regression.spec.ts b/tests/browser/visual-regression.spec.ts
index bd37f36..657a378 100644
--- a/tests/browser/visual-regression.spec.ts
+++ b/tests/browser/visual-regression.spec.ts
@@ -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),
diff --git a/tests/helpers/serve-dist.ts b/tests/helpers/serve-dist.ts
index 4a05d0c..470e5d4 100644
--- a/tests/helpers/serve-dist.ts
+++ b/tests/helpers/serve-dist.ts
@@ -1,29 +1,29 @@
-import { readFile, realpath, stat } from 'node:fs/promises';
-import { createServer, type ServerResponse } from 'node:http';
-import { extname, isAbsolute, relative, resolve, sep } from 'node:path';
-import { expect, test as base, type Page } from '@playwright/test';
+import { readFile, realpath, stat } from "node:fs/promises";
+import { createServer, type ServerResponse } from "node:http";
+import { extname, isAbsolute, relative, resolve, sep } from "node:path";
+import { expect, test as base, type Page } from "@playwright/test";
-const LOOPBACK_HOST = '127.0.0.1';
+const LOOPBACK_HOST = "127.0.0.1";
const contentTypes: Readonly> = {
- '.avif': 'image/avif',
- '.br': 'application/octet-stream',
- '.css': 'text/css; charset=utf-8',
- '.gif': 'image/gif',
- '.html': 'text/html; charset=utf-8',
- '.ico': 'image/x-icon',
- '.jpeg': 'image/jpeg',
- '.jpg': 'image/jpeg',
- '.js': 'text/javascript; charset=utf-8',
- '.json': 'application/json; charset=utf-8',
- '.map': 'application/json; charset=utf-8',
- '.png': 'image/png',
- '.svg': 'image/svg+xml; charset=utf-8',
- '.txt': 'text/plain; charset=utf-8',
- '.webp': 'image/webp',
- '.woff': 'font/woff',
- '.woff2': 'font/woff2',
- '.xml': 'application/xml; charset=utf-8',
+ ".avif": "image/avif",
+ ".br": "application/octet-stream",
+ ".css": "text/css; charset=utf-8",
+ ".gif": "image/gif",
+ ".html": "text/html; charset=utf-8",
+ ".ico": "image/x-icon",
+ ".jpeg": "image/jpeg",
+ ".jpg": "image/jpeg",
+ ".js": "text/javascript; charset=utf-8",
+ ".json": "application/json; charset=utf-8",
+ ".map": "application/json; charset=utf-8",
+ ".png": "image/png",
+ ".svg": "image/svg+xml; charset=utf-8",
+ ".txt": "text/plain; charset=utf-8",
+ ".webp": "image/webp",
+ ".woff": "font/woff",
+ ".woff2": "font/woff2",
+ ".xml": "application/xml; charset=utf-8",
};
const sendText = (
@@ -32,10 +32,10 @@ const sendText = (
body: string,
): void => {
response.writeHead(status, {
- 'Cache-Control': 'no-store',
- 'Content-Length': Buffer.byteLength(body),
- 'Content-Type': 'text/plain; charset=utf-8',
- 'X-Content-Type-Options': 'nosniff',
+ "Cache-Control": "no-store",
+ "Content-Length": Buffer.byteLength(body),
+ "Content-Type": "text/plain; charset=utf-8",
+ "X-Content-Type-Options": "nosniff",
});
response.end(body);
};
@@ -43,15 +43,15 @@ const sendText = (
const isInside = (root: string, candidate: string): boolean => {
const pathFromRoot = relative(root, candidate);
return (
- pathFromRoot === '' ||
+ pathFromRoot === "" ||
(!isAbsolute(pathFromRoot) &&
- pathFromRoot !== '..' &&
+ pathFromRoot !== ".." &&
!pathFromRoot.startsWith(`..${sep}`))
);
};
const decodedRequestPath = (requestTarget: string): string | null => {
- const rawPath = requestTarget.split(/[?#]/u, 1)[0] ?? '/';
+ const rawPath = requestTarget.split(/[?#]/u, 1)[0] ?? "/";
let decoded: string;
try {
decoded = decodeURIComponent(rawPath);
@@ -60,19 +60,19 @@ const decodedRequestPath = (requestTarget: string): string | null => {
}
if (
- !decoded.startsWith('/') ||
- decoded.includes('\0') ||
- decoded.includes('\\')
+ !decoded.startsWith("/") ||
+ decoded.includes("\0") ||
+ decoded.includes("\\")
) {
return null;
}
- const segments = decoded.split('/');
- if (segments.some((segment) => segment === '.' || segment === '..')) {
+ const segments = decoded.split("/");
+ if (segments.some((segment) => segment === "." || segment === "..")) {
return null;
}
- return decoded === '/' || decoded.endsWith('/')
+ return decoded === "/" || decoded.endsWith("/")
? `${decoded}index.html`
: decoded;
};
@@ -116,8 +116,24 @@ export async function expectInsetFocusIndicator(
});
}
+export async function revealNavigationLinks(page: Page): Promise {
+ const summary = page.locator(".site-navigation summary");
+ if (!(await summary.isVisible())) return;
+ await page.locator(".site-navigation details").evaluate((details) => {
+ if (details instanceof HTMLDetailsElement) details.open = true;
+ });
+}
+
+export async function clickNavigationLink(
+ page: Page,
+ href: string,
+): Promise {
+ await revealNavigationLinks(page);
+ await page.locator(`nav a[href="${href}"]`).click();
+}
+
export async function startDistServer(
- distRoot = resolve(process.cwd(), 'dist'),
+ distRoot = resolve(process.cwd(), "dist"),
): Promise {
const root = await realpath(distRoot);
if (!(await stat(root)).isDirectory()) {
@@ -126,21 +142,21 @@ export async function startDistServer(
const server = createServer((request, response) => {
void (async () => {
- if (request.method !== 'GET' && request.method !== 'HEAD') {
- response.setHeader('Allow', 'GET, HEAD');
- sendText(response, 405, 'Method not allowed.');
+ if (request.method !== "GET" && request.method !== "HEAD") {
+ response.setHeader("Allow", "GET, HEAD");
+ sendText(response, 405, "Method not allowed.");
return;
}
- const requestPath = decodedRequestPath(request.url ?? '/');
+ const requestPath = decodedRequestPath(request.url ?? "/");
if (requestPath === null) {
- sendText(response, 403, 'Forbidden.');
+ sendText(response, 403, "Forbidden.");
return;
}
const candidate = resolve(root, `.${requestPath}`);
if (!isInside(root, candidate)) {
- sendText(response, 403, 'Forbidden.');
+ sendText(response, 403, "Forbidden.");
return;
}
@@ -148,34 +164,34 @@ export async function startDistServer(
try {
filePath = await realpath(candidate);
} catch {
- sendText(response, 404, 'Not found.');
+ sendText(response, 404, "Not found.");
return;
}
if (!isInside(root, filePath)) {
- sendText(response, 403, 'Forbidden.');
+ sendText(response, 403, "Forbidden.");
return;
}
const file = await stat(filePath);
if (!file.isFile()) {
- sendText(response, 404, 'Not found.');
+ sendText(response, 404, "Not found.");
return;
}
const body = await readFile(filePath);
response.writeHead(200, {
- 'Cache-Control': 'no-store',
- 'Content-Length': body.byteLength,
- 'Content-Type':
+ "Cache-Control": "no-store",
+ "Content-Length": body.byteLength,
+ "Content-Type":
contentTypes[extname(filePath).toLowerCase()] ??
- 'application/octet-stream',
- 'X-Content-Type-Options': 'nosniff',
+ "application/octet-stream",
+ "X-Content-Type-Options": "nosniff",
});
- response.end(request.method === 'HEAD' ? undefined : body);
+ response.end(request.method === "HEAD" ? undefined : body);
})().catch(() => {
if (!response.headersSent) {
- sendText(response, 500, 'Internal server error.');
+ sendText(response, 500, "Internal server error.");
} else {
response.destroy();
}
@@ -184,9 +200,9 @@ export async function startDistServer(
await new Promise((resolveListening, reject) => {
const onError = (error: Error): void => reject(error);
- server.once('error', onError);
+ server.once("error", onError);
server.listen({ host: LOOPBACK_HOST, port: 0, exclusive: true }, () => {
- server.off('error', onError);
+ server.off("error", onError);
resolveListening();
});
});
@@ -194,7 +210,7 @@ export async function startDistServer(
const address = server.address();
if (
address === null ||
- typeof address === 'string' ||
+ typeof address === "string" ||
address.address !== LOOPBACK_HOST
) {
server.closeAllConnections();
@@ -202,7 +218,7 @@ export async function startDistServer(
server.close(() => resolveClosed()),
);
throw new Error(
- 'The preview server did not bind the owned loopback listener.',
+ "The preview server did not bind the owned loopback listener.",
);
}
@@ -233,7 +249,7 @@ export const test = base.extend<{}, { distUrl: string }>({
await preview.close();
}
},
- { scope: 'worker' },
+ { scope: "worker" },
],
});
diff --git a/tests/unit/SceneCanvas.test.ts b/tests/unit/SceneCanvas.test.ts
index 7a2ca6e..c5e8cab 100644
--- a/tests/unit/SceneCanvas.test.ts
+++ b/tests/unit/SceneCanvas.test.ts
@@ -1,29 +1,31 @@
-import { mount, flushPromises } from '@vue/test-utils';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import { nextTick } from 'vue';
-import SceneCanvas from '@/components/SceneCanvas.vue';
-import DriveControls from '@/components/DriveControls.vue';
-import JourneyHud from '@/components/JourneyHud.vue';
-import type { AudioUiState } from '@/audio/types';
-import type { MotionPolicy, QualityTier } from '@/journey/motion-policy';
-import type { JourneySnapshot } from '@/journey/types';
+import { mount, flushPromises } from "@vue/test-utils";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { nextTick } from "vue";
+import SceneCanvas from "@/components/SceneCanvas.vue";
+import DriveControls from "@/components/DriveControls.vue";
+import JourneyHud from "@/components/JourneyHud.vue";
+import type { AudioUiState } from "@/audio/types";
+import type { MotionPolicy, QualityTier } from "@/journey/motion-policy";
+import type { JourneySnapshot } from "@/journey/types";
const sceneMocks = vi.hoisted(() => ({
build: vi.fn(async () => ({ render: vi.fn(), dispose: vi.fn() })),
}));
-vi.mock('@/scene/world/environment', () => ({
+vi.mock("@/scene/world/environment", () => ({
createProceduralSceneGeneration: sceneMocks.build,
}));
-const journey = (overrides: Partial = {}): JourneySnapshot => ({
+const journey = (
+ overrides: Partial = {},
+): JourneySnapshot => ({
scrollY: 0,
routeProgress: 0.42,
direction: 1,
acceleration: 0.3,
driveSpeed: 0.6,
- activeSectionId: 'infrastructure',
- chapter: 'infrastructure',
+ activeSectionId: "infrastructure",
+ chapter: "infrastructure",
sign: null,
arrived: false,
...overrides,
@@ -32,12 +34,12 @@ const journey = (overrides: Partial = {}): JourneySnapshot => (
const policy = (overrides: Partial = {}): MotionPolicy => ({
allowSceneRequest: true,
allowAutomaticVideo: true,
- maximumTier: 'high',
- reason: 'full',
+ maximumTier: "high",
+ reason: "full",
...overrides,
});
-describe('SceneCanvas', () => {
+describe("SceneCanvas", () => {
beforeEach(() => sceneMocks.build.mockClear());
afterEach(() => {
vi.restoreAllMocks();
@@ -45,73 +47,111 @@ describe('SceneCanvas', () => {
});
it.each([
- ['reduced motion', policy({ allowSceneRequest: false, maximumTier: 'static', reason: 'system-reduce' })],
- ['saved Static mode', policy({ allowSceneRequest: false, maximumTier: 'static', reason: 'saved-static' })],
- ])('does not request or render the scene for %s', async (_name, blockedPolicy) => {
- const wrapper = mount(SceneCanvas, {
- props: {
- journey: journey(),
- policy: blockedPolicy,
- quality: 'static',
- sceneDescription: 'A decorative night road behind the portfolio content.',
- },
- });
+ [
+ "reduced motion",
+ policy({
+ allowSceneRequest: false,
+ maximumTier: "static",
+ reason: "system-reduce",
+ }),
+ ],
+ [
+ "saved Static mode",
+ policy({
+ allowSceneRequest: false,
+ maximumTier: "static",
+ reason: "saved-static",
+ }),
+ ],
+ ])(
+ "does not request or render the scene for %s",
+ async (_name, blockedPolicy) => {
+ const wrapper = mount(SceneCanvas, {
+ props: {
+ journey: journey(),
+ policy: blockedPolicy,
+ quality: "static",
+ sceneDescription:
+ "A decorative night road behind the portfolio content.",
+ },
+ });
- await flushPromises();
+ await flushPromises();
- expect(sceneMocks.build).not.toHaveBeenCalled();
- expect(wrapper.findAll('canvas')).toHaveLength(0);
- expect(wrapper.get('img').attributes()).toMatchObject({ alt: '', width: '1600', height: '900' });
- expect(wrapper.findAll('.journey-poster-composition > span')).toHaveLength(7);
- expect(wrapper.find('.journey-poster__sun').exists()).toBe(true);
- expect(wrapper.find('.journey-poster__road').exists()).toBe(true);
- expect(wrapper.find('.journey-poster__car').exists()).toBe(true);
- expect(wrapper.text()).toContain('A decorative night road behind the portfolio content.');
- wrapper.unmount();
- });
+ expect(sceneMocks.build).not.toHaveBeenCalled();
+ expect(wrapper.findAll("canvas")).toHaveLength(0);
+ expect(wrapper.get("img").attributes()).toMatchObject({
+ alt: "",
+ width: "1600",
+ height: "900",
+ });
+ expect(
+ wrapper.findAll(".journey-poster-composition > span"),
+ ).toHaveLength(7);
+ expect(wrapper.find(".journey-poster__sun").exists()).toBe(true);
+ expect(wrapper.find(".journey-poster__road").exists()).toBe(true);
+ expect(wrapper.find(".journey-poster__car").exists()).toBe(true);
+ expect(wrapper.text()).toContain(
+ "A decorative night road behind the portfolio content.",
+ );
+ wrapper.unmount();
+ },
+ );
- it('owns exactly one decorative canvas when full motion is allowed', async () => {
+ it("owns exactly one decorative canvas when full motion is allowed", async () => {
const wrapper = mount(SceneCanvas, {
props: {
journey: journey(),
policy: policy(),
- quality: 'high',
- sceneDescription: 'A decorative night road behind the portfolio content.',
+ quality: "high",
+ sceneDescription:
+ "A decorative night road behind the portfolio content.",
},
});
await flushPromises();
expect(sceneMocks.build).toHaveBeenCalledTimes(1);
- expect(wrapper.findAll('canvas')).toHaveLength(1);
- expect(wrapper.get('canvas').attributes('aria-hidden')).toBe('true');
- expect(wrapper.get('.journey-world').attributes('aria-hidden')).toBe('true');
+ expect(wrapper.findAll("canvas")).toHaveLength(1);
+ expect(wrapper.get("canvas").attributes("aria-hidden")).toBe("true");
+ expect(wrapper.get(".journey-world").attributes("aria-hidden")).toBe(
+ "true",
+ );
wrapper.unmount();
});
- it('forwards each complete scene frame interval to adaptive quality', async () => {
+ it("forwards each complete scene frame interval to adaptive quality", async () => {
const callbacks = new Map();
let nextFrameId = 1;
- vi.stubGlobal('requestAnimationFrame', vi.fn((callback: FrameRequestCallback) => {
- const id = nextFrameId++;
- callbacks.set(id, callback);
- return id;
- }));
- vi.stubGlobal('cancelAnimationFrame', vi.fn((id: number) => callbacks.delete(id)));
+ vi.stubGlobal(
+ "requestAnimationFrame",
+ vi.fn((callback: FrameRequestCallback) => {
+ const id = nextFrameId++;
+ callbacks.set(id, callback);
+ return id;
+ }),
+ );
+ vi.stubGlobal(
+ "cancelAnimationFrame",
+ vi.fn((id: number) => callbacks.delete(id)),
+ );
const sampleFrame = vi.fn();
const wrapper = mount(SceneCanvas, {
props: {
journey: journey(),
policy: policy(),
- quality: 'high',
- sceneDescription: 'A decorative night road behind the portfolio content.',
+ quality: "high",
+ sceneDescription:
+ "A decorative night road behind the portfolio content.",
sampleFrame,
},
});
await flushPromises();
const frame = [...callbacks.entries()][0]!;
callbacks.delete(frame[0]);
- vi.spyOn(performance, 'now').mockReturnValueOnce(2_000).mockReturnValueOnce(2_301);
+ vi.spyOn(performance, "now")
+ .mockReturnValueOnce(2_000)
+ .mockReturnValueOnce(2_301);
frame[1](2_000);
@@ -119,162 +159,232 @@ describe('SceneCanvas', () => {
wrapper.unmount();
});
- it('falls back to the poster without obscuring semantic content after scene failure', async () => {
- sceneMocks.build.mockRejectedValueOnce(new Error('WebGL unavailable'));
+ it("falls back to the poster without obscuring semantic content after scene failure", async () => {
+ sceneMocks.build.mockRejectedValueOnce(new Error("WebGL unavailable"));
const wrapper = mount(SceneCanvas, {
props: {
journey: journey(),
policy: policy(),
- quality: 'medium',
- sceneDescription: 'A decorative night road behind the portfolio content.',
+ quality: "medium",
+ sceneDescription:
+ "A decorative night road behind the portfolio content.",
},
- slots: { default: ' ' },
+ slots: { default: " " },
});
await flushPromises();
- expect(wrapper.get('img').isVisible()).toBe(true);
- expect(wrapper.findAll('canvas')).toHaveLength(0);
- expect(wrapper.text()).toContain('Semantic portfolio');
+ expect(wrapper.get("img").isVisible()).toBe(true);
+ expect(wrapper.findAll("canvas")).toHaveLength(0);
+ expect(wrapper.text()).toContain("Semantic portfolio");
wrapper.unmount();
});
- it('aborts and removes the canvas on a Static transition', async () => {
+ it("aborts and removes the canvas on a Static transition", async () => {
const wrapper = mount(SceneCanvas, {
props: {
journey: journey(),
policy: policy(),
- quality: 'high',
- sceneDescription: 'A decorative night road behind the portfolio content.',
+ quality: "high",
+ sceneDescription:
+ "A decorative night road behind the portfolio content.",
},
});
await flushPromises();
- expect(wrapper.findAll('canvas')).toHaveLength(1);
+ expect(wrapper.findAll("canvas")).toHaveLength(1);
await wrapper.setProps({
- policy: policy({ allowSceneRequest: false, maximumTier: 'static', reason: 'saved-static' }),
- quality: 'static',
+ policy: policy({
+ allowSceneRequest: false,
+ maximumTier: "static",
+ reason: "saved-static",
+ }),
+ quality: "static",
});
await nextTick();
- expect(wrapper.findAll('canvas')).toHaveLength(0);
- expect(wrapper.get('img').isVisible()).toBe(true);
+ expect(wrapper.findAll("canvas")).toHaveLength(0);
+ expect(wrapper.get("img").isVisible()).toBe(true);
wrapper.unmount();
});
- it('retains the same canvas and scene generation for a visitor pause and resume', async () => {
+ it("retains the same canvas and scene generation for a visitor pause and resume", async () => {
const wrapper = mount(SceneCanvas, {
props: {
journey: journey(),
policy: policy(),
- quality: 'high',
- sceneDescription: 'A decorative night road behind the portfolio content.',
+ quality: "high",
+ sceneDescription:
+ "A decorative night road behind the portfolio content.",
},
});
await flushPromises();
- const canvas = wrapper.get('canvas').element;
+ const canvas = wrapper.get("canvas").element;
const runtime = await sceneMocks.build.mock.results[0]!.value;
await wrapper.setProps({
- policy: policy({ allowSceneRequest: false, maximumTier: 'static', reason: 'visitor-pause' }),
- quality: 'high',
+ policy: policy({
+ allowSceneRequest: false,
+ maximumTier: "static",
+ reason: "visitor-pause",
+ }),
+ quality: "high",
});
await flushPromises();
- expect(wrapper.get('canvas').element).toBe(canvas);
+ expect(wrapper.get("canvas").element).toBe(canvas);
expect(runtime.dispose).not.toHaveBeenCalled();
expect(sceneMocks.build).toHaveBeenCalledTimes(1);
await wrapper.setProps({ policy: policy() });
await flushPromises();
- expect(wrapper.get('canvas').element).toBe(canvas);
+ expect(wrapper.get("canvas").element).toBe(canvas);
expect(runtime.dispose).not.toHaveBeenCalled();
expect(sceneMocks.build).toHaveBeenCalledTimes(1);
wrapper.unmount();
});
});
-describe('DriveControls', () => {
+describe("DriveControls", () => {
const mountControls = (
motionPolicy: MotionPolicy,
- maximum: QualityTier = 'high',
- audioState: AudioUiState = { kind: 'idle' },
- ) => mount(DriveControls, {
- attachTo: document.body,
- props: { policy: motionPolicy, maximumQuality: maximum, sceneStatus: 'running', audioState },
- });
+ maximum: QualityTier = "high",
+ audioState: AudioUiState = { kind: "idle" },
+ ) =>
+ mount(DriveControls, {
+ attachTo: document.body,
+ props: {
+ policy: motionPolicy,
+ maximumQuality: maximum,
+ sceneStatus: "running",
+ audioState,
+ },
+ });
- it('presents one explicit drive action before audio activation', async () => {
+ it("presents one explicit drive action before audio activation", async () => {
const wrapper = mountControls(policy());
- expect(wrapper.get('[data-drive-state="idle"]').text()).toContain('START THE DRIVE');
+ expect(wrapper.get('[data-drive-state="idle"]').text()).toContain(
+ "START THE DRIVE",
+ );
expect(wrapper.find('[aria-label="Mute sound"]').exists()).toBe(false);
- await wrapper.get('[aria-label="Start the drive"]').trigger('click');
- expect(wrapper.emitted('audioStart')).toHaveLength(1);
+ await wrapper.get('[aria-label="Start the drive"]').trigger("click");
+ expect(wrapper.emitted("audioStart")).toHaveLength(1);
wrapper.unmount();
});
- it('collapses the running state to named vehicle controls and an upward settings panel', async () => {
- const wrapper = mountControls(policy(), 'high', { kind: 'running', muted: false, volume: 0.4 });
+ it("collapses the running state to named vehicle controls and an upward settings panel", async () => {
+ const wrapper = mountControls(policy(), "high", {
+ kind: "running",
+ muted: false,
+ volume: 0.4,
+ });
expect(wrapper.get('[aria-label="Mute sound"]')).toBeTruthy();
expect(wrapper.get('[aria-label="Sound volume"]')).toBeTruthy();
expect(wrapper.get('[aria-label="Pause motion"]')).toBeTruthy();
const settings = wrapper.get('[aria-label="Drive settings"]');
- await settings.trigger('click');
+ await settings.trigger("click");
const panel = wrapper.get('[role="dialog"][aria-label="Drive settings"]');
- expect(panel.text()).not.toContain('Motion: active');
+ expect(panel.text()).not.toContain("Motion: active");
expect(panel.find('[role="status"]').exists()).toBe(false);
- expect(panel.get('select').attributes('aria-label')).toBe('Quality control');
- expect(settings.attributes('aria-expanded')).toBe('true');
+ expect(panel.get("select").attributes("aria-label")).toBe(
+ "Quality control",
+ );
+ expect(settings.attributes("aria-expanded")).toBe("true");
wrapper.unmount();
});
- it('dismisses settings with Escape and outside interaction', async () => {
- const wrapper = mountControls(policy(), 'high', { kind: 'running', muted: false, volume: 0.4 });
+ it("dismisses settings with Escape and outside interaction", async () => {
+ const wrapper = mountControls(policy(), "high", {
+ kind: "running",
+ muted: false,
+ volume: 0.4,
+ });
const settings = wrapper.get('[aria-label="Drive settings"]');
- await settings.trigger('click');
- document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
+ await settings.trigger("click");
+ document.dispatchEvent(
+ new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
+ );
await nextTick();
- expect(wrapper.find('[role="dialog"][aria-label="Drive settings"]').exists()).toBe(false);
+ expect(
+ wrapper.find('[role="dialog"][aria-label="Drive settings"]').exists(),
+ ).toBe(false);
expect(document.activeElement).toBe(settings.element);
- await settings.trigger('click');
- document.body.dispatchEvent(new MouseEvent('pointerdown', { bubbles: true }));
+ await settings.trigger("click");
+ document.body.dispatchEvent(
+ new MouseEvent("pointerdown", { bubbles: true }),
+ );
await nextTick();
- expect(wrapper.find('[role="dialog"][aria-label="Drive settings"]').exists()).toBe(false);
+ expect(
+ wrapper.find('[role="dialog"][aria-label="Drive settings"]').exists(),
+ ).toBe(false);
wrapper.unmount();
});
it.each([
- ['visitor-pause', 'Resume motion'],
- ['saved-static', 'Enable motion'],
- ] as const)('offers the correct recovery action for %s', (reason, label) => {
+ ["visitor-pause", "Resume motion"],
+ ["saved-static", "Enable motion"],
+ ] as const)("offers the correct recovery action for %s", (reason, label) => {
const wrapper = mountControls(
- policy({ allowSceneRequest: false, maximumTier: 'static', reason }),
- 'static',
- { kind: 'running', muted: false, volume: 0.4 },
+ policy({ allowSceneRequest: false, maximumTier: "static", reason }),
+ "static",
+ { kind: "running", muted: false, volume: 0.4 },
);
expect(wrapper.get(`[aria-label="${label}"]`)).toBeTruthy();
wrapper.unmount();
});
-});
-describe('JourneyHud', () => {
- it('renders only route percent, current section, and arrival from one snapshot', () => {
- const wrapper = mount(JourneyHud, {
+ it.each([
+ ["visitor-pause", "Resume motion"],
+ ["saved-static", "Enable motion"],
+ ] as const)(
+ "keeps the %s recovery action available before audio starts",
+ (reason, label) => {
+ const wrapper = mountControls(
+ policy({ allowSceneRequest: false, maximumTier: "static", reason }),
+ "static",
+ );
+ expect(wrapper.get(`[aria-label="${label}"]`)).toBeTruthy();
+ wrapper.unmount();
+ },
+ );
+
+ it("announces a nondefault scene state without requiring the settings panel", () => {
+ const wrapper = mount(DriveControls, {
props: {
- journey: journey({ routeProgress: 1, activeSectionId: 'contact', arrived: true }),
- sectionLabel: 'Contact',
+ policy: policy(),
+ maximumQuality: "high",
+ sceneStatus: "failed",
+ audioState: { kind: "idle" },
},
});
- expect(wrapper.text()).toContain('100%');
- expect(wrapper.text()).toContain('Contact');
- expect(wrapper.text()).toContain('ARRIVED');
- expect(wrapper.text()).not.toContain('Low');
- expect(wrapper.attributes('aria-hidden')).toBe('true');
+ expect(wrapper.find('[role="dialog"]').exists()).toBe(false);
+ expect(wrapper.get('[role="status"]').text()).toBe("Visuals unavailable");
+ });
+});
+
+describe("JourneyHud", () => {
+ it("renders only route percent, current section, and arrival from one snapshot", () => {
+ const wrapper = mount(JourneyHud, {
+ props: {
+ journey: journey({
+ routeProgress: 1,
+ activeSectionId: "contact",
+ arrived: true,
+ }),
+ sectionLabel: "Contact",
+ },
+ });
+
+ expect(wrapper.text()).toContain("100%");
+ expect(wrapper.text()).toContain("Contact");
+ expect(wrapper.text()).toContain("ARRIVED");
+ expect(wrapper.text()).not.toContain("Low");
+ expect(wrapper.attributes("aria-hidden")).toBe("true");
});
});
diff --git a/tests/unit/components.test.ts b/tests/unit/components.test.ts
index 2715ade..0c65a82 100644
--- a/tests/unit/components.test.ts
+++ b/tests/unit/components.test.ts
@@ -1,105 +1,168 @@
-import { mount } from '@vue/test-utils';
-import { describe, expect, it } from 'vitest';
-import ContactDestination from '@/components/ContactDestination.vue';
-import InfrastructureDiagram from '@/components/InfrastructureDiagram.vue';
-import ProjectCaseStudy from '@/components/ProjectCaseStudy.vue';
-import ToolkitGrid from '@/components/ToolkitGrid.vue';
-import type { ContactContent, InfrastructureCaseStudy, PublicProject, ToolkitGroup } from '@/content/types';
+import { mount } from "@vue/test-utils";
+import { describe, expect, it } from "vitest";
+import ContactDestination from "@/components/ContactDestination.vue";
+import InfrastructureDiagram from "@/components/InfrastructureDiagram.vue";
+import ProjectCaseStudy from "@/components/ProjectCaseStudy.vue";
+import ToolkitGrid from "@/components/ToolkitGrid.vue";
+import type {
+ ContactContent,
+ InfrastructureCaseStudy,
+ PublicProject,
+ ToolkitGroup,
+} from "@/content/types";
const infrastructure: InfrastructureCaseStudy = {
- id: 'safe-infrastructure',
- title: 'Secure self-hosted platform and lifecycle',
- summary: 'A compact infrastructure case study.',
+ id: "safe-infrastructure",
+ title: "Secure self-hosted platform and lifecycle",
+ summary: "A compact infrastructure case study.",
bands: [
- { id: 'ingress-tls', title: 'Ingress and TLS', summary: 'Managed encrypted ingress.' },
- { id: 'identity-access', title: 'Identity and access', summary: 'Protected service access.' },
- { id: 'service-lifecycle', title: 'Service lifecycle', summary: 'Managed container lifecycle.' },
- { id: 'operations', title: 'Operations', summary: 'Routine maintenance and investigation.' },
+ {
+ id: "ingress-tls",
+ title: "Ingress and TLS",
+ summary: "Managed encrypted ingress.",
+ },
+ {
+ id: "identity-access",
+ title: "Identity and access",
+ summary: "Protected service access.",
+ },
+ {
+ id: "service-lifecycle",
+ title: "Service lifecycle",
+ summary: "Managed container lifecycle.",
+ },
+ {
+ id: "operations",
+ title: "Operations",
+ summary: "Routine maintenance and investigation.",
+ },
],
};
const privateProject: PublicProject = {
- id: 'private-work',
- title: 'Private self-hosted medication tracker',
- visibility: 'private',
- maturity: 'active',
- summary: 'A private application.',
- problem: 'A recurring workflow needs reliable tracking.',
- contribution: 'Designed and maintained the application.',
- technicalDecisions: ['Used a synchronised client and server design.'],
- outcome: 'The work remains private.',
+ id: "private-work",
+ title: "Private self-hosted medication tracker",
+ visibility: "private",
+ maturity: "active",
+ summary: "A private application.",
+ problem: "A recurring workflow needs reliable tracking.",
+ contribution: "Designed and maintained the application.",
+ technicalDecisions: ["Used a synchronised client and server design."],
+ outcome: "The work remains private.",
assetIds: [],
};
const publicProject: PublicProject = {
...privateProject,
- id: 'public-work',
- title: 'Footy Tracker',
- visibility: 'public',
- url: 'https://github.com/example/footy-tracker',
+ id: "public-work",
+ title: "Footy Tracker",
+ visibility: "public",
+ url: "https://github.com/example/footy-tracker",
};
-describe('semantic section components', () => {
- it('gives the infrastructure graphic an accessible name, description, caption, and visible text equivalent', () => {
+describe("semantic section components", () => {
+ it("gives the infrastructure graphic an accessible name, description, caption, and visible text equivalent", () => {
const wrapper = mount(InfrastructureDiagram, { props: { infrastructure } });
const flow = wrapper.get('[data-testid="infrastructure-flow"]');
- expect(flow.attributes('role')).toBe('img');
- expect(flow.attributes('aria-labelledby')).toBe('safe-infrastructure-diagram-title');
- expect(flow.attributes('aria-describedby')).toBe('safe-infrastructure-diagram-description');
- expect(wrapper.get('figcaption').text()).toContain('Secure self-hosted platform and lifecycle');
- expect(wrapper.get('[data-testid="infrastructure-text-equivalent"]').text()).toContain('Ingress and TLS');
- expect(wrapper.get('[data-testid="infrastructure-text-equivalent"]').text()).toContain('Identity and access');
+ expect(flow.attributes("role")).toBe("img");
+ expect(flow.attributes("aria-labelledby")).toBe(
+ "safe-infrastructure-diagram-title",
+ );
+ expect(flow.attributes("aria-describedby")).toBe(
+ "safe-infrastructure-diagram-description",
+ );
+ expect(wrapper.get("figcaption").text()).toContain(
+ "Secure self-hosted platform and lifecycle",
+ );
+ expect(
+ wrapper.get('[data-testid="infrastructure-text-equivalent"]').text(),
+ ).toContain("Ingress and TLS");
+ expect(
+ wrapper.get('[data-testid="infrastructure-text-equivalent"]').text(),
+ ).toContain("Identity and access");
});
- it('renders the real infrastructure route with explicit trust boundaries and operational support', () => {
+ it("renders the real infrastructure route with explicit trust boundaries and operational support", () => {
const wrapper = mount(InfrastructureDiagram, { props: { infrastructure } });
const flow = wrapper.get('[data-testid="infrastructure-flow"]');
- expect(flow.findAll('[data-infrastructure-node]').map((node) => node.attributes('data-infrastructure-node'))).toEqual([
- 'visitor', 'nginx-tls', 'authentication', 'containerized-services', 'operations',
+ expect(
+ flow
+ .findAll("[data-infrastructure-node]")
+ .map((node) => node.attributes("data-infrastructure-node")),
+ ).toEqual([
+ "visitor",
+ "nginx-tls",
+ "authentication",
+ "containerized-services",
+ "operations",
]);
- expect(flow.findAll('[data-trust-boundary]').map((boundary) => boundary.text())).toEqual([
- 'Public edge', 'Access boundary', 'Service boundary',
- ]);
- expect(flow.text()).toContain('fail2ban guard');
+ expect(
+ flow.findAll("[data-trust-boundary]").map((boundary) => boundary.text()),
+ ).toEqual(["Public edge", "Access boundary", "Service boundary"]);
+ expect(
+ wrapper.get('[aria-label="Infrastructure diagram legend"]').text(),
+ ).toContain("Trust boundary");
+ expect(
+ wrapper.get('[aria-label="Infrastructure diagram legend"]').text(),
+ ).toContain("Request flow");
+ expect(
+ wrapper.get('[data-testid="infrastructure-text-equivalent"]').text(),
+ ).toContain("Boundary labels mark changes in trust or access rules");
+ expect(flow.text()).toContain("fail2ban guard");
});
- it('does not turn private project content into a link container', () => {
- const wrapper = mount(ProjectCaseStudy, { props: { project: privateProject, priority: 'primary' } });
+ it("does not turn private project content into a link container", () => {
+ const wrapper = mount(ProjectCaseStudy, {
+ props: { project: privateProject, priority: "primary" },
+ });
- expect(wrapper.get('article').findAll('a')).toHaveLength(0);
- expect(wrapper.get('h3').text()).toBe('Private self-hosted medication tracker');
- expect(wrapper.get('article').attributes('data-project-priority')).toBe('primary');
+ expect(wrapper.get("article").findAll("a")).toHaveLength(0);
+ expect(wrapper.get("h3").text()).toBe(
+ "Private self-hosted medication tracker",
+ );
+ expect(wrapper.get("article").attributes("data-project-priority")).toBe(
+ "primary",
+ );
});
- it('uses a safe external link only for a public project with an approved URL', () => {
- const wrapper = mount(ProjectCaseStudy, { props: { project: publicProject, priority: 'supporting' } });
- const link = wrapper.get('article a');
+ it("uses a safe external link only for a public project with an approved URL", () => {
+ const wrapper = mount(ProjectCaseStudy, {
+ props: { project: publicProject, priority: "supporting" },
+ });
+ const link = wrapper.get("article a");
- expect(link.attributes('href')).toBe('https://github.com/example/footy-tracker');
- expect(link.attributes('target')).toBe('_blank');
- expect(link.attributes('rel')).toBe('noopener noreferrer');
+ expect(link.attributes("href")).toBe(
+ "https://github.com/example/footy-tracker",
+ );
+ expect(link.attributes("target")).toBe("_blank");
+ expect(link.attributes("rel")).toBe("noopener noreferrer");
});
- it('announces every toolkit technology once while hiding the decorative icon placeholder', () => {
- const groups: readonly ToolkitGroup[] = [{
- title: 'Systems & delivery',
- technologies: [{ name: 'Linux' }],
- }];
+ it("announces every toolkit technology once while hiding the decorative icon placeholder", () => {
+ const groups: readonly ToolkitGroup[] = [
+ {
+ title: "Systems & delivery",
+ technologies: [{ name: "Linux" }],
+ },
+ ];
const wrapper = mount(ToolkitGrid, { props: { groups } });
const tile = wrapper.get('[data-testid="toolkit-technology"]');
- expect(tile.text()).toBe('Linux');
- expect(tile.get('[aria-hidden="true"]').attributes('aria-hidden')).toBe('true');
+ expect(tile.text()).toBe("Linux");
+ expect(tile.get('[aria-hidden="true"]').attributes("aria-hidden")).toBe(
+ "true",
+ );
expect(tile.findAll('[aria-label="Linux"]').length).toBe(0);
});
- it('renders a visible mailto contact and safe verified social links', () => {
+ it("renders a visible mailto contact and safe verified social links", () => {
const contact: ContactContent = {
- email: 'hello@example.test',
- cvMessage: 'For a current CV or role-specific information, contact me directly.',
- social: [{ service: 'GitHub', url: 'https://github.com/example' }],
+ email: "hello@example.test",
+ cvMessage:
+ "For a current CV or role-specific information, contact me directly.",
+ social: [{ service: "GitHub", url: "https://github.com/example" }],
};
const wrapper = mount(ContactDestination, {
props: {
@@ -107,10 +170,14 @@ describe('semantic section components', () => {
},
});
- expect(wrapper.get('a[href="mailto:hello@example.test"]').text()).toBe('Start a conversation');
- expect(wrapper.text()).toContain("Thanks for making the trip. The road's open if you'd like to build something together.");
+ expect(wrapper.get('a[href="mailto:hello@example.test"]').text()).toBe(
+ "Start a conversation",
+ );
+ expect(wrapper.text()).toContain(
+ "Thanks for making the trip. The road's open if you'd like to build something together.",
+ );
const social = wrapper.get('a[href="https://github.com/example"]');
- expect(social.text()).toBe('GitHub');
- expect(social.attributes('rel')).toBe('noopener noreferrer');
+ expect(social.text()).toBe("GitHub");
+ expect(social.attributes("rel")).toBe("noopener noreferrer");
});
});
diff --git a/tests/unit/navigation-controller.test.ts b/tests/unit/navigation-controller.test.ts
index 1d3652a..73ec02d 100644
--- a/tests/unit/navigation-controller.test.ts
+++ b/tests/unit/navigation-controller.test.ts
@@ -152,6 +152,25 @@ describe("canonical fragment validation", () => {
anchor.href = "#unknown";
expect(validateSectionAnchor(anchor, sectionIds)).toBeNull();
});
+
+ it("validates the nested hero H1 as the Start navigation target", () => {
+ const section = document.createElement("section");
+ section.id = "start";
+ const content = document.createElement("div");
+ const heading = document.createElement("h1");
+ heading.id = "start-heading";
+ heading.tabIndex = -1;
+ content.append(heading);
+ section.append(content);
+ const anchor = document.createElement("a");
+ anchor.href = "#start";
+ document.body.append(section, anchor);
+
+ expect(validateSectionAnchor(anchor, sectionIds)).toEqual({
+ id: "start",
+ target: section,
+ });
+ });
});
describe("navigation epochs and effects", () => {
diff --git a/tests/unit/world-models.test.ts b/tests/unit/world-models.test.ts
index 133af1e..7b4a178 100644
--- a/tests/unit/world-models.test.ts
+++ b/tests/unit/world-models.test.ts
@@ -1,27 +1,27 @@
-import { describe, expect, it, vi } from 'vitest';
-import * as THREE from 'three';
-import type { EnvironmentChapter } from '@/journey/types';
-import { renderQualityFor } from '@/scene/render-quality';
-import { SceneResourceBag } from '@/scene/resources';
+import { describe, expect, it, vi } from "vitest";
+import * as THREE from "three";
+import type { EnvironmentChapter } from "@/journey/types";
+import { renderQualityFor } from "@/scene/render-quality";
+import { SceneResourceBag } from "@/scene/resources";
import {
CHAPTER_PALETTES,
createMutableChapterPalette,
writeChapterPalette,
-} from '@/scene/world/palette';
-import { createRoad } from '@/scene/world/road';
-import { createTerrain } from '@/scene/world/terrain';
-import { createSun } from '@/scene/world/sun';
-import { createAtmosphere } from '@/scene/world/atmosphere';
-import { createPostProcessor } from '@/scene/post-processing';
-import { createCar } from '@/scene/world/car';
-import { createChaseCamera } from '@/scene/world/environment';
-import { createSign, routeLabelFor } from '@/scene/world/sign';
+} from "@/scene/world/palette";
+import { createRoad } from "@/scene/world/road";
+import { createTerrain } from "@/scene/world/terrain";
+import { createSun } from "@/scene/world/sun";
+import { createAtmosphere } from "@/scene/world/atmosphere";
+import { createPostProcessor } from "@/scene/post-processing";
+import { createCar } from "@/scene/world/car";
+import { createChaseCamera } from "@/scene/world/environment";
+import { createSign, routeLabelFor } from "@/scene/world/sign";
-describe('procedural world models', () => {
- it('scales geometry and atmosphere work monotonically by live quality tier', () => {
- const high = renderQualityFor('high', 3, false);
- const medium = renderQualityFor('medium', 3, false);
- const low = renderQualityFor('low', 3, false);
+describe("procedural world models", () => {
+ it("scales geometry and atmosphere work monotonically by live quality tier", () => {
+ const high = renderQualityFor("high", 3, false);
+ const medium = renderQualityFor("medium", 3, false);
+ const low = renderQualityFor("low", 3, false);
expect(high.roadSegments).toBeGreaterThan(medium.roadSegments);
expect(medium.roadSegments).toBeGreaterThan(low.roadSegments);
@@ -31,50 +31,60 @@ describe('procedural world models', () => {
expect(low.postEffects).toBe(false);
});
- it('builds a shader road whose motion is entirely uniform-driven', () => {
+ it("builds a shader road whose motion is entirely uniform-driven", () => {
const resources = new SceneResourceBag();
- const road = createRoad(resources, renderQualityFor('high', 1, false));
- const surface = road.group.getObjectByName('road-surface') as ShaderRenderable;
+ const road = createRoad(resources, renderQualityFor("high", 1, false));
+ const surface = road.group.getObjectByName(
+ "road-surface",
+ ) as ShaderRenderable;
expect(surface).toBeInstanceOf(THREE.Mesh);
expect(surface.geometry.index).not.toBeNull();
- expect(surface.geometry.getAttribute('position').count).toBeGreaterThan(1_000);
+ expect(surface.geometry.getAttribute("position").count).toBeGreaterThan(
+ 1_000,
+ );
expect(surface.material.uniforms).toMatchObject({
uDistance: { value: 0 },
uSpeed: { value: 0 },
uAcceleration: { value: 0 },
});
- expect(surface.material.fragmentShader).toContain('centerLane');
- expect(surface.material.fragmentShader).toContain('outerEdge');
+ expect(surface.material.fragmentShader).toContain("centerLane");
+ expect(surface.material.fragmentShader).toContain("outerEdge");
- const positionArray = surface.geometry.getAttribute('position').array;
+ const positionArray = surface.geometry.getAttribute("position").array;
const before = resources.snapshot();
for (let index = 0; index < 300; index += 1) {
road.update(index * 0.5, 0.65, 0.4);
}
expect(resources.snapshot()).toEqual(before);
- expect(surface.geometry.getAttribute('position').array).toBe(positionArray);
+ expect(surface.geometry.getAttribute("position").array).toBe(positionArray);
expect(surface.material.uniforms.uDistance?.value).toBe(149.5);
expect(surface.material.uniforms.uSpeed?.value).toBe(0.65);
expect(surface.material.uniforms.uAcceleration?.value).toBe(0.4);
resources.dispose();
});
- it('builds one wide indexed line terrain with a flat road-center mask', () => {
+ it("builds one wide indexed line terrain with a flat road-center mask", () => {
const resources = new SceneResourceBag();
- const terrain = createTerrain(resources, renderQualityFor('medium', 1, false));
- const lines = terrain.group.getObjectByName('terrain-lines') as ShaderRenderable;
+ const terrain = createTerrain(
+ resources,
+ renderQualityFor("medium", 1, false),
+ );
+ const lines = terrain.group.getObjectByName(
+ "terrain-lines",
+ ) as ShaderRenderable;
expect(lines).toBeInstanceOf(THREE.LineSegments);
expect(lines.geometry.index).not.toBeNull();
lines.geometry.computeBoundingBox();
expect(lines.geometry.boundingBox).not.toBeNull();
expect(
- (lines.geometry.boundingBox?.max.x ?? 0) - (lines.geometry.boundingBox?.min.x ?? 0),
+ (lines.geometry.boundingBox?.max.x ?? 0) -
+ (lines.geometry.boundingBox?.min.x ?? 0),
).toBeGreaterThan(120);
- const roadMask = lines.geometry.getAttribute('roadMask');
+ const roadMask = lines.geometry.getAttribute("roadMask");
expect(roadMask).toBeDefined();
const maskValues = Array.from(roadMask.array as ArrayLike);
expect(Math.min(...maskValues)).toBe(0);
@@ -84,28 +94,28 @@ describe('procedural world models', () => {
uSpeed: { value: 0 },
uRouteProgress: { value: 0 },
});
- expect(lines.material.vertexShader).toContain('multiOctaveNoise');
+ expect(lines.material.vertexShader).toContain("multiOctaveNoise");
const indexArray = lines.geometry.index?.array;
- const positionArray = lines.geometry.getAttribute('position').array;
+ const positionArray = lines.geometry.getAttribute("position").array;
const lowColor = lines.material.uniforms.uLowColor?.value;
const highColor = lines.material.uniforms.uHighColor?.value;
const before = resources.snapshot();
for (let index = 0; index < 300; index += 1) {
- terrain.update(index * 0.4, 0.7, 'builds', index / 299);
+ terrain.update(index * 0.4, 0.7, "builds", index / 299);
}
expect(resources.snapshot()).toEqual(before);
expect(lines.geometry.index?.array).toBe(indexArray);
- expect(lines.geometry.getAttribute('position').array).toBe(positionArray);
+ expect(lines.geometry.getAttribute("position").array).toBe(positionArray);
expect(lines.material.uniforms.uLowColor?.value).toBe(lowColor);
expect(lines.material.uniforms.uHighColor?.value).toBe(highColor);
expect(lines.material.uniforms.uRouteProgress?.value).toBe(1);
resources.dispose();
});
- it('writes deterministic chapter blends into stable color allocations', () => {
- const target = createMutableChapterPalette('deep-night');
+ it("writes deterministic chapter blends into stable color allocations", () => {
+ const target = createMutableChapterPalette("deep-night");
const colorReferences = [
target.sky,
target.fog,
@@ -116,16 +126,20 @@ describe('procedural world models', () => {
];
expect(Object.keys(CHAPTER_PALETTES)).toEqual([
- 'deep-night',
- 'systems',
- 'infrastructure',
- 'builds',
- 'pre-dawn',
- 'destination',
+ "deep-night",
+ "systems",
+ "infrastructure",
+ "builds",
+ "pre-dawn",
+ "destination",
] satisfies EnvironmentChapter[]);
- expect(writeChapterPalette(target, 'deep-night', 'destination', 0.5)).toBe(target);
+ expect(writeChapterPalette(target, "deep-night", "destination", 0.5)).toBe(
+ target,
+ );
const first = paletteSnapshot(target);
- expect(writeChapterPalette(target, 'deep-night', 'destination', 0.5)).toBe(target);
+ expect(writeChapterPalette(target, "deep-night", "destination", 0.5)).toBe(
+ target,
+ );
expect(paletteSnapshot(target)).toEqual(first);
expect([
target.sky,
@@ -135,62 +149,90 @@ describe('procedural world models', () => {
target.sun,
target.haze,
]).toEqual(colorReferences);
- expect(target.warmth).toBeGreaterThan(CHAPTER_PALETTES['deep-night'].warmth);
+ expect(target.warmth).toBeGreaterThan(
+ CHAPTER_PALETTES["deep-night"].warmth,
+ );
expect(target.warmth).toBeLessThan(CHAPTER_PALETTES.destination.warmth);
});
- it('renders the sun as a scan-banded disc with a separately owned halo', () => {
+ it("renders the sun as a scan-banded disc with a separately owned halo", () => {
const resources = new SceneResourceBag();
const sun = createSun(resources);
- const disc = sun.group.getObjectByName('sun-disc') as ShaderRenderable;
- const halo = sun.group.getObjectByName('sun-halo');
+ const disc = sun.group.getObjectByName("sun-disc") as ShaderRenderable;
+ const halo = sun.group.getObjectByName("sun-halo");
expect(disc).toBeInstanceOf(THREE.Mesh);
expect(halo).toBeInstanceOf(THREE.Sprite);
expect(disc.material.uniforms).toMatchObject({
uScanBands: { value: 14 },
- uWarmth: { value: CHAPTER_PALETTES['deep-night'].warmth },
+ uWarmth: { value: CHAPTER_PALETTES["deep-night"].warmth },
});
- expect(disc.material.fragmentShader).toContain('scanBand');
+ expect(disc.material.fragmentShader).toContain("scanBand");
expect(resources.snapshot().owned.texture).toBe(1);
- const palette = createMutableChapterPalette('destination');
+ const palette = createMutableChapterPalette("destination");
sun.update(palette, 1);
expect(disc.material.uniforms.uWarmth?.value).toBe(1);
expect(disc.material.uniforms.uRouteProgress?.value).toBe(1);
resources.dispose();
});
- it('builds deterministic stars, haze layers, and quality-capped speed streaks', () => {
+ it("builds deterministic stars, haze layers, and quality-capped speed streaks", () => {
const highResources = new SceneResourceBag();
const lowResources = new SceneResourceBag();
- const highQuality = renderQualityFor('high', 1, false);
- const lowQuality = renderQualityFor('low', 1, true);
+ const highQuality = renderQualityFor("high", 1, false);
+ const lowQuality = renderQualityFor("low", 1, true);
const high = createAtmosphere(highResources, highQuality);
const low = createAtmosphere(lowResources, lowQuality);
- const highStars = high.group.getObjectByName('atmosphere-stars') as ShaderRenderable;
- const lowStars = low.group.getObjectByName('atmosphere-stars') as ShaderRenderable;
- const highStreaks = high.group.getObjectByName('atmosphere-streaks') as ShaderRenderable;
- const lowStreaks = low.group.getObjectByName('atmosphere-streaks') as ShaderRenderable;
+ const highStars = high.group.getObjectByName(
+ "atmosphere-stars",
+ ) as ShaderRenderable;
+ const lowStars = low.group.getObjectByName(
+ "atmosphere-stars",
+ ) as ShaderRenderable;
+ const highStreaks = high.group.getObjectByName(
+ "atmosphere-streaks",
+ ) as ShaderRenderable;
+ const lowStreaks = low.group.getObjectByName(
+ "atmosphere-streaks",
+ ) as ShaderRenderable;
- expect(highStars.geometry.getAttribute('position').count).toBe(highQuality.starCount);
- expect(lowStars.geometry.getAttribute('position').count).toBe(lowQuality.starCount);
+ expect(highStars.geometry.getAttribute("position").count).toBe(
+ highQuality.starCount,
+ );
+ expect(lowStars.geometry.getAttribute("position").count).toBe(
+ lowQuality.starCount,
+ );
expect(highStars.material.uniforms).toMatchObject({
uPointScale: { value: 18 },
uMaxPointSize: { value: 4.5 },
});
- expect(highStreaks.geometry.getAttribute('position').count).toBe(highQuality.streakCount * 2);
- expect(lowStreaks.geometry.getAttribute('position').count).toBe(lowQuality.streakCount * 2);
- expect(high.group.getObjectByName('horizon-haze-near')).toBeInstanceOf(THREE.Mesh);
- expect(high.group.getObjectByName('horizon-haze-far')).toBeInstanceOf(THREE.Mesh);
+ expect(highStreaks.geometry.getAttribute("position").count).toBe(
+ highQuality.streakCount * 2,
+ );
+ expect(lowStreaks.geometry.getAttribute("position").count).toBe(
+ lowQuality.streakCount * 2,
+ );
+ expect(high.group.getObjectByName("horizon-haze-near")).toBeInstanceOf(
+ THREE.Mesh,
+ );
+ expect(high.group.getObjectByName("horizon-haze-far")).toBeInstanceOf(
+ THREE.Mesh,
+ );
- const highStarPositions = Array.from(highStars.geometry.getAttribute('position').array);
+ const highStarPositions = Array.from(
+ highStars.geometry.getAttribute("position").array,
+ );
const secondResources = new SceneResourceBag();
const secondHigh = createAtmosphere(secondResources, highQuality);
- const secondStars = secondHigh.group.getObjectByName('atmosphere-stars') as ShaderRenderable;
- expect(Array.from(secondStars.geometry.getAttribute('position').array)).toEqual(highStarPositions);
+ const secondStars = secondHigh.group.getObjectByName(
+ "atmosphere-stars",
+ ) as ShaderRenderable;
+ expect(
+ Array.from(secondStars.geometry.getAttribute("position").array),
+ ).toEqual(highStarPositions);
- const palette = createMutableChapterPalette('builds');
+ const palette = createMutableChapterPalette("builds");
const before = highResources.snapshot();
for (let index = 0; index < 300; index += 1) {
high.update(index, 0.7, 0.8, palette);
@@ -204,25 +246,70 @@ describe('procedural world models', () => {
secondResources.dispose();
});
- it('disposes a composer and its passes exactly once', () => {
+ it("disposes a composer and its passes exactly once", () => {
const resources = new SceneResourceBag();
+ const target = (width: number, height: number, depthBuffer: boolean) => ({
+ width,
+ height,
+ depthBuffer,
+ samples: 1,
+ dispose: vi.fn(),
+ });
const composer = {
addPass: vi.fn(),
setPixelRatio: vi.fn(),
setSize: vi.fn(),
render: vi.fn(),
dispose: vi.fn(),
+ renderTarget1: target(1_500, 1_000, true),
+ renderTarget2: target(1_500, 1_000, true),
};
- const renderPass = { scene: null, camera: null, setSize: vi.fn(), dispose: vi.fn() };
- const bloomPass = { setSize: vi.fn(), dispose: vi.fn() };
- const processor = createPostProcessor(resources, {}, {
- createComposer: () => composer,
- createRenderPass: () => renderPass,
- createBloomPass: () => bloomPass,
- });
+ const renderPass = {
+ scene: null,
+ camera: null,
+ setSize: vi.fn(),
+ dispose: vi.fn(),
+ };
+ const bloomPass = {
+ setSize: vi.fn(),
+ dispose: vi.fn(),
+ renderTargetBright: target(750, 500, false),
+ renderTargetsHorizontal: [target(375, 250, false)],
+ renderTargetsVertical: [target(375, 250, false)],
+ };
+ const processor = createPostProcessor(
+ resources,
+ {},
+ {
+ createComposer: () => composer,
+ createRenderPass: () => renderPass,
+ createBloomPass: () => bloomPass,
+ },
+ );
processor.resize(1_200, 800, 1.25);
processor.render({}, {});
+ const allocations = resources.gpuBudgetSnapshot({
+ tier: "high",
+ defaultFramebuffer: {
+ width: 1_500,
+ height: 1_000,
+ bytesPerPixel: 4,
+ attachments: 2,
+ samples: 4,
+ },
+ });
+ expect(
+ allocations.renderTargets.map((allocation) => allocation.id),
+ ).toEqual(
+ expect.arrayContaining([
+ "composer-target-1",
+ "composer-target-2",
+ "bloom-bright",
+ "bloom-horizontal-0",
+ "bloom-vertical-0",
+ ]),
+ );
processor.dispose();
processor.dispose();
resources.dispose();
@@ -236,19 +323,19 @@ describe('procedural world models', () => {
expect(composer.dispose).toHaveBeenCalledOnce();
});
- it('keeps the grand tourer anatomy readable in desktop and portrait chase views', () => {
+ it("keeps the grand tourer anatomy readable in desktop and portrait chase views", () => {
const resources = new SceneResourceBag();
const car = createCar(resources);
for (const name of [
- 'rear-silhouette',
- 'rear-body-face',
- 'rear-light-bar',
- 'rear-glass',
- 'rear-glass-panel',
- 'rear-fascia',
- 'rear-wheel-left',
- 'rear-wheel-right',
- 'car-underglow',
+ "rear-silhouette",
+ "rear-body-face",
+ "rear-light-bar",
+ "rear-glass",
+ "rear-glass-panel",
+ "rear-fascia",
+ "rear-wheel-left",
+ "rear-wheel-right",
+ "car-underglow",
]) {
expect(car.group.getObjectByName(name), name).toBeDefined();
}
@@ -262,23 +349,26 @@ describe('procedural world models', () => {
expect(desktop.minY).toBeGreaterThan(-0.99);
expect(desktop.maxY).toBeLessThan(-0.05);
- car.setDetail('reduced');
+ car.setDetail("reduced");
const portrait = projectedBounds(car.group, createChaseCamera(390 / 844));
expect(portrait.minX).toBeGreaterThan(-0.99);
expect(portrait.maxX).toBeLessThan(0.99);
expect(portrait.minY).toBeGreaterThan(-0.99);
expect(portrait.maxY).toBeLessThan(0.16);
- expect((car.group.getObjectByName('secondary-detail') as { visible: boolean }).visible).toBe(false);
+ expect(
+ (car.group.getObjectByName("secondary-detail") as { visible: boolean })
+ .visible,
+ ).toBe(false);
resources.dispose();
});
- it('damps squat and light response without allocating or changing the scene graph', () => {
+ it("damps squat and light response without allocating or changing the scene graph", () => {
const resources = new SceneResourceBag();
const car = createCar(resources);
- const light = car.group.getObjectByName('rear-light-bar') as {
+ const light = car.group.getObjectByName("rear-light-bar") as {
material: { emissiveIntensity: number };
};
- const underglow = car.group.getObjectByName('car-underglow') as {
+ const underglow = car.group.getObjectByName("car-underglow") as {
material: { uniforms: Record };
};
car.update(0.3, 0, 0);
@@ -292,28 +382,36 @@ describe('procedural world models', () => {
car.update(0.92, 1, frame / 60);
}
- expect((car.group as { position: { y: number } }).position.y).toBeLessThan(restingY);
+ expect((car.group as { position: { y: number } }).position.y).toBeLessThan(
+ restingY,
+ );
expect(light.material.emissiveIntensity).toBeGreaterThan(restingLight);
- expect(underglow.material.uniforms.uOpacity?.value).toBeGreaterThan(restingGlow);
- expect(Math.abs((car.group as { rotation: { z: number } }).rotation.z)).toBeLessThanOrEqual(0.006);
- expect((car.group as { children: unknown[] }).children).toHaveLength(childCount);
+ expect(underglow.material.uniforms.uOpacity?.value).toBeGreaterThan(
+ restingGlow,
+ );
+ expect(
+ Math.abs((car.group as { rotation: { z: number } }).rotation.z),
+ ).toBeLessThanOrEqual(0.006);
+ expect((car.group as { children: unknown[] }).children).toHaveLength(
+ childCount,
+ );
expect(resources.snapshot()).toEqual(allocation);
resources.dispose();
});
- it('renders one physical roadside sign and updates its plain label once per target', () => {
+ it("renders one physical roadside sign and updates its plain label once per target", () => {
const resources = new SceneResourceBag();
const sign = createSign(resources);
for (const name of [
- 'sign-backplate',
- 'sign-face',
- 'sign-frame',
- 'sign-post-left',
- 'sign-post-right',
+ "sign-backplate",
+ "sign-face",
+ "sign-frame",
+ "sign-post-left",
+ "sign-post-right",
]) {
expect(sign.group.getObjectByName(name), name).toBeDefined();
}
- const face = sign.group.getObjectByName('sign-face') as {
+ const face = sign.group.getObjectByName("sign-face") as {
userData: { textureUpdateCount: number };
material: { opacity: number };
};
@@ -322,18 +420,18 @@ describe('procedural world models', () => {
sign.update(null);
expect((sign.group as { visible: boolean }).visible).toBe(false);
sign.update({
- targetId: 'infrastructure',
- phase: 'anticipate',
+ targetId: "infrastructure",
+ phase: "anticipate",
progress: 0,
opacity: 0,
});
expect((sign.group as { visible: boolean }).visible).toBe(false);
expect(face.userData.textureUpdateCount).toBe(1);
- expect(routeLabelFor('infrastructure')).toBe('Infrastructure');
+ expect(routeLabelFor("infrastructure")).toBe("Infrastructure");
sign.update({
- targetId: 'infrastructure',
- phase: 'approach',
+ targetId: "infrastructure",
+ phase: "approach",
progress: 0.5,
opacity: 0.7,
});
@@ -344,20 +442,24 @@ describe('procedural world models', () => {
const desktopX = (sign.group as { position: { x: number } }).position.x;
sign.setViewportAspect(0.6);
sign.update({
- targetId: 'selected-builds',
- phase: 'dwell',
+ targetId: "selected-builds",
+ phase: "dwell",
progress: 1,
opacity: 1,
});
expect(face.userData.textureUpdateCount).toBe(2);
- expect(routeLabelFor('selected-builds')).toBe('Selected builds');
- expect((sign.group as { position: { x: number } }).position.x).toBeLessThan(desktopX);
+ expect(routeLabelFor("selected-builds")).toBe("Selected builds");
+ expect((sign.group as { position: { x: number } }).position.x).toBeLessThan(
+ desktopX,
+ );
expect(resources.snapshot()).toEqual(allocation);
resources.dispose();
});
});
-function paletteSnapshot(palette: ReturnType) {
+function paletteSnapshot(
+ palette: ReturnType,
+) {
return {
sky: palette.sky.getHex(),
fog: palette.fog.getHex(),
+ Access boundary
02 / verify
Authentication
Protected access
-
- Service boundary
+
+ Service boundary
03 / run
Containerized services
Managed workloads
-
-
+
+
04 / sustain
Operations
- Lifecycle + maintenance
+ Lifecycle + maintenance
-
+
- + 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. +
- - {{ band.title }}: {{ band.summary }} + {{ band.title }}: {{ band.summary }}