207 lines
6.1 KiB
JavaScript
207 lines
6.1 KiB
JavaScript
|
|
// @vitest-environment jsdom
|
||
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||
|
|
import { cleanup, fireEvent, render, screen, within } from '@testing-library/react';
|
||
|
|
|
||
|
|
import ScannerHistoryStrip, {
|
||
|
|
TAB_DUPLICATES,
|
||
|
|
TAB_QUEUE,
|
||
|
|
TAB_RECENT,
|
||
|
|
getDuplicateCards,
|
||
|
|
} from '../../components/scanner/ScannerHistoryStrip.js';
|
||
|
|
|
||
|
|
const CARD_A = {
|
||
|
|
id: 'card-a',
|
||
|
|
name: 'Lightning Bolt',
|
||
|
|
set: 'Alpha',
|
||
|
|
databaseId: 101,
|
||
|
|
quantity: 1,
|
||
|
|
processed: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
const CARD_B = {
|
||
|
|
id: 'card-b',
|
||
|
|
name: 'Lightning Bolt',
|
||
|
|
set: 'Alpha',
|
||
|
|
databaseId: 102,
|
||
|
|
quantity: 1,
|
||
|
|
processed: false,
|
||
|
|
};
|
||
|
|
|
||
|
|
const CARD_C = {
|
||
|
|
id: 'card-c',
|
||
|
|
name: 'Counterspell',
|
||
|
|
set: 'Beta',
|
||
|
|
databaseId: 201,
|
||
|
|
quantity: 2,
|
||
|
|
processed: true,
|
||
|
|
};
|
||
|
|
|
||
|
|
function renderStrip(overrides = {}) {
|
||
|
|
const props = {
|
||
|
|
scannedCards: [],
|
||
|
|
ownershipMap: {},
|
||
|
|
focusedCardId: null,
|
||
|
|
onFocusCard: vi.fn(),
|
||
|
|
activeTab: TAB_RECENT,
|
||
|
|
onTabChange: vi.fn(),
|
||
|
|
batchProgress: null,
|
||
|
|
onClearAll: vi.fn(),
|
||
|
|
onCommitSelectedToOwned: vi.fn(),
|
||
|
|
onOpenListPicker: vi.fn(),
|
||
|
|
isProcessing: false,
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
...render(<ScannerHistoryStrip {...props} />),
|
||
|
|
props,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('getDuplicateCards', () => {
|
||
|
|
it('flags session name/set repeats', () => {
|
||
|
|
const result = getDuplicateCards([CARD_A, CARD_B], {});
|
||
|
|
expect(result.map((card) => card.id)).toEqual(['card-b']);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('flags quantity greater than one', () => {
|
||
|
|
const solo = { ...CARD_C, quantity: 2 };
|
||
|
|
const result = getDuplicateCards([solo], {});
|
||
|
|
expect(result).toHaveLength(1);
|
||
|
|
expect(result[0].id).toBe('card-c');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('flags cards already in ownershipMap by databaseId', () => {
|
||
|
|
const owned = { ...CARD_A, databaseId: 999 };
|
||
|
|
const result = getDuplicateCards([owned], { 999: { quantity: 1 } });
|
||
|
|
expect(result).toHaveLength(1);
|
||
|
|
expect(result[0].id).toBe('card-a');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('ScannerHistoryStrip', () => {
|
||
|
|
afterEach(() => cleanup());
|
||
|
|
|
||
|
|
it('renders three tabs with badge counts in aria-label', () => {
|
||
|
|
renderStrip({
|
||
|
|
scannedCards: [CARD_A, CARD_B, CARD_C],
|
||
|
|
activeTab: TAB_RECENT,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByRole('tab', { name: 'Recent Scans' })).toBeTruthy();
|
||
|
|
expect(screen.getByRole('tab', { name: 'Scan Queue, 2 unprocessed cards' })).toBeTruthy();
|
||
|
|
expect(screen.getByRole('tab', { name: 'Duplicates, 2 duplicate cards' })).toBeTruthy();
|
||
|
|
expect(screen.getByRole('tab', { name: 'Scan Queue, 2 unprocessed cards' }).innerHTML).not.toContain('#ffffff');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls onTabChange when a tab is clicked', () => {
|
||
|
|
const { props } = renderStrip({ scannedCards: [CARD_A] });
|
||
|
|
|
||
|
|
fireEvent.click(screen.getByRole('tab', { name: /scan queue/i }));
|
||
|
|
expect(props.onTabChange).toHaveBeenCalledWith(TAB_QUEUE);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows empty states for each tab', () => {
|
||
|
|
const { rerender, props } = renderStrip({ activeTab: TAB_RECENT });
|
||
|
|
expect(screen.getByText('No scans yet this session.')).toBeTruthy();
|
||
|
|
|
||
|
|
rerender(<ScannerHistoryStrip {...props} activeTab={TAB_QUEUE} />);
|
||
|
|
expect(
|
||
|
|
screen.getByText('Scan queue is empty — matches appear here before you add them.')
|
||
|
|
).toBeTruthy();
|
||
|
|
|
||
|
|
rerender(<ScannerHistoryStrip {...props} activeTab={TAB_DUPLICATES} />);
|
||
|
|
expect(screen.getByText('No duplicates detected.')).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('row click invokes onFocusCard without commit handlers', () => {
|
||
|
|
const onFocusCard = vi.fn();
|
||
|
|
const onCommitSelectedToOwned = vi.fn();
|
||
|
|
const onOpenListPicker = vi.fn();
|
||
|
|
|
||
|
|
renderStrip({
|
||
|
|
scannedCards: [CARD_A],
|
||
|
|
activeTab: TAB_RECENT,
|
||
|
|
onFocusCard,
|
||
|
|
onCommitSelectedToOwned,
|
||
|
|
onOpenListPicker,
|
||
|
|
});
|
||
|
|
|
||
|
|
fireEvent.click(screen.getByRole('button', { name: /lightning bolt/i }));
|
||
|
|
expect(onFocusCard).toHaveBeenCalledWith('card-a');
|
||
|
|
expect(onCommitSelectedToOwned).not.toHaveBeenCalled();
|
||
|
|
expect(onOpenListPicker).not.toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('applies focused styling when focusedCardId matches a chip', () => {
|
||
|
|
renderStrip({
|
||
|
|
scannedCards: [CARD_A],
|
||
|
|
activeTab: TAB_RECENT,
|
||
|
|
focusedCardId: 'card-a',
|
||
|
|
});
|
||
|
|
|
||
|
|
const chip = screen.getByRole('button', { name: /lightning bolt/i });
|
||
|
|
expect(chip.getAttribute('aria-current')).toBe('true');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows batch progress banner with cancel in Scan Queue tab', () => {
|
||
|
|
const onCancel = vi.fn();
|
||
|
|
renderStrip({
|
||
|
|
scannedCards: [CARD_A],
|
||
|
|
activeTab: TAB_QUEUE,
|
||
|
|
batchProgress: { active: true, current: 2, total: 5, onCancel },
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByText('Scanning 2 of 5…')).toBeTruthy();
|
||
|
|
fireEvent.click(screen.getByRole('button', { name: /^cancel$/i }));
|
||
|
|
expect(onCancel).toHaveBeenCalledTimes(1);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('disables Clear All while batch progress is active', () => {
|
||
|
|
renderStrip({
|
||
|
|
scannedCards: [CARD_A],
|
||
|
|
batchProgress: { active: true, current: 1, total: 3, onCancel: vi.fn() },
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByRole('button', { name: /clear all/i }).disabled).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows identify failed label on failed queue rows', () => {
|
||
|
|
const failedCard = {
|
||
|
|
...CARD_A,
|
||
|
|
identifyFailed: true,
|
||
|
|
identifyFailureReason: 'No match found',
|
||
|
|
};
|
||
|
|
|
||
|
|
renderStrip({
|
||
|
|
scannedCards: [failedCard],
|
||
|
|
activeTab: TAB_QUEUE,
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(screen.getByText('Identify failed')).toBeTruthy();
|
||
|
|
expect(screen.getByText('No match found')).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('lists recent scans newest first', () => {
|
||
|
|
const older = { ...CARD_A, id: 'older', name: 'Older Card' };
|
||
|
|
const newer = { ...CARD_B, id: 'newer', name: 'Newer Card', set: 'New Set' };
|
||
|
|
|
||
|
|
renderStrip({
|
||
|
|
scannedCards: [older, newer],
|
||
|
|
activeTab: TAB_RECENT,
|
||
|
|
});
|
||
|
|
|
||
|
|
const panel = screen.getByRole('tabpanel', { name: /recent scans/i });
|
||
|
|
const buttons = within(panel).getAllByRole('button');
|
||
|
|
expect(buttons[0].textContent).toContain('Newer Card');
|
||
|
|
expect(buttons[1].textContent).toContain('Older Card');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('exposes tablist semantics', () => {
|
||
|
|
renderStrip({ scannedCards: [CARD_A] });
|
||
|
|
expect(screen.getByRole('tablist', { name: 'Scan history' })).toBeTruthy();
|
||
|
|
expect(screen.getAllByRole('tab')).toHaveLength(3);
|
||
|
|
expect(screen.getAllByRole('tabpanel', { hidden: true })).toHaveLength(3);
|
||
|
|
});
|
||
|
|
});
|