fix(react): address stale media reference (#1677)

This commit is contained in:
Sam Potts
2026-06-11 09:37:28 -07:00
committed by GitHub
parent e10455aed7
commit b9e0a75c1b
2 changed files with 75 additions and 7 deletions
@@ -1,10 +1,11 @@
'use client';
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
import { CAPTIONS_OFF_VALUE } from '@videojs/core';
import type { ReactNode } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { PlayerContextProvider, type PlayerContextValue } from '../../../player/context';
import { createPlayerWrapper } from '../../../testing/mocks';
import { Menu } from '../../menu';
import { useCaptionsOptions } from '../use-captions-options';
@@ -45,6 +46,42 @@ function renderCaptionsMenu({
return { selectSubtitlesTrack };
}
function createReactiveTextTrackWrapper(initialState: Record<string, unknown>) {
const listeners = new Set<() => void>();
const store = {
state: initialState,
subscribe: (callback: () => void) => {
listeners.add(callback);
return () => listeners.delete(callback);
},
attach: vi.fn(() => vi.fn()),
destroy: vi.fn(),
};
const value: PlayerContextValue = {
store: store as unknown as PlayerContextValue['store'],
media: null,
setMedia: vi.fn(),
container: null,
setContainer: vi.fn(),
};
return {
updateState(next: Record<string, unknown>) {
store.state = next;
for (const listener of listeners) listener();
},
Wrapper({ children }: { children: ReactNode }) {
return <PlayerContextProvider value={value}>{children}</PlayerContextProvider>;
},
};
}
function CaptionsAvailability(): ReactNode {
const captions = useCaptionsOptions();
return <div data-testid="availability">{captions?.state.availability ?? 'missing'}</div>;
}
function CaptionsRadioGroup(): ReactNode {
const captions = useCaptionsOptions();
if (!captions?.showMenu) return null;
@@ -94,4 +131,37 @@ describe('useCaptionsOptions', () => {
expect(selectSubtitlesTrack).toHaveBeenCalledWith(CAPTIONS_OFF_VALUE);
});
it('updates when caption tracks become available', () => {
const { Wrapper, updateState } = createReactiveTextTrackWrapper({
chaptersCues: [],
thumbnailCues: [],
thumbnailTrackSrc: null,
textTrackList: [],
subtitlesShowing: false,
selectSubtitlesTrack: vi.fn(),
toggleSubtitles: vi.fn(),
});
render(<CaptionsAvailability />, { wrapper: Wrapper });
expect(screen.getByTestId('availability').textContent).toBe('unavailable');
act(() => {
updateState({
chaptersCues: [],
thumbnailCues: [],
thumbnailTrackSrc: null,
textTrackList: [
{ kind: 'subtitles', label: 'English', language: 'en', mode: 'disabled' },
{ kind: 'subtitles', label: 'Spanish', language: 'es', mode: 'showing' },
],
subtitlesShowing: true,
selectSubtitlesTrack: vi.fn(),
toggleSubtitles: vi.fn(),
});
});
expect(screen.getByTestId('availability').textContent).toBe('available');
});
});
@@ -1,10 +1,6 @@
'use client';
import {
CAPTIONS_OFF_VALUE,
type CaptionsRadioGroupCore,
CaptionsRadioGroupCore as CaptionsRadioGroupCoreClass,
} from '@videojs/core';
import { CAPTIONS_OFF_VALUE, CaptionsRadioGroupCore } from '@videojs/core';
import { logMissingFeature, selectTextTrack } from '@videojs/core/dom';
import { useCallback, useState } from 'react';
@@ -28,8 +24,10 @@ export interface CaptionsOptionsResult {
}
export function useCaptionsOptions(props?: CaptionsOptionsProps): CaptionsOptionsResult | null {
'use no memo';
const media = usePlayer(selectTextTrack);
const [core] = useState(() => new CaptionsRadioGroupCoreClass());
const [core] = useState(() => new CaptionsRadioGroupCore());
core.setProps(props ?? {});