diff --git a/playwright.config.ts b/playwright.config.ts index ff133fc..3876f12 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -2,6 +2,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ testDir: './tests/browser', + globalSetup: './tests/helpers/test-build-auth.ts', workers: 4, use: { serviceWorkers: 'block', diff --git a/scripts/verify-artifacts.mjs b/scripts/verify-artifacts.mjs index 5c84a21..92a22ec 100644 --- a/scripts/verify-artifacts.mjs +++ b/scripts/verify-artifacts.mjs @@ -15,6 +15,8 @@ const canonicalSectionIds = [ ]; const prohibitedSurfaces = [ /__PORTFOLIO_INPUTS__/iu, + /__PORTFOLIO_TEST_HOOKS__/iu, + /PORTFOLIO_TEST_TOKEN/iu, /__PORTFOLIO_(?:TEST|INSTRUMENTATION)__/iu, /__portfolio(?:Journey)?Instrumentation/iu, /(?:evidenceId|sourcePath|sourceFingerprint|approvalOwner|privacyClassification|denylist|originalFilename)/iu, diff --git a/src/audio/create-audio-controller.ts b/src/audio/create-audio-controller.ts index 98d8339..71ed933 100644 --- a/src/audio/create-audio-controller.ts +++ b/src/audio/create-audio-controller.ts @@ -103,8 +103,8 @@ export function createAudioController(options: AudioControllerOptions): AudioCon if (!graph) return; try { await graph.resume(); - } catch { - if (firstStart) await fail(); + } catch (error) { + if (firstStart || (error instanceof Error && error.name === 'InvalidStateError')) await fail(); else { desired = 'suspended'; resumeIntent = 'user-required'; diff --git a/src/audio/synth-engine.ts b/src/audio/synth-engine.ts index ccbaa20..72df00d 100644 --- a/src/audio/synth-engine.ts +++ b/src/audio/synth-engine.ts @@ -1,12 +1,44 @@ import type { AudioContextLifecycleState, AudioGraph } from '@/audio/types'; +import { isTestFault, recordTestFaultEvent } from '@/testing/fault-hooks'; + +declare const __PORTFOLIO_TEST_BUILD__: boolean; +const TEST_BUILD = typeof __PORTFOLIO_TEST_BUILD__ !== 'undefined' && __PORTFOLIO_TEST_BUILD__; type AudioContextFactory = () => AudioContext; export function createBrowserSynthEngine(): AudioGraph { + if (TEST_BUILD && isTestFault('F10-audio-start-rejection')) { + recordTestFaultEvent('audio-start-rejected'); + throw new DOMException('Injected audio start rejection', 'NotAllowedError'); + } const AudioContextConstructor = window.AudioContext ?? (window as typeof window & { webkitAudioContext?: typeof AudioContext }).webkitAudioContext; if (!AudioContextConstructor) throw new Error('Web Audio is unavailable'); - return createSynthEngine(() => new AudioContextConstructor()); + const graph = createSynthEngine(() => new AudioContextConstructor()); + if (!TEST_BUILD) return graph; + let resumeCount = 0; + return { + ...graph, + async resume() { + resumeCount += 1; + if (isTestFault('F11-audio-resume-rejection') && resumeCount > 1) { + recordTestFaultEvent('audio-resume-rejected'); + throw new DOMException('Injected audio resume rejection', 'NotAllowedError'); + } + if (isTestFault('F12-audio-graph-invalid') && resumeCount > 1) { + recordTestFaultEvent('audio-graph-invalid'); + throw new DOMException('Injected invalid audio graph', 'InvalidStateError'); + } + await graph.resume(); + }, + async suspend() { + if (isTestFault('F13-audio-suspend-rejection')) { + recordTestFaultEvent('audio-suspend-rejected'); + throw new DOMException('Injected audio suspend rejection', 'InvalidStateError'); + } + await graph.suspend(); + }, + }; } export function createSynthEngine(createContext: AudioContextFactory): AudioGraph { diff --git a/src/components/AccessibleVideo.vue b/src/components/AccessibleVideo.vue index 0806560..53258f3 100644 --- a/src/components/AccessibleVideo.vue +++ b/src/components/AccessibleVideo.vue @@ -1,6 +1,10 @@