Skip to content

fix(call): device disconnect recovery — sequential pickers, speaker auto-recovery, picker key fix - #1198

Merged
JamesPHoughton merged 39 commits into
mainfrom
fix/device-recovery-1190
Mar 4, 2026
Merged

fix(call): device disconnect recovery — sequential pickers, speaker auto-recovery, picker key fix#1198
JamesPHoughton merged 39 commits into
mainfrom
fix/device-recovery-1190

Conversation

@JamesPHoughton

Copy link
Copy Markdown
Collaborator

Summary

This PR implements device disconnect recovery improvements for Issue #1190, covering three areas:

  • Sequential per-device error modals: Replace the single merged deviceError state with three independent states (cameraError, micError, speakerError). When multiple devices disconnect simultaneously (e.g. monitor with camera + speakers unplugged), pickers now appear in sequence — camera first (most critical), then mic, then speaker — rather than merging into a single ambiguous error.
  • Auto-dismiss on device return: When alignment detects that a preferred device has returned (non-fallback match), any stale "not-found" picker modal on that channel is automatically dismissed, avoiding a confusing modal asking the user to pick a device they already have.
  • W4 speaker auto-recovery: Extends the devicechange event handler to cover speaker disconnects. Previously it only handled camera and mic (via setInputDevicesAsync). Now when a speaker error is showing and a new audio output device appears, the handler calls devices.setSpeaker() and clears the error.
  • DevicePicker stale key bug: Adds key={pickerDeviceType} to DevicePicker to force remount when transitioning between device types (e.g. camera → mic). Without this, selectedId state from the camera picker was sent as the mic device ID, triggering a re-error loop.

New tests

ID Scenario
DEVRECOV-020 USB headset unplugged: mic picker first, then speaker after switching
DEVRECOV-021 Monitor + webcam mic all missing: camera → mic → speaker sequence
WF4-004 Speaker plugged back in while picker showing: devicechange auto-recovers
DEVRECOV-014 Updated to verify sequential behaviour (was testing old merge behaviour)

459 tests passing across chromium, firefox, and webkit.

Test plan

  • Run npx playwright test --config playwright/playwright.config.mjs "video-call/mocked" — all pass
  • Manual: unplug a monitor mid-call; camera picker appears; switch; speaker picker appears; switch; both resolved
  • Manual: unplug a USB headset mid-call; mic picker appears; switch; speaker picker appears; switch
  • Manual: plug original device back in while picker is open — modal auto-dismisses

Closes #1190

🤖 Generated with Claude Code

JamesPHoughton and others added 30 commits February 20, 2026 09:32
- Keep Tray (Fix A/V button) visible when a device error fires mid-call
  instead of replacing the entire call UI with the error overlay
- Add Dismiss button to UserMediaError so users can clear the error
  without a full page reload
- Show browser-specific permission guidance (with screenshot images)
  when dailyErrorType === "permissions", replacing the generic steps
- Auto-reload when user re-grants camera/mic permissions in browser settings
- Extract shared useGetMicCameraPermissions hook and PermissionDeniedGuidance
  component into PermissionRecovery.jsx (shared by setup flow + in-call)
- Add 24 component tests (DEVRECOV-001–008) across chromium/firefox/webkit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…#1190)

When Daily fires camera-error or mic-error with dailyErrorType "not-found"
(device unplugged mid-call), show a device picker instead of generic steps,
letting users switch to an available device without a full page reload.

- Add DevicePicker component in UserMediaError: enumerates available devices
  and shows a select + "Switch to this device" button
- Add handleSwitchDevice in VideoCall: calls setInputDevicesAsync and updates
  player device preference on success
- Make UserMediaError outer container scrollable (overflow-y-auto) so tall
  error content (e.g. picker + buttons) doesn't overflow the flex container
- Add _setInputDevicesCalls tracking to MockDailyProvider for test assertions
- Wrap MockDailyProvider contextValue + mergedDevices in useMemo to avoid
  unnecessary re-renders
- Add DEVRECOV-009 to DEVRECOV-011 tests covering device picker behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Change trace from 'on-first-retry' to 'on' locally so Playwright UI mode
captures trace data for all test runs (not just retries). CI keeps the
efficient 'on-first-retry' setting via the CI env var.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nd permission revocation (Issue #1190)

TDD implementation of 4 recovery workflows:
- W5: Fatal Daily `error` event shows Connection lost / Removed / Expired UI with contextual Rejoin button
- W6: Network interruption shows non-blocking "Reconnecting..." banner, clears on reconnect
- W1 mid-call: Permission revocation shows proactive PermissionDeniedGuidance overlay, auto-reloads on re-grant
- Sentry: Fix A/V button click sends error-level Sentry message (philosophy: click = detection failure)

11 new Playwright component tests, all passing across Chromium/Firefox/WebKit.
Includes RECOVERY-PLAYBOOK.md documenting all 14 recovery workflows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ecovery (Issue #1190)

TDD implementation of 3 additional recovery workflows:
- W4: Device reconnected auto-recovery — listens for `devicechange` event
  during not-found errors, auto-switches to newly available device
- W2: Auto-fix before picker — when a not-found error fires with exactly
  one alternative device available, auto-switches silently; shows picker
  only when multiple devices available
- W7: Proactive track monitoring — polls track readyState every 5s,
  auto-recovers ended tracks via setInputDevicesAsync

9 new tests (WF4-001 to WF4-003, WF2-001 to WF2-003, WF7-001 to WF7-003).
Updated existing DEVRECOV and ERR tests to mock enumerateDevices after mount
for cross-browser compatibility (webkit resets mocks during CT mount).
398/399 tests pass across Chromium/Firefox/WebKit (1 pre-existing flaky).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ll workflows

Mark W2, W4, W5, W6, W1 mid-call permission revocation, and Sentry on Fix A/V
as fully implemented. Update routing matrix and coverage summary. Note the
distinction between test-file "W7" (proactive track monitoring) and playbook
W7 (network degraded UI). Add WF4/WF5/WF6/WF7/WF1-MID/WF2/WF-SENTRY test IDs
to the test plan section.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- CLAUDE.md: add AI model selection guidance (Sonnet default, Opus on request)
- playwright.config.mjs: serve client/public as publicDir in component tests
- FAILURE-MODES.md: new doc enumerating Daily call failure modes with coverage gaps
- GetPermissions.jsx: remove stray blank line
- test-results/.last-run.json: delete unintentionally tracked test artifact

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…own-cause copy

- Remove click-time Sentry.captureMessage from openFixAV (low signal if user
  cancels without submitting)
- Keep submit-time reportedAVError capture which includes the user's selected
  issues and full diagnostic snapshot in the same event
- Update WF-SENTRY-001 test to verify: click alone sends nothing, submission
  sends reportedAVError with userReportedIssues in extra
- Change "No specific cause identified" to friendlier "We weren't able to
  pinpoint the problem" with reassuring guidance copy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Create three reusable wrapper components to unify error/recovery UI patterns:

- Modal (components/): white card on dark scrim, replaces 5 ad-hoc modal
  implementations (FixAV, ReportMissing, Check-in, IdleProvider, gesture overlay)
- Toast (components/): fixed bottom-right notification with variant colors,
  replaces hard-coded toasts in ReportMissing
- CallBanner (call/): absolute top banner for non-blocking status messages

Reclassify error UIs by severity:
- Fatal errors (call dead): replace call content with white card overlay
- Device errors (remote tracks still working): Modal over call, preserving
  other participants' video instead of unmounting <Call>
- Permission revoked mid-call: Modal over call
- Network interrupted: non-blocking CallBanner

Also switch all error UIs to white/light card style for consistency (previously
mixed dark glassmorphism, white, and slate-800 cards).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…gnostic clutter

- Add showCloseButton prop to Modal (defaults true, renders X in top-right)
- Remove diagnostic info (camera/mic status, device counts) from UserMediaError UI
  (data still collected and sent to Sentry, just not shown to participants)
- Remove Dismiss button from UserMediaError (X close button replaces it)
- Update DEVRECOV-003/004 tests to use modal close button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…1190)

- Refactor deviceErrorCopy to key on [deviceType][dailyErrorType] instead
  of just deviceType, giving each error cause its own title and steps:
  - permissions → "Camera access denied" / "Microphone access denied"
  - in-use → "Camera in use" / "Microphone in use"
  - not-found → "Camera disconnected" / "Microphone disconnected"
  - constraints → "Camera unavailable" / "Microphone unavailable"
  - unknown → "Camera problem" / "Microphone problem"
- Add priority ordering to setDeviceError so higher-priority errors
  (permissions > in-use > not-found > constraints > unknown) are not
  overwritten by lower-priority ones arriving later
- Add DEVRECOV-012/013 tests verifying priority behavior
- Update all existing test assertions for new cause-specific titles

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…itle

When both camera-error and mic-error fire with the same dailyErrorType
(e.g. both "permissions"), the modal now shows "Camera and microphone
access denied" instead of just the last device's error.

- Add combined titles for each dailyErrorType in the default copy section
- Merge logic in setDeviceError detects same-cause, different-device errors
- Add DEVRECOV-014 test verifying the merge behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Update file-level comment with current error title/priority design
- Rewrite DEVRECOV-001/002 descriptions (modal overlay, not content replacement)
- Update DEVRECOV-004 description (X close button, not Dismiss)
- Remove stale "Currently FAILS" markers from DEVRECOV-005/006/007

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… flow

Both the Permissions API onchange listener and Daily's camera-error/mic-error
events now route through setDeviceError with dailyErrorType: "permissions".
This eliminates the separate permissionRevoked state and its dedicated Modal,
ensuring all permission denial errors show the same UserMediaError UI with
cause-specific titles and PermissionDeniedGuidance.

- Remove permissionRevoked state variable and its Modal from render
- Change onchange "denied" handler to call setDeviceError instead
- Remove onchange "granted" handler (UserMediaError's permission monitoring
  hook handles auto-reload when permissions flip back to granted)
- Remove unused PermissionDeniedGuidance import from VideoCall.jsx
- Update WF1-MID-001/002 tests to check for unified device error modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…t found

When device alignment detects a fallback match (preferred device not found by
ID or label), show the device picker instead of silently using the first
available device — but only when 2+ alternatives exist. Single-device fallback
remains silent to avoid unnecessary prompts.

Also renames "disconnected" to "not available" in error titles and picker text,
since this now covers both mid-call disconnection and start-of-call scenarios.

Issue #1190

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the >1 device guard — show the picker even when only one alternative
exists. This gives users situational awareness that we're switching to a
different device than expected (e.g., built-in camera instead of their
preferred external webcam), rather than silently using whatever is available.

Issue #1190

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Brief network blips no longer flash a "Reconnecting..." banner. The banner
only appears if the interruption persists for 5+ seconds — long enough that
participants would notice something is wrong. Reconnection within the grace
period cancels the timer silently.

Added WF6-002 test for the grace period (brief interruption never shows
banner) and WF6-002b for clearing the banner after it appears.

Issue #1190

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the W2 silent auto-switch logic that switched to a single available
device without user knowledge. The picker now always shows for not-found
errors, consistent with the W11 alignment fallback behavior — users should
always be aware when we're using a different device than expected.

Updated WF2-001 and WF2-002 to assert the picker appears rather than a
silent switch, and updated the playbook to document the design rationale.

Issue #1190

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cover the failed, unfixable, partial, unknown, and cant-see states that
were previously untested. Each test monkey-patches the mock call object
to exercise a specific branch of generateRecoverySummary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… root

The playwright/.gitignore already covers these under playwright/, but
test artifacts were landing at the repo root and showing as untracked.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useAudioContextMonitor and useAutoDiagnostics belong alongside
eventLogger.js in call/hooks/. Update VideoCall.jsx imports and
fix the relative path for collectAVDiagnostics inside useAutoDiagnostics.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- UserMediaError: remove unused onDismiss prop and deviceSurvey state
  (fixes no-unused-vars lint errors; dismissal is handled by parent Modal)
- Toast: add role/aria-live for screen reader announcements
  (assertive for error/warning, polite for info/success)
- useGetMicCameraPermissions: null onchange handlers on unmount to
  prevent memory leaks when UserMediaError modal is dismissed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Running all browsers serially in one job was exceeding the 15-minute
limit. Three parallel jobs (chromium / firefox / webkit) each install
only their own browser and run with --project=<browser>, so total
wall-clock time becomes max(chromium, firefox, webkit) rather than
their sum. fail-fast: false lets all three complete even if one fails.
Artifact names are now browser-scoped to avoid upload conflicts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Limit push trigger to main only (PR branches were firing both push
  and pull_request events, creating 6 redundant parallel jobs)
- Add --workers=2 to override the CI=1 default; GitHub Actions runners
  have 2 cores and the mocked tests are lightweight enough to parallelize

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Firefox suspends AudioContext on page load without a user gesture, causing
the "Enable audio" prompt to appear after 800ms regardless of setSpeaker
outcome. SPEAKER-005 tests speaker alignment behaviour, not AudioContext
suspension, so it should mock AudioContext the same way SPEAKER-006 already
does (state: 'running').

Also add `media.autoplay.default: 0` to Firefox Playwright prefs so
headless Firefox allows AudioContext auto-resume without a user gesture,
making the test environment consistent with a focused Firefox tab.

The production code is correct: showing the AudioContext prompt in Firefox
is intentional. The test was simply under-isolated.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Same root cause as SPEAKER-005: Firefox suspends AudioContext without a prior
user gesture, causing the gesture overlay to appear after 800ms and block clicks
on the Fix A/V button (90s timeout on first attempt in CI).

Adding the AudioContext mock (state: 'running') and hasFocus mock isolates the
test to its intended subject — Sentry reporting on Fix A/V submission — and
reduces test time from 90s+ to ~4s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pref controls media element autoplay policy, not AudioContext suspension
tied to page focus. In CI headless Firefox, document.hasFocus() returns false,
so useAudioContextMonitor fires the overlay immediately — before the 800ms
timer is even reached. The pref has no effect on this code path.

The correct fix for each affected test is to mock window.AudioContext
(state: 'running') and document.hasFocus, which we've done in SPEAKER-005
and WF-SENTRY-001. Remove the pref to avoid misleading future readers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… tests from Firefox autoplay policy

In headless Firefox, document.hasFocus() returns false, causing
useAudioContextMonitor to show the gesture overlay immediately on
mount. This was blocking clicks in tests unrelated to AudioContext.

Add a default "running" AudioContext mock + document.hasFocus=()=>true
in the beforeMount hook (playwright/index.jsx) so every test is
isolated from browser autoplay policies by default.

Tests that deliberately test AudioContext behaviour (AUDIO-*, FIXAV-011,
FIXAV-014, PERM-004) opt out by setting window.__customAudioContext=true
in their page.evaluate() block before mount(), which prevents the
beforeMount default from overriding their custom suspended mock.

Remove the now-redundant per-test mocks from SPEAKER-005, SPEAKER-006,
and WF-SENTRY-001 — they are covered by the global default.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ole serialization

Playwright's ConsoleMessage.text() serializes object arguments as
JSHandle@object in Firefox, causing SUB-006 to fail when checking
the log for the participant ID. Switching to Object.keys().join()
produces a consistent string across all browsers and is also more
readable in production logs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JamesPHoughton and others added 7 commits March 3, 2026 08:21
## AGC constraint fix
setInputDevicesAsync() only accepts device IDs — it does not accept
MediaTrackConstraints. The previous audioSource: {...} call was silently
ignored (Daily warned "Received unexpected audioDeviceId"). Switch to
updateInputSettings({ audio: {...} }) which is the correct API for
applying MediaTrackConstraints mid-call. Removes the TEMP marker since
this is the intended approach (still evaluating the strategy).

## Render storm fix
When a mic is unplugged in Firefox, Daily queues mic-error events while
the page is unfocused, then flushes them all (~400+) when focus returns.
Each event called setDeviceError() with a new object reference, which
triggered the recordError useEffect in UserMediaError on every render,
causing console.error and Sentry.captureMessage to fire hundreds of times
and ultimately hitting React's "Maximum update depth exceeded" limit.

Add a same-type dedup guard: if the same error (type + dailyErrorType)
is already displayed, return the existing prev state object rather than a
new one, suppressing the cascade of redundant re-renders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… storms

Verifies that firing 50 rapid mic-error or camera-error events (simulating
Firefox's event-flush on tab focus-regain) produces exactly one error modal
and at most 2 console.error calls, not 50+. Also asserts no "Maximum update
depth exceeded" React error appears.

These tests would have caught the setDeviceError dedup regression.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ssue #1195

Both setInputDevicesAsync({ audioSource: constraints }) and
updateInputSettings({ audio: constraints }) were tried and confirmed
non-functional. Reverts to the original setInputDevicesAsync call
(which silently no-ops) while the correct approach is investigated.
Detailed notes left in code pointing to #1195.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d-call

When a webcam+mic combo or audio output device is unplugged during a call,
the OS silently auto-switches to a fallback device. Previously only Daily
events (camera-error/mic-error) showed a picker, and speaker disconnects had
no UI at all.

Changes:
- alignSpeaker(), alignCamera(), alignMic(): detect fallback match (preferred
  device gone) and show the device picker BEFORE the "skip if already using"
  guard. The old ordering meant currentDevice === fallback.deviceId → early
  return, so the picker never appeared when the OS had already auto-switched.
- All three alignment functions pass pickerDevices directly in the error object
  (from devices.speakers/cameras/microphones) so the picker renders immediately
  without relying on enumerateDevices() returning audiooutput (unreliable in
  webkit headless).
- Storm guard: loggedUnavailable*Ref debounces repeated setDeviceError calls
  when devices updates rapidly (e.g. webcam reconnect initialising), preventing
  render storms and "Maximum update depth exceeded" crashes.
- Storm guard refs reset via useEffect whenever deviceError clears, so future
  disconnects of the newly selected device can show the picker again.
- handleSwitchDevice: added speaker case (setSpeaker + save speakerId/label);
  resets loggedUnavailable*Ref on switch.
- Modal onClose is null for not-found picker errors so users cannot silently
  dismiss to an unknown fallback device by clicking outside.
- UserMediaError: uses error.pickerDevices when present (alignment path),
  falls back to enumerateDevices() result for Daily-event-path errors.

Tests added:
- DEVRECOV-015/016: speaker not found shows picker; selecting clears error
- DEVRECOV-017: mic alignment fallback shows picker (OS auto-switched case)
- DEVRECOV-018: camera alignment fallback shows picker (OS auto-switched case)
- WF-STORM-003: rapid alignment fallback calls do not cause render storm
- ERR-Breadcrumb test updated: fallback no longer logs addBreadcrumb
  (breadcrumbs are for successful id/label switches only)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…und error

When dailyErrorType is "not-found" but pickerDevices is empty (no
alternatives enumerated), the previous code fell through to the generic
steps branch, which is empty for not-found copy, leaving only a title
and "Reload and retry" with no explanation.

Now shows: "No alternative devices were detected. Plug in a device and
reload to retry."

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a monitor with camera and speakers was unplugged, camera-error and
speaker-error both fired with dailyErrorType "not-found". The merge logic
collapsed them into { type: null, pickerDevices: [speakers] }, which:
 - showed the title "Camera and microphone not available" (wrong)
 - showed a DevicePicker labelled "microphone" listing speaker devices
 - called setInputDevicesAsync({ audioDeviceId: speakerDeviceId }) on
   switch, which threw, leaving the modal permanently open

Fix:
 - Move DEVICE_ERROR_PRIORITY / DEVICE_TYPE_PRIORITY outside the component
   (avoids useCallback dep-array warning)
 - Merge only applies to camera-error + mic-error pairs (isCameraMicPair
   guard); speaker-error is never merged
 - Add DEVICE_TYPE_PRIORITY: speaker-error = 1, camera/mic = 2 so a
   camera/mic error always wins over a concurrent speaker error
 - Change final priority comparison from >= to > so equal-priority new
   errors don't displace existing ones (merge handles same-priority
   camera+mic pairs before this check)

Test: DEVRECOV-019 in VideoCall.speakerRecovery.ct.jsx

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…very

Replace single deviceError state with three independent states
(cameraError, micError, speakerError) so multiple simultaneous
disconnects show pickers in sequence (camera → mic → speaker)
rather than merging into a single ambiguous error. Includes:

- keepPrev helper + DEVICE_ERROR_PRIORITY guards dedup/priority
- Fix 1: auto-dismiss not-found modal when preferred device returns
  via successful non-fallback alignment on any of the three channels
- Fix 2: extend W4 devicechange handler to cover speaker-error via
  audiooutput enumeration and devices.setSpeaker()
- key={pickerDeviceType} on DevicePicker to force remount when the
  error type changes (prevents stale selectedId bug)
- Tests: DEVRECOV-014 updated for sequential behavior; DEVRECOV-020
  (mic+speaker), DEVRECOV-021 (camera+mic+speaker), WF4-004 (speaker
  devicechange auto-recovery) added in VideoCall.speakerRecovery.ct

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 4, 2026 14:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements improved in-call recovery when audio/video devices disconnect (Issue #1190), including sequential per-device pickers, speaker output auto-recovery, and a picker remount fix to prevent stale selection loops. It also expands the Playwright component-test harness (multi-browser, default AudioContext mock) and adds extensive new recovery workflow tests and documentation.

Changes:

  • Refactors device error handling to separate camera/mic/speaker error states and show recovery UI sequentially, with speaker reconnect support.
  • Introduces shared UI/permission recovery utilities (Modal, Toast, PermissionRecovery, CallBanner) and integrates them into call/setup flows.
  • Enables multi-browser Playwright CT execution and adds new component tests + recovery playbook docs.

Reviewed changes

Copilot reviewed 33 out of 35 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
test-results/.last-run.json Removes committed Playwright run artifact.
playwright/playwright.config.mjs Enables Firefox/WebKit projects; updates trace behavior; serves client public assets for CT.
playwright/mocks/MockDailyProvider.jsx Adds tracking/spies + memoization to stabilize CT context values and device functions.
playwright/index.jsx Adds default AudioContext mock for Firefox/headless isolation (opt-out via __customAudioContext).
playwright/component-tests/video-call/mocked/VideoCall.speakerRecovery.ct.jsx New CT suite for speaker/output disconnect + recovery + sequencing scenarios.
playwright/component-tests/video-call/mocked/VideoCall.recoveryWorkflows.ct.jsx New CT suite covering recovery playbook workflows (fatal errors, network banner, W4/W7 storm prevention, etc.).
playwright/component-tests/video-call/mocked/VideoCall.deviceRecovery.ct.jsx New CT suite focused on device error UI, picker behavior, priority, and permission guidance.
playwright/component-tests/video-call/mocked/Speaker.ct.jsx Removes per-test AudioContext mocking (now centralized in beforeMount).
playwright/component-tests/video-call/mocked/PermissionMonitoring.ct.jsx Opts out of default AudioContext mock for tests that control AudioContext state.
playwright/component-tests/video-call/mocked/FixAV.ct.jsx Adds non-happy-path Fix A/V modal state coverage; opts out of default AudioContext mock in relevant tests.
playwright/component-tests/video-call/mocked/ErrorReporting.ct.jsx Updates expectations for new not-found picker behavior and Sentry messages.
playwright/component-tests/video-call/mocked/DeviceAlignmentLogs.ct.jsx Updates expected Sentry message text for fallback → picker behavior.
playwright/component-tests/video-call/mocked/AudioContext.ct.jsx Opts out of default AudioContext mock to keep AudioContext tests deterministic.
playwright/component-tests/video-call/TEST-PROGRESS.md Updates test progress counts and notes multi-browser enablement.
playwright/component-tests/video-call/RECOVERY-PLAYBOOK.md Adds a detailed recovery spec / playbook for call failure handling.
playwright/component-tests/video-call/FAILURE-MODES.md Adds failure mode taxonomy + coverage mapping for call recovery.
playwright/add_tests.md Adds additional future testing questions/ideas.
client/src/intro-exit/setup/GetPermissions.jsx Reuses shared permission monitoring + denied guidance component.
client/src/components/Toast.jsx Adds reusable toast component with ARIA live-region behavior.
client/src/components/PermissionRecovery.jsx Adds shared permission hook + denied guidance UI used in setup and in-call errors.
client/src/components/Modal.jsx Adds reusable modal wrapper used across call/setup flows.
client/src/components/IdleProvider.jsx Migrates idle prompt UI to shared Modal.
client/src/call/utils/avRecovery.test.js Updates expected copy for “unknown” recovery summary.
client/src/call/utils/avRecovery.js Updates “unknown” recovery summary messaging.
client/src/call/hooks/useAutoDiagnostics.js Fixes import path for collectAVDiagnostics.
client/src/call/hooks/useAudioContextMonitor.js Adds new hook for AudioContext autoplay/focus handling + auto-resume logic.
client/src/call/VideoCall.jsx Implements sequential device errors, W4/W5/W6/W7 workflows, new modal integration, and speaker recovery logic.
client/src/call/UserMediaError.jsx Adds permission auto-reload, device picker flow (incl. remount key fix), and expanded diagnostics logging.
client/src/call/ReportMissing.jsx Migrates report-missing UI to shared Modal + Toast.
client/src/call/FixAV.jsx Migrates Fix A/V modal to shared Modal; clarifies Sentry submission semantics.
client/src/call/CallBanner.jsx Adds banner component (used for reconnecting indicator).
client/src/call/Call.jsx Reduces console log verbosity for subscription repair.
CLAUDE.md Adds guidance about model selection.
.gitignore Ignores Playwright artifacts (test-results/, playwright-report/).
.github/workflows/playwright_component.yml Runs CT in matrix across chromium/firefox/webkit and uploads per-browser reports on failure.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

*
* Device errors (camera-error, mic-error) are shown as a floating Modal over
* the still-running call — the Call component stays mounted so remote tracks
* are preserved. The modal can be closed via its X button to return to the call.

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header comment says the device error modal “can be closed via its X button to return to the call,” but in the current implementation VideoCall disables onClose for dailyErrorType === "not-found", which hides the close button for the picker modal. Either update this comment to reflect the actual behavior, or re-enable closing for not-found errors if dismissal is intended.

Suggested change
* are preserved. The modal can be closed via its X button to return to the call.
* are preserved. For most error types, the modal can be closed via its X button
* to return to the call; for "not-found" errors, the close button is disabled
* and the modal must be resolved via other recovery flows.

Copilot uses AI. Check for mistakes.
Comment on lines +27 to +35
return (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
onClick={handleBackdropClick}
>
<div
className={`relative mx-4 w-full ${widthClass} rounded-lg bg-white p-6 shadow-xl`}
>

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modal renders a full-screen overlay but doesn’t expose dialog semantics (e.g., role="dialog", aria-modal="true", and a way to associate the title via aria-labelledby). Without these, screen readers may not announce the modal correctly and users can remain in the underlying page context. Consider adding the appropriate ARIA attributes (and ideally initial focus management / Escape-to-close when onClose is provided).

Copilot uses AI. Check for mistakes.
Comment on lines +699 to +729
if (relevantDevices.length > 0) {
const device = relevantDevices[0];
if (isCamera) {
await callObject.setInputDevicesAsync({
videoDeviceId: device.deviceId,
});
setCameraError(null);
} else if (isSpeaker) {
await devices.setSpeaker(device.deviceId);
setSpeakerError(null);
} else {
await callObject.setInputDevicesAsync({
audioDeviceId: device.deviceId,
});
setMicError(null);
}
Sentry.addBreadcrumb({
category: "device-recovery",
message: `Device reconnected: auto-switched ${
// eslint-disable-next-line no-nested-ternary
isCamera ? "camera" : isSpeaker ? "speaker" : "microphone"
}`,
level: "info",
});
}
} catch (err) {
console.warn(
"[VideoCall] Device reconnection auto-recovery failed:",
err
);
}

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the devicechange auto-recovery path, speaker recovery calls devices.setSpeaker() but any NotAllowedError (Safari gesture requirement) is swallowed by the catch, leaving the user stuck with the same modal and no gesture prompt. Consider detecting NotAllowedError / "user gesture" here and routing through handleSetupFailure("speaker", ...) (similar to the alignment + picker switch paths) so the unified gesture modal can recover.

Copilot uses AI. Check for mistakes.
Comment on lines 38 to 41
use: {
trace: 'on-first-retry',
trace: process.env.CI ? 'on-first-retry' : 'on',
screenshot: 'only-on-failure',
video: 'retain-on-failure',

Copilot AI Mar 4, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local runs now always record Playwright traces (trace: 'on' when not in CI). This can significantly slow down component tests and produce large artifacts on developer machines. Consider gating this behind an env flag (e.g. PW_TRACE=1) or using a lighter default like 'retain-on-failure' outside CI.

Copilot uses AI. Check for mistakes.
@cypress

cypress Bot commented Mar 4, 2026

Copy link
Copy Markdown

Deliberation    Run #2084

Run Properties:  status check passed Passed #2084  •  git commit 6427056ea6: chore: merge main (resolve add/add conflicts from squash PR #1191)
Project Deliberation
Branch Review fix/device-recovery-1190
Run status status check passed Passed #2084
Run duration 08m 58s
Commit git commit 6427056ea6: chore: merge main (resolve add/add conflicts from squash PR #1191)
Committer James Houghton
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 1
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 21
View all changes introduced in this branch ↗︎

JamesPHoughton and others added 2 commits March 4, 2026 10:43
UserMediaError.jsx:
- Escape apostrophe in JSX string (react/no-unescaped-entities)
- Extract pickerDevices nested ternary into getPickerDevices() helper
- Replace pickerDeviceType nested ternary with if/else
- Fix consistent-return in useEffect (return undefined on early exit)
- Extract chained JSX ternaries into renderBody() function

VideoCall.jsx:
- Add eslint-disable comment for intentionally-excluded player dep
  in join effect (adding player would cause unwanted re-joins)

client/package.json:
- Move @daily-co/daily-js and @daily-co/daily-react from
  devDependencies to dependencies (they are runtime imports)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
origin/main contained the squashed PR #1191 (all our early commits)
and an independent hooks.js retry log improvement (#1196). Our branch
is a superset of main's content; resolved all add/add file conflicts
by taking the HEAD (branch) version which includes the new sequential
modal + lint fixes added after the PR was opened.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Mar 4, 2026

Copy link
Copy Markdown
/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/call/Call.jsx
  73:9  warning  The 'players' logical expression could make the dependencies of useMemo Hook (at line 213) change on every render. To fix this, wrap the initialization of 'players' in its own useMemo() Hook  react-hooks/exhaustive-deps
  73:9  warning  The 'players' logical expression could make the dependencies of useMemo Hook (at line 242) change on every render. To fix this, wrap the initialization of 'players' in its own useMemo() Hook  react-hooks/exhaustive-deps

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/call/FixAV.jsx
  332:36  error  Return values from promise executor functions cannot be read  no-promise-executor-return
  510:38  error  Return values from promise executor functions cannot be read  no-promise-executor-return
  658:25  error  Do not use Array index in keys                                react/no-array-index-key
  692:25  error  Do not use Array index in keys                                react/no-array-index-key
  749:25  error  Do not use Array index in keys                                react/no-array-index-key
  806:25  error  Do not use Array index in keys                                react/no-array-index-key
  856:25  error  Do not use Array index in keys                                react/no-array-index-key

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/call/layouts/defaultResponsiveLayout.test.js
  1:32  error  'vi' is defined but never used  no-unused-vars

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/call/utils/avRecovery.js
  221:13  error  Use object destructuring                                                                                                                                                   prefer-destructuring
  251:3   error  iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations  no-restricted-syntax
  257:21  error  Unexpected use of continue statement                                                                                                                                       no-continue
  317:3   error  iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations  no-restricted-syntax
  320:7   error  Unexpected use of continue statement                                                                                                                                       no-continue
  331:13  error  Unexpected 'await' inside a loop                                                                                                                                           no-await-in-loop
  347:15  error  Unexpected 'await' inside a loop                                                                                                                                           no-await-in-loop
  365:15  error  Unexpected 'await' inside a loop                                                                                                                                           no-await-in-loop
  376:13  error  Unexpected 'await' inside a loop                                                                                                                                           no-await-in-loop
  393:15  error  Unexpected 'await' inside a loop                                                                                                                                           no-await-in-loop
  404:13  error  Unexpected 'await' inside a loop                                                                                                                                           no-await-in-loop
  455:3   error  iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations  no-restricted-syntax
  457:21  error  Unexpected use of continue statement                                                                                                                                       no-continue

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/call/utils/avRecovery.test.js
   42:7   error  iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations  no-restricted-syntax
  506:45  error  'label' is defined but never used                                                                                                                                          no-unused-vars
  545:45  error  'label' is defined but never used                                                                                                                                          no-unused-vars
  586:45  error  'label' is defined but never used                                                                                                                                          no-unused-vars

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/chat/Chat.jsx
  37:7  warning  React Hook useEffect has a complex expression in the dependency array. Extract it to a separate variable so it can be statically checked  react-hooks/exhaustive-deps

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/components/EmpiricaMenu.jsx
  51:3  error  React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?  react-hooks/rules-of-hooks

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/components/IdleProvider.jsx
   57:6   warning  React Hook useEffect has a missing dependency: 'resetTimer'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
  120:55  error    ''' can be escaped with '&apos;', '&lsquo;', '&#39;', '&rsquo;'                                                react/no-unescaped-entities

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/components/PermissionRecovery.jsx
  79:5  error  Do not nest ternary expressions  no-nested-ternary

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/components/SharedNotepad.jsx
  27:6  warning  React Hook useEffect has missing dependencies: 'defaultText', 'game', and 'record'. Either include them or remove the dependency array  react-hooks/exhaustive-deps

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/components/scroll/useScrollAwareness.js
   67:19  error  Use object destructuring                                                                prefer-destructuring
  108:39  error  Use the '**' operator instead of 'Math.pow'                                             prefer-exponentiation-operator
  108:39  error  'Math.pow' is restricted from being used. Use the exponentiation operator (**) instead  no-restricted-properties

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/elements/Qualtrics.jsx
  55:10  error  'state' is assigned a value but never used  no-unused-vars

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/elements/Survey.jsx
  9:26  error  Unexpected function expression  prefer-arrow-callback

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/elements/TrainingVideo.jsx
  61:6  warning  React Hook useEffect has missing dependencies: 'timer?.elapsed' and 'url'. Either include them or remove the dependency array  react-hooks/exhaustive-deps

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/Consent.jsx
  132:6  warning  React Hook useEffect has a missing dependency: 'player'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/Countdown.jsx
  8:21  error  Use default import syntax to import 'ReactCountdown'  import/no-named-default

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/Lobby.jsx
  64:19  error  ''' can be escaped with '&apos;', '&lsquo;', '&#39;', '&rsquo;'  react/no-unescaped-entities
  64:51  error  ''' can be escaped with '&apos;', '&lsquo;', '&#39;', '&rsquo;'  react/no-unescaped-entities

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/setup/AudioEquipmentCheck.jsx
   87:6   warning  React Hook useEffect has a missing dependency: 'permissionsStatus'. Either include it or remove the dependency array  react-hooks/exhaustive-deps
  104:24  error    Unnecessary return statement                                                                                          no-useless-return
  112:5   error    Arrow function expected no return value                                                                               consistent-return

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/setup/CameraCheck.jsx
  130:6  warning  React Hook useEffect has a missing dependency: 'devices'. Either include it or remove the dependency array  react-hooks/exhaustive-deps

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/setup/GetPermissions.jsx
    3:38  error  'useCallback' is defined but never used  no-unused-vars
  149:5   error  Do not nest ternary expressions          no-nested-ternary

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/setup/LoopbackCheck.jsx
  165:36  error  Return values from promise executor functions cannot be read  no-promise-executor-return

/home/runner/work/deliberation-empirica/deliberation-empirica/client/src/intro-exit/setup/MicCheck.jsx
  118:7  error  React Hook "useCallback" is called conditionally. React Hooks must be called in the exact same order in every component render  react-hooks/rules-of-hooks

/home/runner/work/deliberation-empirica/deliberation-empirica/cypress/e2e/01_Normal_Paths_Omnibus.js
   176:34  error  'actualOrder' is already declared in the upper scope on line 157 column 13  no-shadow
   203:31  error  'newOrder' is already declared in the upper scope on line 192 column 13     no-shadow
  1100:9   error  Expected an assignment or function call and instead saw an expression       no-unused-expressions

/home/runner/work/deliberation-empirica/deliberation-empirica/cypress/e2e/03_Text_Chat.js
  200:9  error  Expected an assignment or function call and instead saw an expression  no-unused-expressions
  215:9  error  Expected an assignment or function call and instead saw an expression  no-unused-expressions

/home/runner/work/deliberation-empirica/deliberation-empirica/cypress/e2e/15_Scroll_Indicator.js
   48:21  error  Assignment to property of function parameter '$el'  no-param-reassign
   69:21  error  Assignment to property of function parameter '$el'  no-param-reassign
  110:21  error  Assignment to property of function parameter '$el'  no-param-reassign

/home/runner/work/deliberation-empirica/deliberation-empirica/cypress/e2e/16_Video_Discussion_Layouts.js
  162:11  error  Expected an assignment or function call and instead saw an expression  no-unused-expressions
  179:11  error  Expected an assignment or function call and instead saw an expression  no-unused-expressions
  187:9   error  Expected an assignment or function call and instead saw an expression  no-unused-expressions

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/getTreatments.js
    8:33  error  Missing file extension for "./preFlight/validateTreatmentFile"  import/extensions
  327:30  error  Unexpected 'await' inside a loop                                no-await-in-loop

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/index.js
  42:43  error  'p' is defined but never used  no-unused-vars

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/preFlight/dispatch.js
  477:9  error  Unexpected use of continue statement  no-continue

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/preFlight/preFlightChecks.js
  20:5  error  iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations  no-restricted-syntax

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/preFlight/validateDlConfig.test.js
  2:32  error  Missing file extension for "./validateDlConfig"                                                                            import/extensions
  3:1   error  'vscode' import should occur before import of './validateDlConfig'                                                         import/order
  6:25  error  Unexpected block statement surrounding arrow body; parenthesize the returned value and move it immediately after the '=>'  arrow-body-style

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/preFlight/validatePromptFile.test.js
  7:8  error  Missing file extension for "./validatePromptFile"  import/extensions

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/providers/dailyco.js
   37:23  error  Expected to return a value at the end of async function 'createRoom'     consistent-return
  157:23  error  Expected to return a value at the end of async function 'stopRecording'  consistent-return
  229:7   error  Unexpected if as the only statement in an else block                     no-lonely-if

/home/runner/work/deliberation-empirica/deliberation-empirica/server/src/utils/logging.js
  1:17  error  'warn' is defined but never used  no-unused-vars
  1:29  error  'log' is defined but never used   no-unused-vars

✖ 76 problems (67 errors, 9 warnings)
  7 errors and 0 warnings potentially fixable with the '--fix' option.

@JamesPHoughton
JamesPHoughton merged commit 74a3466 into main Mar 4, 2026
10 checks passed
@JamesPHoughton
JamesPHoughton deleted the fix/device-recovery-1190 branch March 4, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failing to recover after device removal

2 participants