import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { describe, expect, it } from 'vitest'; import { defaultProject, rectangle } from '../src/lib/defaults'; import { boardSearchLabel, findBoardMatches } from '../src/lib/board-tracking'; import { CutList } from '../src/components/CutList'; import type { FlooringPiece, FlooringPlan, SkirtingPiece, SkirtingPlan } from '../src/lib/types'; const project = defaultProject(); project.rooms.push(rectangle('room-2', 'Office')); function floorPiece(id: string, stockId: string, roomId = 'room-1', cut = true): FlooringPiece { return { id, stockId, roomId, cut, length: cut ? 400 : 1200, width: 190, row: 1, column: 1, polygon: [], template: [], shaped: false, reused: false, }; } const flooring: FlooringPlan = { pieces: [ floorPiece('F1', 'B1'), floorPiece('F12', 'B1', 'room-2'), floorPiece('F10', 'B10'), floorPiece('FU1', 'B3', 'room-2', false), ], rows: [], stocks: [ { id: 'B1', pieceIds: ['F1', 'F12'], remaining: 394 }, { id: 'B10', pieceIds: ['F10'], remaining: 797 }, { id: 'B3', pieceIds: ['FU1'], remaining: 0 }, ], boardCount: 3, cuts: 3, wasteArea: 0, wastePercent: 0, reusedCount: 0, coveredArea: 0, issues: [], }; function skirtPiece(id: string, stockId: string, roomId = 'room-1', length = 900): SkirtingPiece { return { id, stockId, roomId, length, backLength: length, wallId: 'w1', wallIndex: 0, start: { x: 0, y: 0 }, end: { x: length, y: 0 }, leftMitre: 0, rightMitre: 0, leftType: 'square', rightType: 'square', }; } const skirting: SkirtingPlan = { pieces: [ skirtPiece('A2', 'S6'), skirtPiece('B1', 'S6', 'room-2', 1497), skirtPiece('B10', 'S60'), skirtPiece('SU1', 'S7', 'room-2', 2400), ], stocks: [ { id: 'S6', length: 2400, used: 2397, waste: 3, pieceIds: ['A2', 'B1'] }, { id: 'S60', length: 2400, used: 900, waste: 1500, pieceIds: ['B10'] }, { id: 'S7', length: 2400, used: 2400, waste: 0, pieceIds: ['SU1'] }, ], stockCount: 3, cuts: 3, waste: 1503, issues: [], }; const find = (query: string) => findBoardMatches(project, flooring, skirting, query); const noop = () => {}; function list(search = '', completedBoards: string[] = []) { return renderToStaticMarkup( createElement(CutList, { project, flooring, skirting, select: noop, search, setSearch: noop, completedBoards, toggleBoard: noop, locateBoard: noop, locatePiece: noop, }), ); } describe('board tracking search', () => { it('matches cut and stock codes exactly instead of confusing F1 with F10/F12 or B1 with B10', () => { expect(find(' F1 ')).toEqual({ pieceIds: ['F1'], roomIds: ['room-1'], boardKeys: ['floor:B1'], }); expect(find('b1')).toEqual({ pieceIds: ['F1', 'F12', 'B1'], roomIds: ['room-1', 'room-2'], boardKeys: ['floor:B1', 'skirting:S6'], }); expect(find('S6').pieceIds).toEqual(['A2', 'B1']); expect(find('B10').pieceIds).toEqual(['F10', 'B10']); }); it('uses material-qualified source keys for unambiguous board location and includes all its rooms', () => { expect(find(' FLOOR:B1 ')).toEqual({ pieceIds: ['F1', 'F12'], roomIds: ['room-1', 'room-2'], boardKeys: ['floor:B1'], }); expect(find('skirting:S6')).toEqual({ pieceIds: ['A2', 'B1'], roomIds: ['room-1', 'room-2'], boardKeys: ['skirting:S6'], }); }); it('finds full uncut boards as well as cuts, and searches room names without case sensitivity', () => { expect(find('b3')).toEqual({ pieceIds: ['FU1'], roomIds: ['room-2'], boardKeys: ['floor:B3'] }); expect(find('s7')).toEqual({ pieceIds: ['SU1'], roomIds: ['room-2'], boardKeys: ['skirting:S7'], }); expect(find(' OfFiCe ')).toEqual({ pieceIds: ['F12', 'FU1', 'B1', 'SU1'], roomIds: ['room-2'], boardKeys: ['floor:B1', 'floor:B3', 'skirting:S6', 'skirting:S7'], }); expect(find('living').roomIds).toEqual(['room-1']); }); it('returns empty matches for empty or unknown queries and never mutates the plans', () => { const before = structuredClone({ project, flooring, skirting }); expect(find(' ')).toEqual({ pieceIds: [], roomIds: [], boardKeys: [] }); expect(find('missing')).toEqual({ pieceIds: [], roomIds: [], boardKeys: [] }); expect(find('B1')).toEqual(find('B1')); expect({ project, flooring, skirting }).toEqual(before); }); it('keeps all cuts from a matching source together and applies completion to the whole group', () => { const markup = list('F1', ['floor:B1']); expect(markup).toContain('data-board-key="floor:B1"'); expect(markup).not.toContain('data-board-key="floor:B10"'); expect(markup).not.toContain('data-board-key="skirting:S6"'); expect(markup).toContain('Preview flooring cut F1'); expect(markup).toContain('Preview flooring cut F12'); expect(markup.match(/class="board-cut is-done/g)).toHaveLength(2); expect(markup).toContain('aria-label="Mark cuts F1 + F12 done" checked=""'); expect(markup).toContain('Show cuts F1 + F12 on plan'); expect(markup).toContain('Show cut F12 on plan'); expect(markup).toContain('Done'); }); it('names board groups and actions using their cuts while keeping bookkeeping IDs internal', () => { const markup = list(); expect(markup).toContain('aria-label="Flooring cuts F1 + F12"'); expect(markup).toContain('aria-label="Skirting cuts A2 + B1"'); expect(markup).toContain('Mark cuts A2 + B1 done'); expect(markup).toContain('data-board-key="skirting:S6"'); expect(markup).toContain('Find a cut or room'); expect(markup).not.toMatch(/(?:Floorboard B\d|Skirting board S\d|Export CSV)/i); expect(markup).not.toMatch(/aria-label="[^"]*(?:floor:B\d|skirting:S\d)/i); }); it('shows cut names when a located board is stored as a qualified source key', () => { expect(boardSearchLabel(project, flooring, skirting, 'floor:B1')).toBe('F1 + F12'); expect(boardSearchLabel(project, flooring, skirting, 'skirting:S6')).toBe('A2 + B1'); expect(boardSearchLabel(project, flooring, skirting, 'Office')).toBe('Office'); expect(list('floor:B1')).toContain('value="F1 + F12"'); expect(list('skirting:S6')).toContain('value="A2 + B1"'); expect(find('F1 + F12')).toEqual(find('floor:B1')); // B1 here is a skirting cut, not the hidden flooring stock with the same internal code. expect(find('A2 + B1')).toEqual(find('skirting:S6')); }); it('exposes an interactive full-board result only when searched, without inventing a cut preview', () => { expect(list()).not.toContain('data-board-key="floor:B3"'); const markup = list('floor:B3'); expect(markup).toContain('data-board-key="floor:B3"'); expect(markup).toContain('Mark uncut floorboard done'); expect(markup).toContain('Show uncut floorboard on plan'); expect(markup).toContain('Installed uncut'); expect(markup).toContain('value="Uncut floorboard"'); expect(markup).not.toContain('Preview flooring cut FU1'); expect(list('skirting:S7')).toContain('value="Uncut skirting board"'); }); });