This commit is contained in:
Andras Schmelczer 2026-09-11 16:58:58 +01:00
commit 62a385e4c9
33 changed files with 2649 additions and 809 deletions

View file

@ -0,0 +1,200 @@
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('<span>Done</span>');
});
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"');
});
});