mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
test(smoke): cover wall-face overlap, duplicate and nesting
Validate / docs (push) Failing after 22s
Validate / reuse (push) Successful in 37s
Validate / changes (push) Successful in 1m20s
Validate / provenance (push) Successful in 1m26s
Validate / process-gate (push) Failing after 1m25s
Validate / hacs (push) Failing after 14s
Validate / hassfest (push) Failing after 17s
Validate / frontend (push) Successful in 6m28s
Validate / backend (push) Failing after 6m39s
Validate / golden (push) Failing after 15m20s
Validate / performance_smoke (push) Failing after 18m21s
Validate / smoke (push) Failing after 36m34s
Validate / docs (push) Failing after 22s
Validate / reuse (push) Successful in 37s
Validate / changes (push) Successful in 1m20s
Validate / provenance (push) Successful in 1m26s
Validate / process-gate (push) Failing after 1m25s
Validate / hacs (push) Failing after 14s
Validate / hassfest (push) Failing after 17s
Validate / frontend (push) Successful in 6m28s
Validate / backend (push) Failing after 6m39s
Validate / golden (push) Failing after 15m20s
Validate / performance_smoke (push) Failing after 18m21s
Validate / smoke (push) Failing after 36m34s
Issue: #177 User-Visible: no
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Issue #177: пересечения при закрытии контура инструментом «Стены» (#173).
|
||||
*
|
||||
* AC8 задачи #173 обещал доказательство «unit + smoke» для трёх исходов, но
|
||||
* доказан был только сам примитив `_overlapRoom` — вызовом напрямую в
|
||||
* `smoke_island_rooms`, минуя предложение грани. Здесь проверяется та связка,
|
||||
* которую видит пользователь: `_offerWallFaces` (предлагать ли грань) и
|
||||
* `_applyWallFaceBatch` (вторая проверка при создании), обе из #173 §10.3.
|
||||
*
|
||||
* Три исхода контракта:
|
||||
* 1. точный дубль существующей комнаты не предлагается;
|
||||
* 2. частичное пересечение не предлагается и ничего не меняет в конфиге;
|
||||
* 3. полная вложенность в обе стороны предлагается и создаёт island room.
|
||||
*/
|
||||
import { launch, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 1100, height: 820 }, 1);
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const card = window.__card;
|
||||
const update = async () => { card.requestUpdate(); await card.updateComplete; };
|
||||
const space = () => card._serverCfg.spaces[0];
|
||||
const ring = (x0, y0, x1, y1) => [[x0, y0], [x1, y0], [x1, y1], [x0, y1]];
|
||||
const closed = (r) => [...r, [...r[0]]];
|
||||
|
||||
const resetGeometry = async (rooms = []) => {
|
||||
const current = space();
|
||||
current.rooms = JSON.parse(JSON.stringify(rooms));
|
||||
delete current.room_drafts;
|
||||
delete current.partitions;
|
||||
delete current.walls;
|
||||
delete current.open_spans;
|
||||
card._cfgEpoch++;
|
||||
card._modelCache = null;
|
||||
card._path = [];
|
||||
card._activeDraftId = null;
|
||||
card._draftSegmentCms = [];
|
||||
card._wallFaceBatch = null;
|
||||
card._roomDialog = false;
|
||||
card._tool = 'draw';
|
||||
await update();
|
||||
};
|
||||
// Та же цепочка вызовов, что у настоящего клика по плану: точка, запись
|
||||
// отрезка, предложение граней. Подставлять `_path` целиком нельзя — тогда
|
||||
// проверялся бы не тот путь, по которому ходит пользователь.
|
||||
const draw = async (points) => {
|
||||
card._tool = 'draw';
|
||||
card._path = [[...points[0]]];
|
||||
card._activeDraftId = null;
|
||||
card._draftSegmentCms = [];
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const before = card._path.map((point) => [...point]);
|
||||
card._path = [...card._path, [...points[i]]];
|
||||
card._draftSegmentCms = [...card._draftSegmentCms, card._drawWallCm];
|
||||
card._persistActiveDraftSegment();
|
||||
card._offerWallFaces(before);
|
||||
await update();
|
||||
}
|
||||
};
|
||||
const roomAt = (name, r) => ({ id: 'r-' + name, name, poly: r.map((p) => [p[0] / 1000, p[1] / 1000]) });
|
||||
|
||||
card._serverCfg = {
|
||||
spaces: [{
|
||||
id: 'overlap', title: 'Overlap', cell_cm: 5, view_box: [0, 0, 1, 0.7],
|
||||
rooms: [], openings: [], room_drafts: [], partitions: [], wall_columns: [],
|
||||
}],
|
||||
markers: [], settings: {},
|
||||
};
|
||||
card._space = 'overlap';
|
||||
card._layout = {};
|
||||
card._cfgEpoch++;
|
||||
card._modelCache = null;
|
||||
card._setMode('plan');
|
||||
card._tool = 'draw';
|
||||
await update();
|
||||
|
||||
const base = ring(200, 200, 400, 400);
|
||||
|
||||
// 1) Точный дубль: контур повторяет существующую комнату вершина в вершину.
|
||||
await resetGeometry([roomAt('base', base)]);
|
||||
await draw(closed(base));
|
||||
out.duplicateOffersNothing = !card._wallFaceBatch && !card._roomDialog;
|
||||
out.duplicateKeepsOneRoom = space().rooms.length === 1;
|
||||
|
||||
// 2) Частичное пересечение. Контракт оказался сильнее, чем «отклонить»:
|
||||
// пересекающее кольцо целиком инструменту недоступно вовсе — планарный граф
|
||||
// отдаёт куски разреза, каждый из которых либо вложен в комнату, либо лежит
|
||||
// снаружи. Частично перекрывающейся комнаты из этого жеста не построить.
|
||||
await resetGeometry([roomAt('base', base)]);
|
||||
const configBefore = JSON.stringify(space().rooms);
|
||||
const drawnRing = ring(300, 300, 500, 500);
|
||||
const drawnArea = 200 * 200;
|
||||
await draw(closed(drawnRing));
|
||||
const offered = card._wallFaceBatch?.candidates || [];
|
||||
out.partialOverlapDecomposesIntoFaces = offered.length === 3;
|
||||
// Ни одна предложенная грань не равна нарисованному кольцу: 10000 + 30000 +
|
||||
// 30000 = 40000, то есть пересечение и две «буквы Г», а не наложение.
|
||||
out.partialOverlapNeverOffersTheDrawnRing =
|
||||
offered.length > 0 && offered.every((candidate) => candidate.area < drawnArea - 1);
|
||||
out.partialOverlapFacesSumToTheUnion =
|
||||
Math.abs(offered.reduce((sum, candidate) => sum + candidate.area, 0) - drawnArea * 1.75) < 1;
|
||||
out.partialOverlapKeepsRoomsUntilDecision =
|
||||
JSON.stringify(space().rooms) === configBefore;
|
||||
|
||||
// 3a) Вложенность внутрь: контур целиком внутри существующей комнаты.
|
||||
await resetGeometry([roomAt('outer', ring(100, 100, 600, 600))]);
|
||||
await draw(closed(ring(200, 200, 300, 300)));
|
||||
out.innerNestingIsOffered = !!card._wallFaceBatch
|
||||
&& card._wallFaceBatch.candidates.length === 1;
|
||||
card._nameSel = 'Island';
|
||||
card._saveRoom();
|
||||
await update();
|
||||
out.innerNestingCreatesIslandRoom = space().rooms.length === 2
|
||||
&& space().rooms.some((room) => room.name === 'Island');
|
||||
|
||||
// 3b) Вложенность наружу: контур охватывает существующую комнату целиком.
|
||||
await resetGeometry([roomAt('inner', ring(300, 300, 400, 400))]);
|
||||
await draw(closed(ring(150, 150, 600, 600)));
|
||||
out.outerNestingIsOffered = !!card._wallFaceBatch
|
||||
&& card._wallFaceBatch.candidates.length === 1;
|
||||
card._nameSel = 'Around';
|
||||
card._saveRoom();
|
||||
await update();
|
||||
out.outerNestingCreatesRoom = space().rooms.length === 2
|
||||
&& space().rooms.some((room) => room.name === 'Around');
|
||||
|
||||
// 4) Вторая линия обороны (#173 §10.3): даже если решение о грани дошло до
|
||||
// создания, `_applyWallFaceBatch` обязан отказать на пересечении. Батч
|
||||
// собирается вручную — единственный способ пройти мимо первой проверки.
|
||||
await resetGeometry([roomAt('base', base)]);
|
||||
const clashRing = ring(300, 300, 500, 500).map((p) => [p[0], p[1]]);
|
||||
card._wallFaceBatch = {
|
||||
candidates: [{ key: 'manual', area: 1, ring: clashRing }],
|
||||
index: 0,
|
||||
decisions: [{ candidate: { key: 'manual', area: 1, ring: clashRing }, create: true }],
|
||||
activePath: closed(clashRing),
|
||||
activeCms: [15, 15, 15, 15],
|
||||
activeDraftId: null,
|
||||
};
|
||||
const roomsBeforeGuard = space().rooms.length;
|
||||
card._applyWallFaceBatch();
|
||||
await update();
|
||||
out.applyGuardRejectsOverlap = space().rooms.length === roomsBeforeGuard;
|
||||
out.applyGuardClosesBatch = !card._wallFaceBatch;
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
checkAll(result);
|
||||
await finish(browser, result);
|
||||
@@ -165,6 +165,18 @@ export const MUTANTS = [
|
||||
replace: ' const span = centre;',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'wall-face-apply-skips-overlap-guard',
|
||||
guard: 'node demo/smoke_wall_face_overlap.mjs',
|
||||
because: 'вторая проверка при создании — не дубль первой, а defense-in-depth из #173 §10.3: '
|
||||
+ 'решение принимается в диалоге, а геометрия к моменту применения может стать другой '
|
||||
+ '(#177)',
|
||||
patches: [{
|
||||
file: 'src/houseplan-card.ts',
|
||||
find: ' const clash = existingRooms.find((room) => room.id !== ownSplit',
|
||||
replace: ' const clash = [].find((room) => room.id !== ownSplit',
|
||||
}],
|
||||
},
|
||||
{
|
||||
id: 'docs-fixture-splits-posix-only',
|
||||
guard: 'node --test --test-name-pattern="не зависит от разделителя платформы" '
|
||||
|
||||
Reference in New Issue
Block a user