deckhearth/test/components/ScannerHistoryStrip.test.js

207 lines
6.1 KiB
JavaScript
Raw Normal View History

// @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);
});
});