import { describe, expect, it } from 'vitest'; import { defaultState, rectangle } from '../src/lib/defaults'; import { reduceProject } from '../src/lib/project-state'; import { createPlanCalculator } from '../src/lib/planning'; describe('project edits', () => { it('preserves progress when renaming but clears all allocation locations when measurements change', () => { const state = defaultState(); state.project.completedBoards = ['floor:B1']; state.view.selectedPiece = 'flooring:F1'; state.view.focus = { kind: 'board', material: 'flooring', id: 'B1' }; state.view.search = 'F1'; const renamed = reduceProject(state, { type: 'room', id: 'room-1', change: (room) => ({ ...room, name: 'New name' }), }); expect(renamed.project.completedBoards).toBe(state.project.completedBoards); expect(renamed.view).toBe(state.view); const changed = reduceProject(renamed, { type: 'flooring', patch: { gap: 12 } }); expect(changed.project.completedBoards).toEqual([]); expect(changed.view).toMatchObject({ selectedPiece: null, search: '', focus: null }); expect(reduceProject(state, { type: 'flooring', patch: { optimisation: 'cuts' } })).toBe(state); }); it('keeps piece selection material-qualified and clears location when typing', () => { const state = reduceProject(defaultState(), { type: 'select', material: 'skirting', id: 'F1', roomId: 'room-6', }); expect(state.view.selectedPiece).toBe('skirting:F1'); const focused = reduceProject(state, { type: 'focus', focus: { kind: 'board', material: 'skirting', id: 'S1' }, roomId: 'room-6', }); expect(focused.view).toMatchObject({ selectedPiece: null, search: '' }); const searched = reduceProject(focused, { type: 'search', text: 'Kitchen', material: 'flooring', roomId: 'room-1', }); expect(searched.view.focus).toBeNull(); }); it('keeps at least one room and navigates away from a removed room', () => { const state = defaultState(); expect(reduceProject(state, { type: 'removeRoom', id: 'room-1' })).toBe(state); const added = reduceProject(state, { type: 'addRoom' }); expect(added.project.rooms).toHaveLength(2); const removed = reduceProject(added, { type: 'removeRoom', id: added.view.roomId }); expect(removed.view.roomId).toBe('room-1'); }); }); describe('calculation invalidation', () => { it('reuses geometry and plans for names, progress, and selecting an already calculated option', () => { const calculate = createPlanCalculator(); let state = defaultState(); const initial = calculate(state.project); for (const action of [ { type: 'room', id: 'room-1', change: (room: ReturnType) => ({ ...room, name: 'Renamed' }), }, { type: 'toggleBoard', key: 'floor:B1' }, { type: 'flooring', patch: { optimisation: 'random' } }, ] as const) { state = reduceProject(state, action); const next = calculate(state.project); expect(next.flooringOptions).toBe(initial.flooringOptions); expect(next.skirting).toBe(initial.skirting); expect(next.geometries).toBe(initial.geometries); } state = reduceProject(state, { type: 'flooring', patch: { gap: 12 } }); const edited = calculate(state.project); expect(edited.flooringOptions).not.toBe(initial.flooringOptions); expect(edited.geometries).toBe(initial.geometries); expect(edited.skirting).toBe(initial.skirting); }); it('rebuilds changed outlines and leaves another room geometry intact', () => { const calculate = createPlanCalculator(); const state = defaultState(); state.project.rooms.push(rectangle('room-2')); const first = calculate(state.project); const changed = reduceProject(state, { type: 'room', id: 'room-2', change: () => rectangle('room-2', 'Second', 4200, 3600), }); const second = calculate(changed.project); expect(second.geometries[0]).toBe(first.geometries[0]); expect(second.geometries[1]).not.toBe(first.geometries[1]); expect(second.flooringOptions).not.toBe(first.flooringOptions); expect(second.skirting).not.toBe(first.skirting); }); });