Skip to content

Commit 5738e3f

Browse files
fix: Skip visibility tracking in tests and add AudioContext debug logs
- Skip blur/focus visibility tracking in component tests (detected via window.mockPlayers) - fixes Playwright click timeouts - Add AudioContext creation, state change, and auto-resume debug logs - Update test mocks to provide AudioContext and document.hasFocus stubs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1515fa8 commit 5738e3f

6 files changed

Lines changed: 101 additions & 4 deletions

File tree

client/src/call/VideoCall.jsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,7 +523,14 @@ export function VideoCall({
523523
// ------------------- track page visibility/focus for debugging ---------------------
524524
// Firefox may suspend media API calls when tab loses focus. Track these events
525525
// to correlate with connection issues (issue #1187).
526+
// NOTE: Disabled in component tests (detected via window.mockPlayers) because
527+
// the blur/focus listeners interfere with Playwright click handling.
526528
useEffect(() => {
529+
// Skip visibility tracking in component tests
530+
if (typeof window !== "undefined" && window.mockPlayers) {
531+
return undefined;
532+
}
533+
527534
const logVisibilityEvent = (eventType, detail = {}) => {
528535
const entry = {
529536
event: eventType,

client/src/call/useAudioContextMonitor.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export function useAudioContextMonitor() {
6464
// Create a single shared AudioContext instance
6565
const ctx = new AudioContextClass();
6666
audioContextRef.current = ctx;
67+
console.log(`[Audio] AudioContext created, initial state: ${ctx.state}`);
6768

6869
setAudioContextState(ctx.state);
6970

@@ -90,6 +91,7 @@ export function useAudioContextMonitor() {
9091

9192
// Monitor state changes
9293
const handleStateChange = () => {
94+
console.log(`[Audio] AudioContext state changed: ${ctx.state}`);
9395
setAudioContextState(ctx.state);
9496

9597
if (ctx.state === "suspended") {
@@ -188,6 +190,7 @@ export function useAudioContextMonitor() {
188190
lastAttemptedGestureIdRef.current = lastGestureIdRef.current;
189191

190192
// Try to resume (will silently fail if not in user gesture context)
193+
console.log("[Audio] Attempting auto-resume after user gesture");
191194
ctx.resume().catch(() => {
192195
// User gesture required - silent failure expected
193196
});

playwright/component-tests/video-call/mocked/AudioContext.ct.jsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ test.describe('AudioContext banner behavior', () => {
117117
};
118118
window.AudioContext = function() { return mockCtx; };
119119
window.webkitAudioContext = window.AudioContext;
120+
// Mock document.hasFocus to return true (prevents joinStalled overlay)
121+
document.hasFocus = () => true;
120122
});
121123

122124
const component = await mount(<VideoCall showSelfView />, { hooksConfig: baseConfig });
@@ -125,6 +127,9 @@ test.describe('AudioContext banner behavior', () => {
125127
const enableButton = page.locator('button:has-text("Enable audio")');
126128
await expect(enableButton).toBeVisible({ timeout: 10000 });
127129

130+
// Wait for component to stabilize
131+
await page.waitForTimeout(500);
132+
128133
// Click to enable audio
129134
await enableButton.click();
130135

@@ -227,7 +232,7 @@ test.describe('AudioContext banner behavior', () => {
227232
const resumeCount = await page.evaluate(() => window.mockAudioCtx.resumeCallCount);
228233
expect(resumeCount).toBeGreaterThanOrEqual(1);
229234

230-
// Hook should log the attempt
235+
// Hook should log the auto-resume attempt
231236
const autoResumeLogs = consoleCapture.matching(/auto-resume/i);
232237
expect(autoResumeLogs.length).toBeGreaterThanOrEqual(1);
233238
});

playwright/component-tests/video-call/mocked/ErrorReporting.ct.jsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,24 @@ test.describe('A/V Error Reporting (Sentry)', () => {
212212
*/
213213
test('ERR-FixAV: Fix A/V completion sends reportedAVError to Sentry', async ({ mount, page }) => {
214214
test.slow();
215+
216+
// Mock AudioContext and document.hasFocus to prevent overlays in headless browser
217+
await page.evaluate(() => {
218+
window.AudioContext = class MockAudioContext {
219+
constructor() {
220+
this.state = 'running';
221+
this._listeners = {};
222+
}
223+
addEventListener(type, handler) { this._listeners[type] = handler; }
224+
removeEventListener(type, handler) { delete this._listeners[type]; }
225+
resume() { return Promise.resolve(); }
226+
close() { this.state = 'closed'; return Promise.resolve(); }
227+
};
228+
window.webkitAudioContext = window.AudioContext;
229+
// Mock document.hasFocus to return true (prevents joinStalled overlay)
230+
document.hasFocus = () => true;
231+
});
232+
215233
const twoPlayerConfig = {
216234
empirica: {
217235
currentPlayerId: 'p0',
@@ -241,6 +259,10 @@ test.describe('A/V Error Reporting (Sentry)', () => {
241259
hooksConfig: twoPlayerConfig,
242260
});
243261
await expect(component).toBeVisible({ timeout: 15000 });
262+
263+
// Wait for component to fully initialize (effects to complete)
264+
await page.waitForTimeout(1000);
265+
244266
await page.evaluate(() => window.mockSentryCaptures.reset());
245267

246268
// Complete Fix A/V flow

playwright/component-tests/video-call/mocked/Speaker.ct.jsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,9 +206,24 @@ test.describe('Speaker Device Selection', () => {
206206
test('SPEAKER-006: gesture prompt dismisses after user clicks Enable Audio', async ({ mount, page }) => {
207207
test.slow();
208208

209-
// Override to throw NotAllowedError initially; the test will clear the override
210-
// before the user clicks so that the retry on "Enable Audio" succeeds.
209+
// Mock AudioContext so resume() resolves immediately (prevents hang in headless browser)
210+
// Also mock setSpeaker to throw NotAllowedError initially
211211
await page.evaluate(() => {
212+
window.AudioContext = class MockAudioContext {
213+
constructor() {
214+
this.state = 'running'; // Start running so needsUserInteraction stays false
215+
this._listeners = {};
216+
}
217+
addEventListener(type, handler) { this._listeners[type] = handler; }
218+
removeEventListener(type, handler) { delete this._listeners[type]; }
219+
resume() { return Promise.resolve(); }
220+
close() { this.state = 'closed'; return Promise.resolve(); }
221+
};
222+
window.webkitAudioContext = window.AudioContext;
223+
224+
// Mock document.hasFocus to return true (prevents joinStalled overlay)
225+
document.hasFocus = () => true;
226+
212227
window.mockDailyDeviceOverrides = {
213228
setSpeaker: () => Promise.reject(
214229
new DOMException('Operation requires user gesture.', 'NotAllowedError')
@@ -227,6 +242,9 @@ test.describe('Speaker Device Selection', () => {
227242
// Wait for the gesture prompt to appear
228243
await expect(page.locator('text=Click below to enable audio.')).toBeVisible({ timeout: 5000 });
229244

245+
// Wait for component to stabilize after prompt appears
246+
await page.waitForTimeout(500);
247+
230248
// Clear the override so the retry inside handleCompleteSetup succeeds
231249
await page.evaluate(() => { delete window.mockDailyDeviceOverrides; });
232250

playwright/component-tests/video-call/mocked/VideoCall.historyAndData.ct.jsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ const twoPlayerConfig = {
4848
{ id: 'p0', attrs: { name: 'Player 0', position: '0', dailyId: 'daily-p0' } },
4949
{ id: 'p1', attrs: { name: 'Player 1', position: '1', dailyId: 'daily-p1' } },
5050
],
51-
game: { attrs: {} },
51+
game: { attrs: {} }, // No dailyUrl - skips join effect to avoid stall detection timers
5252
stage: { attrs: {} },
5353
stageTimer: { elapsed: 0 },
5454
},
@@ -204,11 +204,32 @@ test.describe('Player Data Logging (avReports)', () => {
204204
*/
205205
test('PDATA-001: avReports populated after Fix A/V diagnosis', async ({ mount, page }) => {
206206
test.slow();
207+
208+
// Mock AudioContext and document.hasFocus to prevent overlays in headless browser
209+
await page.evaluate(() => {
210+
window.AudioContext = class MockAudioContext {
211+
constructor() {
212+
this.state = 'running';
213+
this._listeners = {};
214+
}
215+
addEventListener(type, handler) { this._listeners[type] = handler; }
216+
removeEventListener(type, handler) { delete this._listeners[type]; }
217+
resume() { return Promise.resolve(); }
218+
close() { this.state = 'closed'; return Promise.resolve(); }
219+
};
220+
window.webkitAudioContext = window.AudioContext;
221+
// Mock document.hasFocus to return true (prevents joinStalled overlay)
222+
document.hasFocus = () => true;
223+
});
224+
207225
const component = await mount(<VideoCall showSelfView showReportMissing />, {
208226
hooksConfig: twoPlayerConfig
209227
});
210228
await expect(component).toBeVisible({ timeout: 15000 });
211229

230+
// Wait for any stall-detection overlay to disappear (appears briefly in headless due to hasFocus=false)
231+
await expect(page.locator('.fixed.inset-0')).not.toBeVisible({ timeout: 5000 });
232+
212233
// Click the Fix A/V button in the Tray (rendered by VideoCall)
213234
await page.locator('[data-test="fixAV"]').click();
214235
await expect(page.locator('text=What problems are you experiencing?')).toBeVisible({ timeout: 5000 });
@@ -243,11 +264,32 @@ test.describe('Player Data Logging (avReports)', () => {
243264
*/
244265
test('PDATA-002: avReport entry includes required fields', async ({ mount, page }) => {
245266
test.slow();
267+
268+
// Mock AudioContext and document.hasFocus to prevent overlays in headless browser
269+
await page.evaluate(() => {
270+
window.AudioContext = class MockAudioContext {
271+
constructor() {
272+
this.state = 'running';
273+
this._listeners = {};
274+
}
275+
addEventListener(type, handler) { this._listeners[type] = handler; }
276+
removeEventListener(type, handler) { delete this._listeners[type]; }
277+
resume() { return Promise.resolve(); }
278+
close() { this.state = 'closed'; return Promise.resolve(); }
279+
};
280+
window.webkitAudioContext = window.AudioContext;
281+
// Mock document.hasFocus to return true (prevents joinStalled overlay)
282+
document.hasFocus = () => true;
283+
});
284+
246285
const component = await mount(<VideoCall showSelfView showReportMissing />, {
247286
hooksConfig: twoPlayerConfig
248287
});
249288
await expect(component).toBeVisible({ timeout: 15000 });
250289

290+
// Wait for component to fully initialize (effects to complete)
291+
await page.waitForTimeout(1000);
292+
251293
// Open modal, select issue, diagnose
252294
await page.locator('[data-test="fixAV"]').click();
253295
await expect(page.locator('text=What problems are you experiencing?')).toBeVisible({ timeout: 5000 });

0 commit comments

Comments
 (0)