Files
apex/shared/manufacturing.test.js
T
lejayandCursor 30921aade6 v2.1
Ship configurator updates with catalog DB included for server deploy.

Co-authored-by: Cursor <[email protected]>
2026-10-07 17:13:03 +02:00

351 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
addUpperBlockAboveHorizontalSpan,
getModuleVerticalBoundariesMm,
resizeHorizontalSpanUpperBlock,
resolveHorizontalSpan,
validateHorizontalSpans,
usesBlockConstruction,
columnUsesBlockConstruction,
horizontalConstructionRuns,
getAxiloPlateCentersMm,
getAxiloLateralCenterInsetMm,
getAxiloPlinthFrontSideDeductionMm,
PLINTH
} from './manufacturing.js';
import { validateConfig } from '../server/services/configValidation.js';
import { generateCuttingList } from '../server/services/cuttingEngine.js';
function makeConfig(columnCount, spanHeightMm = 900) {
const bottomMm = 300;
const topMm = bottomMm + spanHeightMm;
const bodyMm = 1800;
const modules = Array.from({ length: columnCount }, (_, index) => ({
id: `col-${index + 1}`,
type: 'penderie',
moduleCode: 'penderie',
widthMm: 600,
minWidth: 300,
maxWidth: 1200,
shelves: 0,
drawers: 0,
hasRod: false,
blockHeightsMm: [bottomMm, spanHeightMm, bodyMm - topMm],
blockBacks: [true, index % 2 === 0, false],
blockDoors: [true, false, true],
hasBack: true,
hasBackTop: false
}));
return {
installationType: 'free',
doorType: 'hinged',
widthMm: columnCount * 600,
heightMm: 1900,
depthMm: 600,
plinthHeightMm: 100,
effectiveDrawerDepthMm: 600,
hardware: { drawerType: 'visible', handleBoreHeightMm: 1000 },
modules,
horizontalSpans: [{
id: 'span-main',
moduleIds: modules.map((module) => module.id),
bottomMm,
topMm,
hasBack: true,
doorMode: 'none',
type: 'open'
}]
};
}
function assertSynchronizedUpperBlock(config, expectedSpanTopMm, expectedUpperHeightMm) {
const resolved = resolveHorizontalSpan(config.horizontalSpans[0], config);
assert.equal(resolved.topMm, expectedSpanTopMm);
for (const module of config.modules) {
const boundaries = getModuleVerticalBoundariesMm(module, config);
assert.ok(boundaries.includes(expectedSpanTopMm));
assert.ok(boundaries.includes(1200));
const upperIndex = boundaries.indexOf(expectedSpanTopMm);
assert.equal(boundaries[upperIndex + 1] - boundaries[upperIndex], expectedUpperHeightMm);
assert.equal(module.blockDoors[upperIndex], true);
resolved.coveredBlocks
.find((covered) => covered.moduleId === module.id)
.blockIndexes
.forEach((blockIndex) => assert.equal(module.blockDoors[blockIndex], false));
assert.equal(module.blockHeightsMm.reduce((sum, height) => sum + height, 0), 1800);
}
}
for (const columnCount of [2, 3]) {
test(`${columnCount}-column span adds and resizes a synchronized upper block`, () => {
const original = makeConfig(columnCount);
const added = addUpperBlockAboveHorizontalSpan(original, 'span-main');
assert.equal(added.ok, true);
assert.notEqual(added.config, original);
assert.deepEqual(original.modules[0].blockHeightsMm, [300, 900, 600]);
assertSynchronizedUpperBlock(added.config, 900, 300);
const resized = resizeHorizontalSpanUpperBlock(added.config, 'span-main', 450);
assert.equal(resized.ok, true);
assertSynchronizedUpperBlock(resized.config, 750, 450);
assert.deepEqual(validateHorizontalSpans(resized.config), []);
assert.deepEqual(validateConfig(resized.config).errors, []);
const cuttingList = generateCuttingList(resized.config, {}, resized.config.modules);
assert.ok(Array.isArray(cuttingList.pieces));
assert.ok(cuttingList.pieces.length > 0);
});
}
test('too-short span rejects upper block without mutation', () => {
const config = makeConfig(2, 500);
const snapshot = structuredClone(config);
const result = addUpperBlockAboveHorizontalSpan(config, 'span-main');
assert.equal(result.ok, false);
assert.equal(result.code, 'span_too_short');
assert.deepEqual(config, snapshot);
});
test('resize rejects non-step heights without mutation', () => {
const added = addUpperBlockAboveHorizontalSpan(makeConfig(2), 'span-main');
assert.equal(added.ok, true);
const snapshot = structuredClone(added.config);
const result = resizeHorizontalSpanUpperBlock(added.config, 'span-main', 425);
assert.equal(result.ok, false);
assert.equal(result.code, 'invalid_height');
assert.deepEqual(added.config, snapshot);
});
test('construction: dos → block ; sinon côté ou rayonnage → block', () => {
assert.equal(usesBlockConstruction({ type: 'penderie' }, true), true);
assert.equal(usesBlockConstruction({ type: 'penderie' }, false), false);
assert.equal(usesBlockConstruction({ type: 'tiroirs' }, false), false);
assert.equal(usesBlockConstruction({ type: 'rayonnage' }, false), true);
assert.equal(usesBlockConstruction({ moduleCode: 'rayonnage' }, false), true);
assert.equal(usesBlockConstruction({ type: 'penderie', shelves: 1 }, false), false);
assert.equal(usesBlockConstruction({ type: 'penderie', shelves: 4 }, false), true);
assert.equal(usesBlockConstruction({ type: 'mixte', shelves: 2 }, false), true);
const penderie = { type: 'penderie', shelves: 1, hasBack: false, blockBacks: [false] };
const rayonnage = { type: 'rayonnage', hasBack: false, blockBacks: [false] };
const shelvedPenderie = { type: 'penderie', shelves: 4, hasBack: false, blockBacks: [false] };
const withDos = { type: 'penderie', hasBack: true, blockBacks: [true] };
const config = { hasBack: false };
assert.equal(columnUsesBlockConstruction(penderie, config), false);
assert.equal(columnUsesBlockConstruction(rayonnage, config), true);
assert.equal(columnUsesBlockConstruction(shelvedPenderie, config), true);
assert.equal(columnUsesBlockConstruction(withDos, config), true);
const modules = [
{ type: 'penderie', widthMm: 600, hasBack: false, blockBacks: [false] },
{ type: 'penderie', widthMm: 600, hasBack: false, blockBacks: [false] },
{ type: 'rayonnage', widthMm: 500, hasBack: false, blockBacks: [false] },
{ type: 'penderie', widthMm: 600, hasBack: false, blockBacks: [false] }
];
assert.deepEqual(
horizontalConstructionRuns(modules, config, 'top').map(({ start, end, isBlock }) => ({ start, end, isBlock })),
[
{ start: 0, end: 1, isBlock: false },
{ start: 2, end: 2, isBlock: true },
{ start: 3, end: 3, isBlock: false }
]
);
const allPenderie = modules.map((m, i) => (
i === 2 ? { ...m, type: 'penderie', moduleCode: 'penderie', shelves: 0 } : m
));
assert.deepEqual(
horizontalConstructionRuns(allPenderie, config, 'top').map(({ start, end, isBlock }) => ({ start, end, isBlock })),
[{ start: 0, end: 3, isBlock: false }]
);
});
test('horizontal block at shell top replaces per-column roofs with one panel', () => {
const modules = [1, 2, 3].map((index) => ({
id: `col-${index}`,
type: 'rayonnage',
widthMm: 600,
blockHeightsMm: [1800],
blockBacks: [false]
}));
const config = {
widthMm: 1800,
heightMm: 1900,
plinthHeightMm: 100,
modules,
horizontalSpans: [{
id: 'span-top',
moduleIds: ['col-1', 'col-2'],
bottomMm: 0,
topMm: 1800,
hasBack: false,
doorMode: 'none',
type: 'open'
}]
};
assert.deepEqual(
horizontalConstructionRuns(modules, config, 'top')
.map(({ start, end, horizontalSpanId }) => ({ start, end, horizontalSpanId })),
[
{ start: 0, end: 1, horizontalSpanId: 'span-top' },
{ start: 2, end: 2, horizontalSpanId: undefined }
]
);
});
test('cutting list: merged dessus for separations, split for rayonnage', () => {
const baseModule = (index, type) => ({
id: `col-${index + 1}`,
type,
moduleCode: type,
widthMm: 600,
minWidth: 300,
maxWidth: 1200,
shelves: type === 'rayonnage' ? 4 : 0,
drawers: 0,
hasRod: type === 'penderie',
blockHeightsMm: [1800],
blockBacks: [false],
blockDoors: [true],
hasBack: false,
hasBackTop: false
});
const configOf = (types) => ({
installationType: 'free',
doorType: 'hinged',
widthMm: types.length * 600,
heightMm: 1900,
depthMm: 600,
plinthHeightMm: 100,
hasBack: false,
columnMode: 'separations',
hardware: { drawerType: 'visible', handleBoreHeightMm: 1000 },
modules: types.map((type, index) => baseModule(index, type)),
horizontalSpans: []
});
const dessusPieces = (config) =>
generateCuttingList(config, {}, config.modules).pieces.filter((piece) => piece.elementType === 'dessus');
const merged = dessusPieces(configOf(['penderie', 'penderie', 'penderie']));
assert.equal(merged.length, 1);
assert.match(merged[0].id, /col1-3/);
const split = dessusPieces(configOf(['penderie', 'rayonnage', 'penderie']));
assert.equal(split.length, 3);
});
test('AXILO feet: 4 corners plus a pair every 600mm on the armoire', () => {
const inset = getAxiloLateralCenterInsetMm(true);
const narrow = getAxiloPlateCentersMm(500, 610, {
leftEdgeCenterInsetMm: inset,
rightEdgeCenterInsetMm: inset
});
assert.equal(narrow.length, 4, 'A bay shorter than 600mm keeps only the 4 corner feet');
const wide = getAxiloPlateCentersMm(2362, 610, {
leftEdgeCenterInsetMm: inset,
rightEdgeCenterInsetMm: inset
});
assert.equal(wide.length, 10, '2400mm armoire: 4 corners + 3 intermediate pairs');
const xs = [...new Set(wide.map((c) => c.xMm))].sort((a, b) => a - b);
assert.equal(xs.length, 5);
for (let i = 1; i < xs.length; i++) {
assert.ok(xs[i] - xs[i - 1] <= PLINTH.maxFootSpacingMm + 0.01);
}
const left = getAxiloPlateCentersMm(761, 610, {
leftEdgeCenterInsetMm: inset,
rightEdgeCenterInsetMm: inset,
spanWidthMm: 2362,
panelOffsetMm: 0,
assignMinMm: 0,
assignMaxMm: 781,
assignMaxInclusive: false
});
const mid = getAxiloPlateCentersMm(759, 610, {
leftEdgeCenterInsetMm: inset,
rightEdgeCenterInsetMm: inset,
spanWidthMm: 2362,
panelOffsetMm: 801.5,
assignMinMm: 781,
assignMaxMm: 1581,
assignMaxInclusive: false
});
const right = getAxiloPlateCentersMm(761, 610, {
leftEdgeCenterInsetMm: inset,
rightEdgeCenterInsetMm: inset,
spanWidthMm: 2362,
panelOffsetMm: 1601.5,
assignMinMm: 1581,
assignMaxMm: 2362,
assignMaxInclusive: true
});
assert.equal(left.length + mid.length + right.length, 10, 'Split fonds share the armoire foot count');
});
test('grooved bottom staggers the rear Ixconnect row away from AXILO holes', () => {
const config = makeConfig(1);
const bottom = generateCuttingList(config, {}, config.modules).pieces.find(
(piece) =>
piece.elementType === 'fond' &&
piece.machining?.groove &&
piece.machining?.axiloFootPlates &&
piece.machining?.carcassFasteners
);
assert.ok(bottom, 'Expected a grooved bottom with AXILO and carcass machining');
const rearFootYs = bottom.machining.axiloFootPlates.centers
.filter((center) => center.row === 'rear')
.flatMap((center) =>
bottom.machining.axiloFootPlates.holeOffsetsFromCenterMm.map(
(offset) => center.yMm + offset.y
)
);
const rearJoint = bottom.machining.carcassFasteners.dowelStations.at(-1);
const minimumAxisClearanceMm = Math.min(
...rearJoint.holesAlongMm.flatMap((jointY) =>
rearFootYs.map((footY) => Math.abs(jointY - footY))
)
);
assert.ok(
minimumAxisClearanceMm >= 14,
`Rear T–IX–T and AXILO axes need clearance; received ${minimumAxisClearanceMm} mm`
);
});
test('niche front plinth spans the full wardrobe width', () => {
const config = {
...makeConfig(3),
installationType: 'niche',
exteriorFinishesEnabled: true,
sideFinishLeftEnabled: true,
sideFinishRightEnabled: true
};
assert.equal(getAxiloPlinthFrontSideDeductionMm(config, 'left'), 0);
assert.equal(getAxiloPlinthFrontSideDeductionMm(config, 'right'), 0);
const plinth = generateCuttingList(config, {}, config.modules).pieces
.find((piece) => piece.elementType === 'plintheBas');
assert.ok(plinth);
assert.equal(plinth.widthMm, config.widthMm);
const cuttingList = generateCuttingList(config, {}, config.modules);
const sideFillers = cuttingList.pieces.find((piece) => piece.elementType === 'fileurCotes');
const topFiller = cuttingList.pieces.find((piece) => piece.elementType === 'fileurHaut');
assert.ok(sideFillers);
assert.equal(sideFillers.widthMm, 50);
assert.equal(sideFillers.heightMm, config.heightMm + 30);
assert.equal(sideFillers.quantity, 2);
assert.ok(topFiller);
assert.equal(topFiller.widthMm, config.widthMm + 60);
assert.equal(topFiller.heightMm, 50);
assert.equal(topFiller.quantity, 1);
});