132 lines
4.6 KiB
JavaScript
132 lines
4.6 KiB
JavaScript
|
|
// @vitest-environment jsdom
|
||
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||
|
|
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||
|
|
|
||
|
|
import ScannerResultPanel from '../../components/scanner/ScannerResultPanel.js';
|
||
|
|
import { VOCAB } from '../../lib/collection-vocabulary.js';
|
||
|
|
|
||
|
|
const FOCUSED_CARD = {
|
||
|
|
id: 'card-1',
|
||
|
|
name: 'Lightning Bolt',
|
||
|
|
set: 'Alpha',
|
||
|
|
rarity: 'Common',
|
||
|
|
cardNumber: '161',
|
||
|
|
image_url: 'https://example.com/bolt.jpg',
|
||
|
|
condition: 'NM',
|
||
|
|
isFoil: false,
|
||
|
|
confidence: 0.92,
|
||
|
|
};
|
||
|
|
|
||
|
|
function createQueueFixture({ addingCardIds = new Set() } = {}) {
|
||
|
|
return {
|
||
|
|
addingCardIds,
|
||
|
|
updateCardMetadata: vi.fn(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function renderPanel(overrides = {}) {
|
||
|
|
const props = {
|
||
|
|
focusedCard: null,
|
||
|
|
queue: createQueueFixture(),
|
||
|
|
isIdentifying: false,
|
||
|
|
commitError: null,
|
||
|
|
onAddToOwned: vi.fn(),
|
||
|
|
onAddToList: vi.fn(),
|
||
|
|
onRescan: vi.fn(),
|
||
|
|
...overrides,
|
||
|
|
};
|
||
|
|
|
||
|
|
return {
|
||
|
|
...render(<ScannerResultPanel {...props} />),
|
||
|
|
props,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('ScannerResultPanel', () => {
|
||
|
|
afterEach(() => cleanup());
|
||
|
|
|
||
|
|
it('renders empty state illustration and instructional copy', () => {
|
||
|
|
renderPanel();
|
||
|
|
|
||
|
|
expect(
|
||
|
|
screen.getByText('Point your camera at a card or upload an image to see a match.')
|
||
|
|
).toBeTruthy();
|
||
|
|
expect(screen.getByRole('heading', { name: 'Scan Result' })).toBeTruthy();
|
||
|
|
expect(screen.queryByRole('button', { name: VOCAB.ADD_TO_MY_COLLECTION })).toBeNull();
|
||
|
|
expect(screen.queryByRole('button', { name: /upload/i })).toBeNull();
|
||
|
|
expect(screen.queryByRole('button', { name: /batch/i })).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows Identifying… placeholder when identifying without a focused card', () => {
|
||
|
|
renderPanel({ isIdentifying: true });
|
||
|
|
|
||
|
|
expect(screen.getByText('Identifying…')).toBeTruthy();
|
||
|
|
expect(
|
||
|
|
screen.queryByText('Point your camera at a card or upload an image to see a match.')
|
||
|
|
).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('renders populated card metadata, controls, and vocab action labels', () => {
|
||
|
|
const queue = createQueueFixture();
|
||
|
|
renderPanel({ focusedCard: FOCUSED_CARD, queue });
|
||
|
|
|
||
|
|
expect(screen.getByRole('heading', { name: FOCUSED_CARD.name, level: 3 })).toBeTruthy();
|
||
|
|
expect(screen.getByText('Alpha · Common · 161')).toBeTruthy();
|
||
|
|
expect(screen.getByRole('combobox', { name: `Condition for ${FOCUSED_CARD.name}` })).toBeTruthy();
|
||
|
|
expect(screen.getByRole('switch', { name: 'Foil' })).toBeTruthy();
|
||
|
|
expect(screen.getByText('92%')).toBeTruthy();
|
||
|
|
expect(screen.getByText('Excellent match.')).toBeTruthy();
|
||
|
|
expect(screen.getByRole('button', { name: VOCAB.ADD_TO_MY_COLLECTION })).toBeTruthy();
|
||
|
|
expect(screen.getByRole('button', { name: 'Rescan' })).toBeTruthy();
|
||
|
|
expect(screen.getByRole('button', { name: VOCAB.ADD_TO_LIST })).toBeTruthy();
|
||
|
|
expect(screen.queryByText(/wishlist/i)).toBeNull();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('disables the primary add button while the card is adding', () => {
|
||
|
|
const queue = createQueueFixture({ addingCardIds: new Set(['card-1']) });
|
||
|
|
renderPanel({ focusedCard: FOCUSED_CARD, queue });
|
||
|
|
|
||
|
|
const primaryButton = screen.getByRole('button', { name: VOCAB.ADD_TO_MY_COLLECTION });
|
||
|
|
expect(primaryButton.disabled).toBe(true);
|
||
|
|
expect(primaryButton.getAttribute('aria-busy')).toBe('true');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls queue.updateCardMetadata when condition changes', () => {
|
||
|
|
const queue = createQueueFixture();
|
||
|
|
renderPanel({ focusedCard: FOCUSED_CARD, queue });
|
||
|
|
|
||
|
|
fireEvent.change(screen.getByRole('combobox', { name: `Condition for ${FOCUSED_CARD.name}` }), {
|
||
|
|
target: { value: 'LP' },
|
||
|
|
});
|
||
|
|
|
||
|
|
expect(queue.updateCardMetadata).toHaveBeenCalledWith('card-1', { condition: 'LP' });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('calls queue.updateCardMetadata when foil switch is toggled', () => {
|
||
|
|
const queue = createQueueFixture();
|
||
|
|
renderPanel({ focusedCard: FOCUSED_CARD, queue });
|
||
|
|
|
||
|
|
fireEvent.click(screen.getByRole('switch', { name: 'Foil' }));
|
||
|
|
|
||
|
|
expect(queue.updateCardMetadata).toHaveBeenCalledWith('card-1', { isFoil: true });
|
||
|
|
});
|
||
|
|
|
||
|
|
it('shows commit errors with role=alert', () => {
|
||
|
|
renderPanel({
|
||
|
|
focusedCard: FOCUSED_CARD,
|
||
|
|
commitError: 'Could not add card. Try again.',
|
||
|
|
});
|
||
|
|
|
||
|
|
const alert = screen.getByRole('alert');
|
||
|
|
expect(alert.textContent).toContain('Could not add card. Try again.');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('includes reduced-motion CSS for the confidence bar', () => {
|
||
|
|
const { container } = renderPanel({ focusedCard: FOCUSED_CARD });
|
||
|
|
|
||
|
|
expect(container.innerHTML).toContain('scanner-result-confidence-bar-fill');
|
||
|
|
expect(container.innerHTML).toContain('prefers-reduced-motion: reduce');
|
||
|
|
expect(container.innerHTML).toContain('transition: none');
|
||
|
|
});
|
||
|
|
});
|