fix(scanner): satisfy react-hooks/immutability and jsdom ResizeObserver in tests

- Move disambiguation-cancel tracker reset into useCameraScanner's new
  resetTrackedCard(cardId) (immutable map + setTrackedCards), wired via
  onTrackerReset — the identification hook no longer mutates state-derived
  objects, clearing the blocking react-hooks/immutability lint error.
- Stub ResizeObserver in test/setup.js so ScannerCamera's workstation
  tests render under jsdom.
- Drop unused eslint-disable directive in CollectionsPageView.

npm run lint: 0 problems; vitest 231/231.
This commit is contained in:
Randall Stillwell 2026-08-23 22:27:49 -05:00
parent 6fab22d315
commit ca3b8a78c2
5 changed files with 24 additions and 5 deletions

View file

@ -1,4 +1,3 @@
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
import Link from 'next/link'; import Link from 'next/link';
import PermissionIndicator from './PermissionIndicator'; import PermissionIndicator from './PermissionIndicator';
import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js'; import { VOCAB, collectionDisplayName } from '../lib/collection-vocabulary.js';

View file

@ -176,6 +176,16 @@ export function useCameraScanner({
setTrackedCards(next); setTrackedCards(next);
}, []); }, []);
const resetTrackedCard = useCallback((cardId) => {
const next = trackedCardsRef.current.map((card) => (
card.id === cardId
? { ...card, status: 'detecting', scanAttempts: 0, negativeAt: undefined }
: card
));
trackedCardsRef.current = next;
setTrackedCards(next);
}, []);
const resetScanTrackers = useCallback(() => { const resetScanTrackers = useCallback(() => {
trackedCardsRef.current = []; trackedCardsRef.current = [];
setTrackedCards([]); setTrackedCards([]);
@ -429,6 +439,7 @@ export function useCameraScanner({
startCamera, startCamera,
stopCamera, stopCamera,
removeTrackedCard, removeTrackedCard,
resetTrackedCard,
resetScanTrackers, resetScanTrackers,
triggerManualScan, triggerManualScan,
streamRef, streamRef,

View file

@ -46,6 +46,7 @@ export function useScannerIdentification({
onCardScanned, onCardScanned,
onError, onError,
onTrackerComplete, onTrackerComplete,
onTrackerReset,
videoRef, videoRef,
canvasRef, canvasRef,
onVerifyCardRef, onVerifyCardRef,
@ -118,10 +119,9 @@ export function useScannerIdentification({
}; };
const cancelDisambiguation = () => { const cancelDisambiguation = () => {
if (disambiguation?.cardTracker) { const { cardTracker } = disambiguation ?? {};
disambiguation.cardTracker.status = 'detecting'; if (cardTracker) {
disambiguation.cardTracker.scanAttempts = 0; onTrackerReset?.(cardTracker.id);
disambiguation.cardTracker.negativeAt = undefined;
} }
setDisambiguation(null); setDisambiguation(null);
disambiguationRefineRef.current = null; disambiguationRefineRef.current = null;

View file

@ -69,6 +69,7 @@ export default function Scanner() {
onCardScanned: queue.handleCardScanned, onCardScanned: queue.handleCardScanned,
onError: (msg) => console.error('Identification error:', msg), onError: (msg) => console.error('Identification error:', msg),
onTrackerComplete: camera.removeTrackedCard, onTrackerComplete: camera.removeTrackedCard,
onTrackerReset: camera.resetTrackedCard,
videoRef: camera.videoRef, videoRef: camera.videoRef,
canvasRef: camera.canvasRef, canvasRef: camera.canvasRef,
onVerifyCardRef, onVerifyCardRef,

View file

@ -1,2 +1,10 @@
process.env.JWT_SECRET = 'test-secret-for-vitest-only-do-not-use-in-prod'; process.env.JWT_SECRET = 'test-secret-for-vitest-only-do-not-use-in-prod';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
if (!globalThis.ResizeObserver) {
globalThis.ResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
};
}