Skip to content

fix: language switcher crash on minority languages in#9244

Open
tanflem wants to merge 3 commits into
mainfrom
tannerfleming/wat-204-fix-language-switcher-crash-on-minority-languages-in
Open

fix: language switcher crash on minority languages in#9244
tanflem wants to merge 3 commits into
mainfrom
tannerfleming/wat-204-fix-language-switcher-crash-on-minority-languages-in

Conversation

@tanflem
Copy link
Copy Markdown
Contributor

@tanflem tanflem commented May 21, 2026

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Enhanced video player controls reliability by adding defensive checks against disposed or uninitialized player instances across multiple video components
    • Implemented fallback handling for missing image metadata to prevent rendering errors
  • Tests

    • Added comprehensive test coverage for disposed player scenarios to ensure graceful error handling

Review Change Stack

tanflem added 3 commits May 14, 2026 20:56
… to prevent crash on minority languages

When language switcher navigates to a language with no images or imageAlt
data, direct array index access [0].prop threw TypeError. Add optional
chaining so empty arrays render gracefully instead of crashing the
Error Boundary.

Fixes WAT-204
…leming/wat-204-fix-language-switcher-crash-on-minority-languages-in
@linear
Copy link
Copy Markdown

linear Bot commented May 21, 2026

WAT-204

@tanflem tanflem self-assigned this May 21, 2026
@tanflem tanflem requested a review from Kneesal May 21, 2026 21:44
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 21, 2026

Walkthrough

Three video control components (VideoHero, VideoBlock, and main VideoControls) now safely guard against disposed or unavailable Video.js players using new helper functions. Image alt props are updated to fall back to empty strings. Tests verify that disposed players do not cause render errors.

Changes

Player Safety Guards and Image Fallbacks

Layer / File(s) Summary
Player readiness helpers and test coverage
apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx, apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.spec.tsx, apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.tsx, apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.spec.tsx, apps/watch/src/components/VideoControls/VideoControls.tsx
Introduces isPlayerReady, getPlayerCurrentTime, getPlayerDuration, and getPlayerVolume helper functions to safely check player readiness and access properties. Adds test cases verifying that disposed players (where isDisposed() is true) do not throw errors during render.
Image alt text safety
apps/resources/src/components/NewVideoContentPage/VideoCarousel/VideoCard/VideoCard.tsx, apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx, apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.tsx, apps/watch/src/components/VideoControls/VideoControls.tsx
Updates next/image alt props to safely fall back to empty strings when imageAlt[0]?.value is missing, using the pattern imageAlt[0]?.value ?? ''.
VideoHero VideoControls readiness guards
apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx
Adopts readiness helpers to guard volume initialization, event handlers (volumechange, fullscreenchange), and all UI action handlers (play/pause, mobile fullscreen, seek, mute, volume, mouse activity) against unsafe player access.
VideoBlock VideoControls readiness guards and analytics
apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.tsx
Replaces optional chaining with helper-based reads throughout duration fallback, analytics events (video_start, video_play, video_pause, video_ended, fullscreen enter/exit), state synchronization (timeupdate, volumechange, fullscreenchange), and UI action handlers (play/pause, fullscreen, seek, mute, volume, mouse activity).
Main VideoControls readiness guards and analytics
apps/watch/src/components/VideoControls/VideoControls.tsx
Replaces optional chaining with helper-based reads throughout duration fallback, GTM analytics events (video_start, video_play, video_pause, video_ended, fullscreen enter/exit), state synchronization (timeupdate, volumechange, fullscreenchange), and UI action handlers (play/pause, fullscreen, seek, mute, volume, mouse activity).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • csiyang
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title mentions fixing a 'language switcher crash on minority languages' but the changeset focuses entirely on defensive coding for Video.js player instances and safe fallbacks for image alt properties across multiple VideoControls components. Update the PR title to accurately reflect the main changes, such as 'fix: add defensive checks for disposed Video.js player instances' or similar, to match the actual code modifications in the changeset.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tannerfleming/wat-204-fix-language-switcher-crash-on-minority-languages-in

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented May 21, 2026

View your CI Pipeline Execution ↗ for commit d9d2d77

Command Status Duration Result
nx run resources-e2e:e2e ✅ Succeeded 10s View ↗
nx run watch-e2e:e2e ✅ Succeeded 19s View ↗
nx run-many --target=vercel-alias --projects=re... ✅ Succeeded 1s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 7s View ↗
nx run-many --target=deploy --projects=resources ✅ Succeeded 1m 36s View ↗
nx run-many --target=vercel-alias --projects=watch ✅ Succeeded 1s View ↗
nx run-many --target=upload-sourcemaps --projec... ✅ Succeeded 5s View ↗
nx run-many --target=deploy --projects=watch ✅ Succeeded 1m 17s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-21 21:53:36 UTC

@github-actions
Copy link
Copy Markdown
Contributor

The latest updates on your projects.

Name Status Preview Updated (UTC)
watch ✅ Ready watch preview Fri May 22 09:48:29 NZST 2026

@github-actions
Copy link
Copy Markdown
Contributor

The latest updates on your projects.

Name Status Preview Updated (UTC)
resources ✅ Ready resources preview Fri May 22 09:48:57 NZST 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
apps/resources/src/components/NewVideoContentPage/VideoCarousel/VideoCard/VideoCard.tsx (1)

57-69: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Next.js Image fallback should not use src=""
In apps/resources/src/components/NewVideoContentPage/VideoCarousel/VideoCard/VideoCard.tsx the next/image src falls back to '' (src={video.images[0]?.mobileCinematicHigh ?? ''}). Next.js 16 treats a falsy/empty src as missing and throws during rendering/optimization, so this can replace the original runtime issue with a Next.js src error.

✅ Suggested change (render Image only when src is available)
-              <Image
-                fill
-                src={video.images[0]?.mobileCinematicHigh ?? ''}
-                alt={video.imageAlt[0]?.value ?? ''}
-                style={{
-                  width: '100%',
-                  objectFit: 'cover',
-                  maskImage:
-                    'linear-gradient(to bottom, rgba(0,0,0,1) 50%, transparent 100%)',
-                  maskSize: 'cover',
-                  pointerEvents: 'none'
-                }}
-              />
+              {video.images[0]?.mobileCinematicHigh != null ? (
+                <Image
+                  fill
+                  src={video.images[0].mobileCinematicHigh}
+                  alt={video.imageAlt[0]?.value ?? ''}
+                  style={{
+                    width: '100%',
+                    objectFit: 'cover',
+                    maskImage:
+                      'linear-gradient(to bottom, rgba(0,0,0,1) 50%, transparent 100%)',
+                    maskSize: 'cover',
+                    pointerEvents: 'none'
+                  }}
+                />
+              ) : (
+                <div className="h-full w-full bg-gray-800" />
+              )}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/resources/src/components/NewVideoContentPage/VideoCarousel/VideoCard/VideoCard.tsx`
around lines 57 - 69, The Image usage in VideoCard (the JSX that renders <Image
... src={video.images[0]?.mobileCinematicHigh ?? ''} ... />) must not pass an
empty string to next/image; change the component to only render the Image when a
valid src exists (e.g. check video.images[0]?.mobileCinematicHigh truthiness)
and otherwise render a safe fallback (placeholder div/skeleton or nothing) so
next/image never receives '' or a falsy src; update the VideoCard component's
JSX accordingly.
apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx (2)

418-423: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Do not set fullscreen state to true when mobile fullscreen request is skipped.

If player is not ready, the request is not executed, but state is still forced to fullscreen. This desynchronizes UI state from actual fullscreen state.

Minimal fix
       if (isMobile()) {
-        if (isPlayerReady(player)) void player.requestFullscreen()
+        if (!isPlayerReady(player)) return
+        void player.requestFullscreen()
         dispatchPlayer({
           type: 'SetFullscreen',
           fullscreen: true
         })
       } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx`
around lines 418 - 423, The code sets fullscreen state to true even when the
mobile fullscreen request is skipped; change the logic so you only
dispatchPlayer({ type: 'SetFullscreen', fullscreen: true }) when the player
request actually runs: check isPlayerReady(player) first, call void
player.requestFullscreen() and then dispatch the SetFullscreen action; if the
player isn't ready, do not set fullscreen to true. Optionally attach a .catch to
player.requestFullscreen() and only dispatch on success to avoid
desynchronization.

228-400: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Missing cleanup for player/fscreen subscriptions can duplicate analytics and state updates.

This effect re-runs (notably when loading changes) and adds new anonymous listeners each time, but never unregisters them. Over time, events will fan out and dispatch multiple times.

Suggested direction
 useEffect(() => {
-  player?.on('play', () => {
+  const handlePlayEvent = () => {
     // ...
-  })
+  }
+  // define all other handlers similarly...
+  const handleFullscreenEvent = () => {
+    // ...
+  }

-  player?.on('pause', () => { ... })
+  player?.on('play', handlePlayEvent)
+  // register other handlers with stable refs...
+  fscreen.addEventListener('fullscreenchange', handleFullscreenEvent)
+
+  return () => {
+    player?.off('play', handlePlayEvent)
+    // unregister all other handlers...
+    fscreen.removeEventListener('fullscreenchange', handleFullscreenEvent)
+  }
-}, [id, player, dispatchPlayer, loading, title, variant])
+}, [id, player, dispatchPlayer, title, variant])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx`
around lines 228 - 400, The effect attaches many anonymous listeners to player
and fscreen without removing them, causing duplicated analytics/state updates on
re-runs; fix by extracting each listener into named handler functions (for play,
pause, timeupdate, volumechange, fullscreenchange, useractive, userinactive,
waiting, playing, ended, canplay, canplaythrough and the fscreen
fullscreenchange handler) and register them with player.on(...) and
fscreen.addEventListener(...), then return a cleanup function that calls
player.off(handler) for each registered event and
fscreen.removeEventListener(...) for the fullscreen handler; ensure you also
clear any state side-effects if needed (e.g., avoid stale closures for
dispatchPlayer, eventToDataLayer, setInitialLoadComplete) by referencing the
same handlers in the cleanup.
apps/watch/src/components/VideoControls/VideoControls.tsx (2)

239-411: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Effect subscribes repeatedly without cleanup, causing event/listener accumulation.

This hook attaches fresh anonymous handlers on each re-run and never unregisters them. Because the dependency list includes state like loading, duplicated analytics and repeated dispatches are likely.

Suggested direction
 useEffect(() => {
-  player?.on('play', () => { ... })
+  const handlePlayEvent = () => { ... }
+  // define remaining handlers
+  const handleFullscreenEvent = () => { ... }

+  player?.on('play', handlePlayEvent)
+  // register other handlers
+  fscreen.addEventListener('fullscreenchange', handleFullscreenEvent)
+
+  return () => {
+    player?.off('play', handlePlayEvent)
+    // unregister other handlers
+    fscreen.removeEventListener('fullscreenchange', handleFullscreenEvent)
+  }
-}, [id, player, dispatchPlayer, loading, title, variant])
+}, [id, player, dispatchPlayer, title, variant])
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/watch/src/components/VideoControls/VideoControls.tsx` around lines 239 -
411, The effect in useEffect is adding anonymous event handlers (player?.on(...)
and fscreen.addEventListener(...)) on every rerun (e.g. due to loading) without
removing them, causing listener accumulation; refactor by extracting stable
handler functions (for play, pause, timeupdate, volumechange, fullscreenchange,
useractive, userinactive, waiting, playing, ended, canplay, canplaythrough and
the fscreen fullscreenchange handler), register them once inside the useEffect,
and return a cleanup function that removes each listener via player.off(...) and
fscreen.removeEventListener(...) so listeners are not duplicated; keep
dispatchPlayer, setInitialLoadComplete, eventToDataLayer and helper calls
(getPlayerCurrentTime, getPlayerDuration, getPlayerVolume, isPlayerReady,
secondsToTimeFormat) inside those handlers and ensure dependencies only include
truly changing refs (id, title, variant, player) or use refs for stable values
to avoid re-subscribing.

429-434: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Prevent fullscreen UI state drift on mobile when player is unavailable.

fullscreen is set to true even when no fullscreen request is made because player readiness failed.

Minimal fix
       if (isMobile()) {
-        if (isPlayerReady(player)) void player.requestFullscreen()
+        if (!isPlayerReady(player)) return
+        void player.requestFullscreen()
         dispatchPlayer({
           type: 'SetFullscreen',
           fullscreen: true
         })
       } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/watch/src/components/VideoControls/VideoControls.tsx` around lines 429 -
434, The code sets fullscreen state unconditionally on mobile even when the
player isn't ready; update the block so you only dispatch the 'SetFullscreen'
action when the player is ready and the fullscreen request actually succeeds:
call isPlayerReady(player) and attempt player.requestFullscreen(), and only
after that succeeds (or inside the same conditional that proved player
readiness) call dispatchPlayer({ type: 'SetFullscreen', fullscreen: true }); do
not set fullscreen=true when player is unavailable or the request fails (handle
the promise rejection or wrap in try/catch around player.requestFullscreen()).
apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.tsx (1)

655-660: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard fullscreen state update behind successful mobile request path.

When player is not ready, fullscreen request is skipped but state is still set to true.

Minimal fix
       if (isMobile()) {
-        if (isPlayerReady(player)) void player.requestFullscreen()
+        if (!isPlayerReady(player)) return
+        void player.requestFullscreen()
         dispatchPlayer({
           type: 'SetFullscreen',
           fullscreen: true
         })
       } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.tsx`
around lines 655 - 660, When handling the mobile fullscreen path in
VideoControls.tsx, avoid setting fullscreen state unless the player's
requestFullscreen actually ran successfully: wrap the dispatchPlayer({ type:
'SetFullscreen', fullscreen: true }) behind the same isPlayerReady(player) check
and only call it after awaiting and/or confirming player.requestFullscreen()
succeeded (catch errors and skip dispatch on failure). Update the block that
currently calls isMobile(), isPlayerReady(player), player.requestFullscreen and
dispatchPlayer so that dispatching the 'SetFullscreen' action occurs only after
a successful requestFullscreen call (or when isPlayerReady is true for
non-mobile paths), referencing isMobile, isPlayerReady,
player.requestFullscreen, and dispatchPlayer/'SetFullscreen'.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@apps/resources/src/components/NewVideoContentPage/VideoCarousel/VideoCard/VideoCard.tsx`:
- Around line 57-69: The Image usage in VideoCard (the JSX that renders <Image
... src={video.images[0]?.mobileCinematicHigh ?? ''} ... />) must not pass an
empty string to next/image; change the component to only render the Image when a
valid src exists (e.g. check video.images[0]?.mobileCinematicHigh truthiness)
and otherwise render a safe fallback (placeholder div/skeleton or nothing) so
next/image never receives '' or a falsy src; update the VideoCard component's
JSX accordingly.

In
`@apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx`:
- Around line 418-423: The code sets fullscreen state to true even when the
mobile fullscreen request is skipped; change the logic so you only
dispatchPlayer({ type: 'SetFullscreen', fullscreen: true }) when the player
request actually runs: check isPlayerReady(player) first, call void
player.requestFullscreen() and then dispatch the SetFullscreen action; if the
player isn't ready, do not set fullscreen to true. Optionally attach a .catch to
player.requestFullscreen() and only dispatch on success to avoid
desynchronization.
- Around line 228-400: The effect attaches many anonymous listeners to player
and fscreen without removing them, causing duplicated analytics/state updates on
re-runs; fix by extracting each listener into named handler functions (for play,
pause, timeupdate, volumechange, fullscreenchange, useractive, userinactive,
waiting, playing, ended, canplay, canplaythrough and the fscreen
fullscreenchange handler) and register them with player.on(...) and
fscreen.addEventListener(...), then return a cleanup function that calls
player.off(handler) for each registered event and
fscreen.removeEventListener(...) for the fullscreen handler; ensure you also
clear any state side-effects if needed (e.g., avoid stale closures for
dispatchPlayer, eventToDataLayer, setInitialLoadComplete) by referencing the
same handlers in the cleanup.

In
`@apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.tsx`:
- Around line 655-660: When handling the mobile fullscreen path in
VideoControls.tsx, avoid setting fullscreen state unless the player's
requestFullscreen actually ran successfully: wrap the dispatchPlayer({ type:
'SetFullscreen', fullscreen: true }) behind the same isPlayerReady(player) check
and only call it after awaiting and/or confirming player.requestFullscreen()
succeeded (catch errors and skip dispatch on failure). Update the block that
currently calls isMobile(), isPlayerReady(player), player.requestFullscreen and
dispatchPlayer so that dispatching the 'SetFullscreen' action occurs only after
a successful requestFullscreen call (or when isPlayerReady is true for
non-mobile paths), referencing isMobile, isPlayerReady,
player.requestFullscreen, and dispatchPlayer/'SetFullscreen'.

In `@apps/watch/src/components/VideoControls/VideoControls.tsx`:
- Around line 239-411: The effect in useEffect is adding anonymous event
handlers (player?.on(...) and fscreen.addEventListener(...)) on every rerun
(e.g. due to loading) without removing them, causing listener accumulation;
refactor by extracting stable handler functions (for play, pause, timeupdate,
volumechange, fullscreenchange, useractive, userinactive, waiting, playing,
ended, canplay, canplaythrough and the fscreen fullscreenchange handler),
register them once inside the useEffect, and return a cleanup function that
removes each listener via player.off(...) and fscreen.removeEventListener(...)
so listeners are not duplicated; keep dispatchPlayer, setInitialLoadComplete,
eventToDataLayer and helper calls (getPlayerCurrentTime, getPlayerDuration,
getPlayerVolume, isPlayerReady, secondsToTimeFormat) inside those handlers and
ensure dependencies only include truly changing refs (id, title, variant,
player) or use refs for stable values to avoid re-subscribing.
- Around line 429-434: The code sets fullscreen state unconditionally on mobile
even when the player isn't ready; update the block so you only dispatch the
'SetFullscreen' action when the player is ready and the fullscreen request
actually succeeds: call isPlayerReady(player) and attempt
player.requestFullscreen(), and only after that succeeds (or inside the same
conditional that proved player readiness) call dispatchPlayer({ type:
'SetFullscreen', fullscreen: true }); do not set fullscreen=true when player is
unavailable or the request fails (handle the promise rejection or wrap in
try/catch around player.requestFullscreen()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e860d431-3a25-4051-9a91-e7730c3f6324

📥 Commits

Reviewing files that changed from the base of the PR and between 64b1ad2 and d9d2d77.

📒 Files selected for processing (6)
  • apps/resources/src/components/NewVideoContentPage/VideoCarousel/VideoCard/VideoCard.tsx
  • apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.spec.tsx
  • apps/resources/src/components/VideoContentPage/VideoHero/VideoPlayer/VideoControls/VideoControls.tsx
  • apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.spec.tsx
  • apps/watch/src/components/VideoBlock/VideoBlockPlayer/VideoControls/VideoControls.tsx
  • apps/watch/src/components/VideoControls/VideoControls.tsx

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.

1 participant