import { describe, expect, it } from 'vitest'; import { convertToVideoCoordinates, mergeDetectedShapesIntoTrackedCards, rectanglesOverlap, selectCardsReadyForVerification, TRACK_STALE_MS, } from '../../lib/scanner-card-detection.js'; describe('rectanglesOverlap', () => { it('returns true when rectangles mostly overlap', () => { const a = { x: 0, y: 0, width: 100, height: 140 }; const b = { x: 10, y: 10, width: 100, height: 140 }; expect(rectanglesOverlap(a, b, 0.3)).toBe(true); }); it('returns false when rectangles do not touch', () => { const a = { x: 0, y: 0, width: 50, height: 70 }; const b = { x: 200, y: 200, width: 50, height: 70 }; expect(rectanglesOverlap(a, b)).toBe(false); }); }); describe('convertToVideoCoordinates', () => { it('scales detection canvas coords to video pixel space', () => { const canvas = { width: 320, height: 240 }; const video = { videoWidth: 1280, videoHeight: 960 }; expect(convertToVideoCoordinates({ x: 10, y: 20, width: 100, height: 140 }, canvas, video)).toEqual({ x: 40, y: 80, width: 400, height: 560, }); }); }); describe('mergeDetectedShapesIntoTrackedCards', () => { it('creates a new tracked card for an unmatched shape', () => { const now = 1_000_000; const shape = { x: 10, y: 20, width: 100, height: 140, score: 80 }; const { cards, nextCardId } = mergeDetectedShapesIntoTrackedCards([], [shape], 1, now); expect(cards).toHaveLength(1); expect(cards[0]).toMatchObject({ id: 1, status: 'detecting', stableCount: 1, scanAttempts: 0, bounds: shape, }); expect(nextCardId).toBe(2); }); it('updates an overlapping tracked card instead of creating a duplicate', () => { const now = 1_000_000; const existing = { id: 5, bounds: { x: 10, y: 20, width: 100, height: 140 }, status: 'detecting', firstSeen: now - 500, lastSeen: now - 100, stableCount: 3, scanAttempts: 0, }; const moved = { x: 12, y: 22, width: 100, height: 140, score: 82 }; const { cards, nextCardId } = mergeDetectedShapesIntoTrackedCards([existing], [moved], 6, now); expect(cards).toHaveLength(1); expect(cards[0].id).toBe(5); expect(cards[0].stableCount).toBe(4); expect(cards[0].lastSeen).toBe(now); expect(nextCardId).toBe(6); }); it('drops cards not seen within TRACK_STALE_MS', () => { const now = 10_000; const stale = { id: 1, bounds: { x: 0, y: 0, width: 50, height: 70 }, status: 'detecting', firstSeen: now - TRACK_STALE_MS - 1, lastSeen: now - TRACK_STALE_MS - 1, stableCount: 2, scanAttempts: 0, }; const { cards } = mergeDetectedShapesIntoTrackedCards([stale], [], 2, now); expect(cards).toHaveLength(0); }); }); describe('selectCardsReadyForVerification', () => { it('returns detecting cards that are stable long enough', () => { const now = 10_000; const ready = { id: 1, status: 'detecting', stableCount: 6, scanAttempts: 0, firstSeen: now - 3000, }; const tooFresh = { id: 2, status: 'detecting', stableCount: 6, scanAttempts: 0, firstSeen: now - 1000, }; expect(selectCardsReadyForVerification([ready, tooFresh], now)).toEqual([ready]); }); it('excludes cards that already attempted verification', () => { const now = 10_000; const retried = { id: 1, status: 'detecting', stableCount: 8, scanAttempts: 1, firstSeen: now - 5000, }; expect(selectCardsReadyForVerification([retried], now)).toHaveLength(0); }); });