declare const __PORTFOLIO_TEST_BUILD__: boolean; declare const __PORTFOLIO_TEST_TOKEN_DIGEST__: string; const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__; const TEST_TOKEN_DIGEST = typeof __PORTFOLIO_TEST_TOKEN_DIGEST__ === 'undefined' ? '' : __PORTFOLIO_TEST_TOKEN_DIGEST__; export const faultIds = [ 'F1-scene-chunk', 'F1-shader', 'F1-car', 'F1-texture', 'F2-first-frame-timeout', 'F3-post-first-frame-exception', 'F4-context-loss-restore', 'F5-hidden-context-loss', 'F6-repeated-context-loss', 'F7-poster-failure', 'F8-video-failure', 'F9-hydration-failure', 'F10-audio-start-rejection', 'F11-audio-resume-rejection', 'F12-audio-graph-invalid', 'F13-audio-suspend-rejection', ] as const; export type PortfolioFaultId = typeof faultIds[number]; export type PortfolioFaultAction = 'context-loss' | 'context-restore' | 'audio-interruption'; interface TestHookSnapshot { readonly fault: PortfolioFaultId; readonly events: readonly string[]; readonly sceneState: string | null; readonly canvasCount: number; readonly posterCount: number; readonly staticFallbackCount: number; readonly audioStatus: string | null; readonly videoStatus: string | null; } interface PortfolioTestHooks { readonly version: 'portfolio-fault-hooks/v1'; readonly fault: PortfolioFaultId; trigger(action: PortfolioFaultAction): void; snapshot(): TestHookSnapshot; } declare global { interface Window { __PORTFOLIO_TEST_HOOKS__?: PortfolioTestHooks; } } const faultSet = new Set(faultIds); const allowedActions: Readonly>> = { 'F4-context-loss-restore': ['context-loss', 'context-restore'], 'F5-hidden-context-loss': ['context-loss', 'context-restore'], 'F6-repeated-context-loss': ['context-loss', 'context-restore'], 'F11-audio-resume-rejection': ['audio-interruption'], 'F12-audio-graph-invalid': ['audio-interruption'], }; let activeFault: PortfolioFaultId | null = null; const events: string[] = []; const digest = async (value: string): Promise => { const bytes = new TextEncoder().encode(value); const output = await crypto.subtle.digest('SHA-256', bytes); return [...new Uint8Array(output)].map((byte) => byte.toString(16).padStart(2, '0')).join(''); }; const configuredFault = (): PortfolioFaultId | null => { const value = new URL(location.href).searchParams.get('__portfolio_fault'); return value !== null && faultSet.has(value) ? value as PortfolioFaultId : null; }; const dispatchCanvasEvent = (type: 'webglcontextlost' | 'webglcontextrestored'): void => { const canvas = document.querySelector('.journey-canvas'); if (canvas === null) return; canvas.dispatchEvent(new Event(type, { cancelable: type === 'webglcontextlost' })); }; const trigger = (action: PortfolioFaultAction): void => { if (activeFault === null || !(allowedActions[activeFault]?.includes(action) ?? false)) { throw new TypeError('The requested action is not valid for the configured fault.'); } if (action === 'context-loss') dispatchCanvasEvent('webglcontextlost'); else if (action === 'context-restore') dispatchCanvasEvent('webglcontextrestored'); else window.dispatchEvent(new Event('portfolio-test-audio-interruption')); events.push(action); }; const snapshot = (): TestHookSnapshot => Object.freeze({ fault: activeFault!, events: Object.freeze([...events]), sceneState: document.querySelector('.journey-world')?.getAttribute('data-scene-state') ?? null, canvasCount: document.querySelectorAll('.journey-canvas').length, posterCount: document.querySelectorAll('.journey-poster').length, staticFallbackCount: document.querySelectorAll('.journey-world[data-static-fallback="true"]').length, audioStatus: document.querySelector('.drive-controls__audio-status')?.textContent?.trim() ?? null, videoStatus: document.querySelector('[data-testid="video-status"]')?.textContent?.trim() ?? null, }); export const installAuthorizedTestHooks = async (): Promise => { if (!TEST_BUILD) return; const url = new URL(location.href); const token = url.searchParams.get('__portfolio_test_token'); const fault = configuredFault(); if (token === null || fault === null || await digest(token) !== TEST_TOKEN_DIGEST) return; activeFault = fault; events.length = 0; Object.defineProperty(window, '__PORTFOLIO_TEST_HOOKS__', { configurable: false, enumerable: false, writable: false, value: Object.freeze({ version: 'portfolio-fault-hooks/v1', fault, trigger, snapshot }), }); }; export const isTestFault = (fault: PortfolioFaultId): boolean => ( TEST_BUILD && activeFault === fault ); export const recordTestFaultEvent = (event: string): void => { if (TEST_BUILD && activeFault !== null) events.push(event); };