floor-planner/tests/project-state.test.ts
Andras Schmelczer 8bf3e3b705
Some checks failed
Deploy to Pages / build (push) Failing after 10m45s
Simplify
2026-09-11 23:27:29 +01:00

99 lines
4.2 KiB
TypeScript

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<typeof rectangle>) => ({ ...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);
});
});