Cold cross-family review of 57aefcdf. Six findings, all web-only. F1 (HIGH, data-loss shaped): RerunCollectionsController.ProjectToResponseModel derives BOTH selectedId and selectedName from the same eager-loaded navigation, and GetRerunCollectionByIdHandler loads media metadata only for Show/Season/Artist/Movie while MediaCollections/Mapper maps RemoteStream through `_ => null`. So opening a RemoteStream rerun collection returned HTTP 200 with a null selection and the edit-load refresh CLEARED a stored id, leaving Save permanently disabled. The refresh now merges instead of replacing, so no path can clear an id it merely failed to name; the label degrades to `#id`. Covered per affected type — RemoteStream, Episode, MusicVideo, Song, OtherVideo, Image — plus a re-save assertion. The read-model gaps themselves are server-side and are NOT touched here. F2: `&` and `|` were missing from the escaped set, so `Rock && Roll` compiled with the boolean operator live. Pre-existing in Auto-Tune's original helper, but propagated to three more pickers — and now fixed for Auto-Tune too, since the helper is shared. The test that claimed to cover "every Lucene special" carried its own hand-copied sample and could not see its own omissions; it is now driven per-character off an exported LIBRARY_PICKER_LUCENE_SPECIALS. F3: a slow edit-load name resolution could relabel a newer selection. The label is now keyed to the id it was resolved for AND refuses to overwrite a label naming a different id — keying the render alone stops the mislabelling but discards the correct new label. F4: searchLibraryPickerOptions clamps pageSize instead of merely defaulting it. A bound a caller can exceed is not a bound. F6: replacing a native <select> with an input+listbox dropped keyboard operability. Full ARIA combobox pattern added — role/aria-expanded/aria-controls/aria-autocomplete, Arrow/Home/End over aria-activedescendant, Enter to commit, Escape to dismiss, options as non-tab-stops, cursor reset on each new result set. F7: both is-mounted tests were unsound. React 19 no longer warns on setState-after-unmount and an unmounted tree renders nothing either way, so the DOM assertion could not fail; the hook re-arm test used rerender rather than an effect cleanup. Now: a hook-module mock proving SearchPicker actually reads the guard and sees false, and a StrictMode double-invoke for the re-arm. Both verified by removing the mechanism and watching them fail. Same for the LCG divisor, which now has a direct boundary test. F5 (FillerPresets collection-family names) is filed as #670, not fixed here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { cleanup, render } from '@testing-library/react';
|
|
import { StrictMode, useEffect } from 'react';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import { useIsMountedRef } from './hooks';
|
|
|
|
afterEach(cleanup);
|
|
|
|
describe('useIsMountedRef (#578)', () => {
|
|
it('reads true while mounted and false after unmount', () => {
|
|
let captured: { readonly current: boolean } | null = null;
|
|
|
|
function Probe() {
|
|
captured = useIsMountedRef();
|
|
return null;
|
|
}
|
|
|
|
const { unmount } = render(<Probe />);
|
|
|
|
// The guard every async callback reads: true for as long as the component is on screen...
|
|
expect(captured).not.toBeNull();
|
|
expect((captured as unknown as { current: boolean }).current).toBe(true);
|
|
|
|
unmount();
|
|
|
|
// ...and false afterwards, so a fetch resolving late can be dropped instead of calling setState
|
|
// on a component that no longer exists. Without the effect's cleanup this stays true, which is
|
|
// exactly the defect the hook exists to prevent.
|
|
expect((captured as unknown as { current: boolean }).current).toBe(false);
|
|
});
|
|
|
|
it('is re-armed by a StrictMode double-invoke (mount → cleanup → mount)', () => {
|
|
let captured: { readonly current: boolean } | null = null;
|
|
let effectRuns = 0;
|
|
|
|
function Probe() {
|
|
captured = useIsMountedRef();
|
|
useEffect(() => {
|
|
effectRuns += 1;
|
|
});
|
|
return null;
|
|
}
|
|
|
|
render(
|
|
<StrictMode>
|
|
<Probe />
|
|
</StrictMode>
|
|
);
|
|
|
|
// StrictMode in a dev build mounts, tears down, and remounts every effect. If that did not
|
|
// happen, this test would not be exercising re-arming at all, so assert it explicitly rather
|
|
// than assume it.
|
|
expect(effectRuns).toBeGreaterThan(1);
|
|
|
|
// The first cleanup set the flag false; the remount must set it back to true. Remove
|
|
// `ref.current = true` from the effect body and this reads false.
|
|
expect((captured as unknown as { current: boolean }).current).toBe(true);
|
|
});
|
|
});
|