fix: close final portfolio quality gaps

This commit is contained in:
2026-09-02 17:38:11 +03:00
parent 0899b6b00f
commit b4728ceef0
24 changed files with 1822 additions and 889 deletions

View File

@@ -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> = {}): JourneySnapshot => ({
const journey = (
overrides: Partial<JourneySnapshot> = {},
): 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> = {}): JourneySnapshot => (
const policy = (overrides: Partial<MotionPolicy> = {}): 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<number, FrameRequestCallback>();
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: '<main><h1>Semantic portfolio</h1></main>' },
slots: { default: "<main><h1>Semantic portfolio</h1></main>" },
});
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");
});
});

View File

@@ -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");
});
});

View File

@@ -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", () => {

View File

@@ -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<number>);
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<string, { value: number }> };
};
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<typeof createMutableChapterPalette>) {
function paletteSnapshot(
palette: ReturnType<typeof createMutableChapterPalette>,
) {
return {
sky: palette.sky.getHex(),
fog: palette.fog.getHex(),