test: lock journey lifecycle and static fallback
This commit is contained in:
35
tests/browser/README.md
Normal file
35
tests/browser/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Browser characterization tests
|
||||
|
||||
These Playwright tests lock the observable portfolio behavior at the browser
|
||||
boundary. They exercise the built `dist/` output through the local preview
|
||||
server rather than replacing Vue, Three.js, WebGL, or Web Audio with production
|
||||
test hooks.
|
||||
|
||||
## Run
|
||||
|
||||
Build the preview artifacts, then run the whole browser suite:
|
||||
|
||||
```bash
|
||||
npm run build:preview
|
||||
npm run test:browser
|
||||
```
|
||||
|
||||
Run only the Phase 2 journey gate:
|
||||
|
||||
```bash
|
||||
npm run test:browser -- tests/browser/journey-integration.spec.ts tests/browser/reduced-motion.spec.ts tests/browser/static-fallback.spec.ts tests/browser/bfcache-lifecycle.spec.ts
|
||||
```
|
||||
|
||||
## Add a case
|
||||
|
||||
1. Name the concrete regression the case catches.
|
||||
2. Use literal inputs and expected browser-visible results.
|
||||
3. Install `scene-instrumentation.ts` before navigation when resource counts or
|
||||
platform calls are the observable boundary.
|
||||
4. Keep failure injection in `page.addInitScript()` or Playwright routing; do
|
||||
not add a test API to the application bundle.
|
||||
5. Run the new case against both configured Chromium profiles, then run the
|
||||
full suite.
|
||||
|
||||
Never copy credentials, private project identifiers, or private URLs into a
|
||||
fixture. Use clearly fake values when a credential-shaped input is required.
|
||||
57
tests/browser/bfcache-lifecycle.spec.ts
Normal file
57
tests/browser/bfcache-lifecycle.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import {
|
||||
dispatchPersistedPageTransition,
|
||||
installSceneInstrumentation,
|
||||
readSceneInstrumentation,
|
||||
} 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 ({
|
||||
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();
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
const state = await readSceneInstrumentation(page);
|
||||
expect(state.maximumCanvasCount).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 ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
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(0);
|
||||
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();
|
||||
|
||||
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);
|
||||
});
|
||||
101
tests/browser/journey-integration.spec.ts
Normal file
101
tests/browser/journey-integration.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import {
|
||||
installSceneInstrumentation,
|
||||
readSceneInstrumentation,
|
||||
waitForOneAnimationFrame,
|
||||
} from '../helpers/scene-instrumentation';
|
||||
|
||||
const sections = [
|
||||
{ id: 'start', label: 'Start' },
|
||||
{ id: 'profile', label: 'Profile' },
|
||||
{ id: 'experience', label: 'Experience' },
|
||||
{ id: 'infrastructure', label: 'Infrastructure' },
|
||||
{ id: 'selected-builds', label: 'Selected builds' },
|
||||
{ id: 'additional-work', label: 'Additional work' },
|
||||
{ id: 'toolkit', label: 'Toolkit' },
|
||||
{ id: 'contact', label: 'Contact' },
|
||||
] as const;
|
||||
|
||||
test.beforeEach(async ({ page }) => installSceneInstrumentation(page));
|
||||
|
||||
test('keeps navigation, HUD, progress, chapter, and arrival on the same next-frame section snapshot', async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', /running|failed|static/u);
|
||||
const chapterColors: string[] = [];
|
||||
|
||||
for (const section of sections) {
|
||||
await page.evaluate((id) => {
|
||||
const heading = document.getElementById(`${id}-heading`);
|
||||
if (!(heading instanceof HTMLElement)) throw new Error(`Missing heading: ${id}`);
|
||||
const top = heading.getBoundingClientRect().top + window.scrollY;
|
||||
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
||||
const destination = id === 'contact'
|
||||
? document.documentElement.scrollHeight
|
||||
: Math.max(0, top - viewportHeight * 0.35 + 1);
|
||||
window.scrollTo({ top: destination, behavior: 'instant' });
|
||||
}, section.id);
|
||||
await waitForOneAnimationFrame(page);
|
||||
|
||||
await expect(page.locator(`nav a[href="#${section.id}"]`)).toHaveAttribute('aria-current', 'page');
|
||||
await expect(page.locator('.journey-hud__section')).toHaveText(section.label);
|
||||
const routeText = await page.locator('.journey-hud__route').textContent();
|
||||
expect(routeText).toMatch(/^ROUTE (?:100|[0-9]{1,2})%$/u);
|
||||
if (section.id === 'contact') {
|
||||
await expect(page.locator('.journey-hud__arrival')).toHaveText('ARRIVED');
|
||||
await expect(page.locator('.journey-hud__route')).toHaveText('ROUTE 100%');
|
||||
} else {
|
||||
await expect(page.locator('.journey-hud__arrival')).toHaveCount(0);
|
||||
}
|
||||
// The section snapshot is emitted in the scroll RAF above; the scene's
|
||||
// already-running loop must consume it no later than its next RAF.
|
||||
await waitForOneAnimationFrame(page);
|
||||
const clearColors = (await readSceneInstrumentation(page)).clearColors.filter((color) => color !== '#000000');
|
||||
const latestChapterColor = clearColors.at(-1);
|
||||
if (latestChapterColor === undefined) throw new Error(`Missing rendered chapter color for ${section.id}`);
|
||||
chapterColors.push(latestChapterColor);
|
||||
}
|
||||
|
||||
const instrumentation = await readSceneInstrumentation(page);
|
||||
expect(instrumentation.maximumCanvasCount).toBeLessThanOrEqual(1);
|
||||
const expectedChapterColors = (page.viewportSize()?.width ?? 0) < 768
|
||||
? [
|
||||
'#040713', '#040713', '#060918', '#080b1d',
|
||||
'#0d1025', '#0d1025', '#18152c', '#2b2035',
|
||||
]
|
||||
: [
|
||||
'#000102', '#000102', '#000102', '#010103',
|
||||
'#010105', '#010105', '#020206', '#060409',
|
||||
];
|
||||
expect(chapterColors).toEqual(expectedChapterColors);
|
||||
});
|
||||
|
||||
test('draws only the independently eligible next roadside sign during passive forward travel', async ({
|
||||
page,
|
||||
distUrl,
|
||||
}) => {
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
await expect(page.locator('.journey-world')).toHaveAttribute('data-scene-state', 'running');
|
||||
|
||||
const oracle = await page.evaluate(() => {
|
||||
const profile = document.getElementById('profile-heading');
|
||||
if (!(profile instanceof HTMLElement)) throw new Error('Missing profile heading');
|
||||
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
||||
const headingTop = profile.getBoundingClientRect().top + window.scrollY;
|
||||
const destination = Math.max(1, headingTop - viewportHeight * (0.35 + 0.8));
|
||||
return { destination, viewportHeight, expectedLabel: '01 / PROFILE' };
|
||||
});
|
||||
await page.evaluate(() => window.dispatchEvent(new WheelEvent('wheel', { deltaY: 1 })));
|
||||
for (let y = 1; y < oracle.destination; y += oracle.viewportHeight * 0.5) {
|
||||
await page.evaluate((nextY) => window.scrollTo(0, nextY), y);
|
||||
await waitForOneAnimationFrame(page);
|
||||
}
|
||||
await page.evaluate((destination) => window.scrollTo(0, destination), oracle.destination);
|
||||
await waitForOneAnimationFrame(page);
|
||||
|
||||
await expect.poll(async () => (await readSceneInstrumentation(page)).routeLabelsDrawn).toContain(oracle.expectedLabel);
|
||||
const labels = (await readSceneInstrumentation(page)).routeLabelsDrawn;
|
||||
expect([...new Set(labels)]).toEqual([oracle.expectedLabel]);
|
||||
});
|
||||
40
tests/browser/reduced-motion.spec.ts
Normal file
40
tests/browser/reduced-motion.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import {
|
||||
environmentChunkPattern,
|
||||
installSceneInstrumentation,
|
||||
readSceneInstrumentation,
|
||||
} from '../helpers/scene-instrumentation';
|
||||
|
||||
test.beforeEach(async ({ page }) => installSceneInstrumentation(page));
|
||||
|
||||
test('boots reduced motion without requesting the live scene or automatic media', async ({ page, distUrl }) => {
|
||||
const requests: string[] = [];
|
||||
page.on('request', (request) => requests.push(request.url()));
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await expect(page.locator('.journey-poster')).toHaveCount(1);
|
||||
await expect(page.locator('.journey-canvas')).toHaveCount(0);
|
||||
await expect(page.locator('.journey-hud')).toBeVisible();
|
||||
await expect(page.locator('nav[aria-label="Portfolio sections"]')).toBeVisible();
|
||||
await expect(page.locator('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
await expect(page.getByRole('status')).toContainText('reduced motion requested');
|
||||
expect(requests.some((url) => environmentChunkPattern.test(url))).toBe(false);
|
||||
await expect(page.locator('video[src], video source[src]')).toHaveCount(0);
|
||||
expect((await readSceneInstrumentation(page)).routeLabelsDrawn).toEqual([]);
|
||||
});
|
||||
|
||||
test('requires explicit Enable motion after the system stops requesting reduction', async ({ page, distUrl }) => {
|
||||
const requests: string[] = [];
|
||||
page.on('request', (request) => requests.push(request.url()));
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' });
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
|
||||
await page.emulateMedia({ reducedMotion: 'no-preference' });
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Enable motion' })).toBeVisible();
|
||||
expect(requests.some((url) => environmentChunkPattern.test(url))).toBe(false);
|
||||
|
||||
await page.getByRole('button', { name: 'Enable motion' }).click();
|
||||
await expect.poll(() => requests.some((url) => environmentChunkPattern.test(url))).toBe(true);
|
||||
});
|
||||
68
tests/browser/static-fallback.spec.ts
Normal file
68
tests/browser/static-fallback.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { expect, test } from '../helpers/serve-dist';
|
||||
import {
|
||||
environmentChunkPattern,
|
||||
installSceneInstrumentation,
|
||||
readSceneInstrumentation,
|
||||
} 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 ({
|
||||
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' });
|
||||
|
||||
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('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([]);
|
||||
expect(pageErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test('contains a WebGL initialization failure without hiding the portfolio', async ({ page, distUrl }) => {
|
||||
const pageErrors: string[] = [];
|
||||
page.on('pageerror', (error) => pageErrors.push(error.message));
|
||||
await page.addInitScript(() => {
|
||||
const nativeGetContext = 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;
|
||||
},
|
||||
});
|
||||
});
|
||||
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('#contact a[href^="mailto:"]')).toBeVisible();
|
||||
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 }) => {
|
||||
const requests: string[] = [];
|
||||
page.on('request', (request) => requests.push(request.url()));
|
||||
await page.goto(distUrl, { waitUntil: 'domcontentloaded' });
|
||||
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.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);
|
||||
});
|
||||
160
tests/helpers/scene-instrumentation.ts
Normal file
160
tests/helpers/scene-instrumentation.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
export const environmentChunkPattern = /\/assets\/environment-[^/]+\.js(?:[?#].*)?$/u;
|
||||
|
||||
export interface SceneInstrumentationSnapshot {
|
||||
activeAnimationFrames: number;
|
||||
maximumCanvasCount: number;
|
||||
audioContextsConstructed: number;
|
||||
liveAudioContexts: number;
|
||||
routeLabelsDrawn: string[];
|
||||
clearColors: string[];
|
||||
unhandledRejections: string[];
|
||||
}
|
||||
|
||||
type InstrumentedWindow = Window & {
|
||||
__portfolioJourneyInstrumentation?: SceneInstrumentationSnapshot & {
|
||||
animationFrameIds: Set<number>;
|
||||
audioContexts: AudioContext[];
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Installs browser-owned observers before application code runs. The helper
|
||||
* patches platform boundaries only; no test API is compiled into the site.
|
||||
*/
|
||||
export async function installSceneInstrumentation(page: Page): Promise<void> {
|
||||
await page.addInitScript(() => {
|
||||
const instrumentedWindow = window as InstrumentedWindow;
|
||||
const state: NonNullable<InstrumentedWindow['__portfolioJourneyInstrumentation']> = {
|
||||
activeAnimationFrames: 0,
|
||||
maximumCanvasCount: 0,
|
||||
audioContextsConstructed: 0,
|
||||
liveAudioContexts: 0,
|
||||
routeLabelsDrawn: [],
|
||||
clearColors: [],
|
||||
unhandledRejections: [],
|
||||
animationFrameIds: new Set<number>(),
|
||||
audioContexts: [],
|
||||
};
|
||||
Object.defineProperty(instrumentedWindow, '__portfolioJourneyInstrumentation', {
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
value: state,
|
||||
writable: false,
|
||||
});
|
||||
|
||||
const nativeRequestAnimationFrame = window.requestAnimationFrame.bind(window);
|
||||
const nativeCancelAnimationFrame = window.cancelAnimationFrame.bind(window);
|
||||
window.requestAnimationFrame = (callback: FrameRequestCallback): number => {
|
||||
let id = 0;
|
||||
id = nativeRequestAnimationFrame((now) => {
|
||||
state.animationFrameIds.delete(id);
|
||||
state.activeAnimationFrames = state.animationFrameIds.size;
|
||||
callback(now);
|
||||
});
|
||||
state.animationFrameIds.add(id);
|
||||
state.activeAnimationFrames = state.animationFrameIds.size;
|
||||
return id;
|
||||
};
|
||||
window.cancelAnimationFrame = (id: number): void => {
|
||||
state.animationFrameIds.delete(id);
|
||||
state.activeAnimationFrames = state.animationFrameIds.size;
|
||||
nativeCancelAnimationFrame(id);
|
||||
};
|
||||
|
||||
const updateCanvasCount = (): void => {
|
||||
state.maximumCanvasCount = Math.max(
|
||||
state.maximumCanvasCount,
|
||||
document.querySelectorAll('.journey-canvas').length,
|
||||
);
|
||||
};
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
updateCanvasCount();
|
||||
new MutationObserver(updateCanvasCount).observe(document.documentElement, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
}, { once: true });
|
||||
|
||||
const nativeFillText = CanvasRenderingContext2D.prototype.fillText;
|
||||
CanvasRenderingContext2D.prototype.fillText = function fillText(
|
||||
text: string,
|
||||
x: number,
|
||||
y: number,
|
||||
maxWidth?: number,
|
||||
): void {
|
||||
if (/^0[0-7] \/ /u.test(text)) state.routeLabelsDrawn.push(text);
|
||||
if (maxWidth === undefined) nativeFillText.call(this, text, x, y);
|
||||
else nativeFillText.call(this, text, x, y, maxWidth);
|
||||
};
|
||||
|
||||
const colorChannel = (value: number): string => Math.round(value * 255)
|
||||
.toString(16)
|
||||
.padStart(2, '0');
|
||||
const patchClearColor = (prototype: WebGLRenderingContext | WebGL2RenderingContext): void => {
|
||||
const nativeClearColor = prototype.clearColor;
|
||||
prototype.clearColor = function clearColor(red, green, blue, alpha): void {
|
||||
state.clearColors.push(`#${colorChannel(red)}${colorChannel(green)}${colorChannel(blue)}`);
|
||||
nativeClearColor.call(this, red, green, blue, alpha);
|
||||
};
|
||||
};
|
||||
if (typeof WebGLRenderingContext !== 'undefined') patchClearColor(WebGLRenderingContext.prototype);
|
||||
if (typeof WebGL2RenderingContext !== 'undefined') patchClearColor(WebGL2RenderingContext.prototype);
|
||||
|
||||
const NativeAudioContext = window.AudioContext;
|
||||
if (NativeAudioContext) {
|
||||
class InstrumentedAudioContext extends NativeAudioContext {
|
||||
constructor(options?: AudioContextOptions) {
|
||||
super(options);
|
||||
state.audioContextsConstructed += 1;
|
||||
state.audioContexts.push(this);
|
||||
state.liveAudioContexts = state.audioContexts.filter((context) => context.state !== 'closed').length;
|
||||
this.addEventListener('statechange', () => {
|
||||
state.liveAudioContexts = state.audioContexts.filter((context) => context.state !== 'closed').length;
|
||||
});
|
||||
}
|
||||
}
|
||||
Object.defineProperty(window, 'AudioContext', {
|
||||
configurable: true,
|
||||
value: InstrumentedAudioContext,
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
const reason = event.reason;
|
||||
state.unhandledRejections.push(reason instanceof Error ? reason.message : String(reason));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function readSceneInstrumentation(page: Page): Promise<SceneInstrumentationSnapshot> {
|
||||
return page.evaluate(() => {
|
||||
const state = (window as InstrumentedWindow).__portfolioJourneyInstrumentation;
|
||||
if (!state) throw new Error('Scene instrumentation was not installed before navigation.');
|
||||
return {
|
||||
activeAnimationFrames: state.animationFrameIds.size,
|
||||
maximumCanvasCount: state.maximumCanvasCount,
|
||||
audioContextsConstructed: state.audioContextsConstructed,
|
||||
liveAudioContexts: state.audioContexts.filter((context) => context.state !== 'closed').length,
|
||||
routeLabelsDrawn: [...state.routeLabelsDrawn],
|
||||
clearColors: [...state.clearColors],
|
||||
unhandledRejections: [...state.unhandledRejections],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function waitForOneAnimationFrame(page: Page): Promise<void> {
|
||||
await page.evaluate(() => new Promise<void>((resolve) => {
|
||||
window.requestAnimationFrame(() => queueMicrotask(resolve));
|
||||
}));
|
||||
}
|
||||
|
||||
export async function dispatchPersistedPageTransition(
|
||||
page: Page,
|
||||
type: 'pagehide' | 'pageshow',
|
||||
): Promise<void> {
|
||||
await page.evaluate((eventType) => {
|
||||
window.dispatchEvent(new PageTransitionEvent(eventType, { persisted: true }));
|
||||
}, type);
|
||||
}
|
||||
@@ -200,7 +200,7 @@ describe('useMotionPreferences', () => {
|
||||
connection.saveData = true;
|
||||
connection.dispatchEvent(new Event('change'));
|
||||
await nextTick();
|
||||
expect(wrapper.vm.policy).toMatchObject({ reason: 'data-saver', allowSceneRequest: false });
|
||||
expect(wrapper.vm.policy).toMatchObject({ reason: 'saved-static', allowSceneRequest: false });
|
||||
|
||||
connection.saveData = false;
|
||||
connection.dispatchEvent(new Event('change'));
|
||||
@@ -209,6 +209,10 @@ describe('useMotionPreferences', () => {
|
||||
coarseQuery.dispatchEvent(new Event('change'));
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
await nextTick();
|
||||
expect(wrapper.vm.policy).toMatchObject({ reason: 'saved-static', maximumTier: 'static' });
|
||||
|
||||
wrapper.vm.setMaximumQuality('high');
|
||||
await nextTick();
|
||||
expect(wrapper.vm.policy).toMatchObject({ reason: 'full', maximumTier: 'low' });
|
||||
|
||||
wrapper.unmount();
|
||||
|
||||
Reference in New Issue
Block a user