// @vitest-environment jsdom // // Regression lock for PR #144 (`31da384`, 2026-06-13) — `useFocusTrap` // was called by this component but never imported, shipping a runtime // `ReferenceError: useFocusTrap is not defined` to production. The // `enable-no-undef-eslint-rule` convoy closes that bug class at lint // time; this file additionally exercises the component at render time // so the regression is locked in BOTH static-analysis AND // dynamic-execution paths. A future identical bug (call a hook without // importing it) would fail this test even if someone disabled the // lint rule. // // The "renders without crashing" test is intentionally the FIRST // assertion — it's the cheapest failure mode and most directly tied // to the original PR #144 regression. import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, screen, fireEvent, cleanup } from '@testing-library/react'; import ScanDisambiguationDialog from '../../components/ScanDisambiguationDialog'; const baseFixture = { message: 'Multiple matches found. Pick one.', candidates: [ { id: 'card-1', name: 'Lightning Bolt', set_name: 'Alpha', set_code: 'LEA', card_number: '161', image_url: 'https://cards.scryfall.io/normal/front/1/1.jpg', }, { id: 'card-2', name: 'Lightning Bolt', set_name: 'Beta', set_code: 'LEB', card_number: '161', // intentionally no image — exercises the placeholder branch }, ], }; const noop = () => {}; describe('ScanDisambiguationDialog', () => { afterEach(() => cleanup()); // ── Regression-lock for PR #144 ───────────────────────────────────── // If `useFocusTrap` (or any other imported identifier) is missing, // React's render throws `ReferenceError` from inside the function // component, and this assertion fails. Do NOT relax this test to // accept thrown errors — that's the entire point. it('renders without crashing when given a disambiguation (PR #144 regression-lock)', () => { expect(() => { render( ); }).not.toThrow(); }); it('returns null when disambiguation prop is falsy', () => { const { container } = render( ); expect(container.querySelector('[role="dialog"]')).toBeNull(); }); it('renders dialog with the correct ARIA shape', () => { render( ); const dialog = screen.getByRole('dialog'); expect(dialog.getAttribute('aria-modal')).toBe('true'); expect(dialog.getAttribute('aria-labelledby')).toBe('disambiguation-title'); expect(screen.getByText('Which card is this?')).toBeTruthy(); expect(screen.getByText(baseFixture.message)).toBeTruthy(); }); it('renders one button per candidate with accessible labels', () => { render( ); const buttons = screen.getAllByRole('button', { name: /Select Lightning Bolt/i }); expect(buttons).toHaveLength(2); expect(buttons[0].getAttribute('aria-label')).toContain('Alpha'); expect(buttons[1].getAttribute('aria-label')).toContain('Beta'); }); it('calls onPick with the selected candidate', () => { const onPick = vi.fn(); render( ); fireEvent.click(screen.getByRole('button', { name: /Select Lightning Bolt, Alpha/i })); expect(onPick).toHaveBeenCalledTimes(1); expect(onPick).toHaveBeenCalledWith(baseFixture.candidates[0]); }); it('renders the vision hint when one is provided', () => { render( ); expect(screen.getByText(/Vision detected set: LEA/i)).toBeTruthy(); }); it('disables the "send for review" button while submitting and shows in-flight copy', () => { render( ); const reviewBtn = screen.getByRole('button', { name: /Submitting/i }); expect(reviewBtn.hasAttribute('disabled')).toBe(true); }); it('calls onCancel when the Cancel button is clicked', () => { const onCancel = vi.fn(); render( ); fireEvent.click(screen.getByRole('button', { name: /^Cancel$/i })); expect(onCancel).toHaveBeenCalledTimes(1); }); });