99 lines
4.2 KiB
TypeScript
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);
|
|
});
|
|
});
|