import { describe, expect, it } from 'vitest'; import { defaultProject } from '../src/lib/defaults'; import { cutListCsv } from '../src/lib/export'; import { flooringCuts, isSkirtingCut, uncutFlooring, uncutSkirting } from '../src/lib/cut-list'; import type { FlooringPlan, SkirtingPlan } from '../src/lib/types'; const floor: FlooringPlan = { pieces: [ { id: 'F1', roomId: 'room-1', row: 1, column: 1, polygon: [], length: 450, width: 190, stockId: 'B1', reused: true, cut: true, shaped: true, template: [ { x: 0, y: 0 }, { x: 450, y: 0 }, { x: 450, y: 190 }, { x: 0, y: 100 }, ], }, ], rows: [], stocks: [{ id: 'B1', pieceIds: ['F1'], remaining: 747 }], boardCount: 1, cuts: 2, wasteArea: 0, wastePercent: 0, reusedCount: 1, coveredArea: 65250, issues: [{ severity: 'warning', message: 'Check the shaped edge.' }], }; const skirt: SkirtingPlan = { pieces: [ { id: 'A1', roomId: 'room-1', wallId: 'room-1-w1', wallIndex: 0, start: { x: 0, y: 0 }, end: { x: 1500, y: 0 }, length: 1518, backLength: 1500, leftMitre: 0, rightMitre: 45, leftType: 'square', rightType: 'external', stockId: 'S1', }, ], stocks: [{ id: 'S1', length: 2400, used: 1521, waste: 879, pieceIds: ['A1'] }], stockCount: 1, cuts: 1, waste: 879, issues: [], }; describe('cut list CSV', () => { it('exports measured cuts, shape coordinates, mitres, board references and purchase counts', () => { const csv = cutListCsv(defaultProject(), floor, skirt); expect(csv.startsWith('\uFEFF"Material"')).toBe(true); expect(csv).toContain('"Flooring","Living room","F1","B1","450","190"'); expect(csv).toContain('Reused offcut; Shaped cut'); expect(csv).toContain('0.0:0.0 | 450.0:0.0 | 450.0:190.0 | 0.0:100.0'); expect(csv).toContain('"Skirting","Living room","A1","S1","1518","18","0","45"'); expect(csv).toContain('Back length 1500.0 mm; left square; right external'); expect(csv).toContain('Boards to buy; 1 installed pieces; 2 saw cuts'); expect(csv).not.toContain('Check the shaped edge.'); expect(csv.split('\r\n')).toHaveLength(6); }); it('counts uncut full boards and lengths without exporting their codes or source allocations', () => { const project = defaultProject(); const wholeFloor = { ...floor.pieces[0], id: 'FULL-F', stockId: 'FULL-B', length: 1200, width: 190, cut: false, shaped: false, reused: false, }; const readyOffcut = { ...floor.pieces[0], id: 'READY-F', length: 747, cut: false, shaped: false, }; const floorPlan: FlooringPlan = { ...floor, boardCount: 2, pieces: [...floor.pieces, wholeFloor, readyOffcut], stocks: [ { ...floor.stocks[0], pieceIds: ['F1', 'READY-F'], remaining: 0 }, { id: 'FULL-B', pieceIds: ['FULL-F'], remaining: 0 }, ], }; const wholeSkirt = { ...skirt.pieces[0], id: 'FULL-A', stockId: 'FULL-S', length: 2400, backLength: 2400, leftMitre: 0, rightMitre: 0, leftType: 'square' as const, rightType: 'square' as const, }; const skirtPlan: SkirtingPlan = { ...skirt, stockCount: 2, pieces: [...skirt.pieces, wholeSkirt], stocks: [ ...skirt.stocks, { id: 'FULL-S', length: 2400, used: 2400, waste: 0, pieceIds: ['FULL-A'] }, ], }; const csv = cutListCsv(project, floorPlan, skirtPlan); for (const code of ['FULL-F', 'FULL-B', 'FULL-A', 'FULL-S']) expect(csv).not.toContain(code); expect(csv).toContain('"Flooring uncut boards","","","","1200","190","","","1"'); expect(csv).toContain('"Flooring ready offcuts","","","","","","","","1"'); expect(csv).toContain('"Skirting uncut lengths","","","","2400","18","","","1"'); expect(csv).toContain('"Flooring total","","","","1200","190","","","2"'); expect(csv).toContain('"Skirting total","","","","","18","","","2"'); expect(csv.split('\r\n').filter((row) => row.startsWith('"Flooring",'))).toHaveLength(2); expect(csv.split('\r\n').filter((row) => row.startsWith('"Skirting",'))).toHaveLength(1); expect(csv.split('\r\n').filter((row) => row.startsWith('"Flooring stock",'))).toHaveLength(0); expect(csv.split('\r\n').filter((row) => row.startsWith('"Skirting stock",'))).toHaveLength(0); expect(flooringCuts(floorPlan)).toEqual([...floor.pieces, readyOffcut]); expect(csv).toContain('"Flooring","Living room","READY-F","B1","747","190"'); expect(csv).toContain('Reused offcut; Use offcut; no new cut'); expect(csv).not.toContain('"Flooring stock"'); expect(uncutFlooring(floorPlan, project.flooring)).toEqual({ wholeBoards: 1, readyOffcuts: 1 }); expect(uncutSkirting(skirtPlan)).toEqual([{ length: 2400, count: 1 }]); }); it('keeps full-length mitred pieces and shortened square skirting in the cut list', () => { const mitred = { ...skirt.pieces[0], length: 2400 }; const shortened = { ...skirt.pieces[0], leftMitre: 0, rightMitre: 0 }; const uncut = { ...shortened, length: 2400 }; expect(isSkirtingCut(mitred, skirt)).toBe(true); expect(isSkirtingCut(shortened, skirt)).toBe(true); expect(isSkirtingCut(uncut, skirt)).toBe(false); expect(isSkirtingCut({ ...uncut, stockId: 'missing' }, skirt)).toBe(true); const csv = cutListCsv(defaultProject(), floor, { ...skirt, pieces: [mitred] }); expect(csv).toContain('"Skirting","Living room","A1","S1","2400","18","0","45"'); }); it('quotes commas, double quotes, newlines and Unicode room names', () => { const project = defaultProject(); project.rooms[0].name = 'Café 北, "main"\nroom'; const csv = cutListCsv(project, floor, skirt); expect(csv).toContain('"Café 北, ""main""\nroom"'); }); it.each(['=SUM(1,1)', '+cmd', '-formula', '@formula'])( 'neutralizes spreadsheet formulas in user text %s', (name) => { const project = defaultProject(); project.rooms[0].name = name; expect(cutListCsv(project, floor, skirt)).toContain(`"'${name}"`); }, ); it('produces identical output for identical input without mutating the plans', () => { const project = defaultProject(); const before = structuredClone({ project, floor, skirt }); expect(cutListCsv(project, floor, skirt)).toBe(cutListCsv(project, floor, skirt)); expect({ project, floor, skirt }).toEqual(before); }); });