wip
This commit is contained in:
commit
d8f2407ddf
44 changed files with 10377 additions and 0 deletions
181
tests/export.test.ts
Normal file
181
tests/export.test.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
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, allocations 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('Remaining length 747.0 mm');
|
||||
expect(csv).toContain('Waste including kerf 879.0 mm');
|
||||
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(8);
|
||||
});
|
||||
|
||||
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(1);
|
||||
expect(csv.split('\r\n').filter((row) => row.startsWith('"Skirting stock",'))).toHaveLength(1);
|
||||
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).toContain('"Flooring stock","","F1 / READY-F","B1"');
|
||||
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);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue