Свидетель ресайза меряет то же и тогда же, что и карточка

Смок считал экранные точки один раз, заранее, а карточка переводит их обратно в
момент события — от текущего размера стейджа и текущего вида. Стоило раскладке
осесть между замером и жестом, и 34 экранных пикселя превращались уже не в 50
единиц плана: ресайз коммитил не ту величину, а свидетель сообщал об этом
четырьмя немыми `expected true, got false`. Раннер это ловил, локальная машина —
нет, и красный гейт закрыл выпуск v1.74.0.

Теперь координаты передаются в единицах плана, а перевод живёт внутри того же
кадра, что и отправка события. Доставка события проверяется (прежде
возвращаемое значение хелпера для `pointermove` игнорировалось молча), а
устойчивость отображения — отдельной проверкой `safe_resize.mapping_stable`:
масштаб на захвате и на движении обязан совпасть, иначе в имени проверки
печатаются оба масштаба и оба размера стейджа.

Продуктовый код не тронут: ни один из экспериментов не указал на дефект
ресайза. Если `mapping_stable` когда-нибудь покраснеет на раннере — это и будет
доказательством обратного, с числами в первой же строке.

Issue: #533
User-Visible: no
This commit is contained in:
Codex
2026-09-11 16:18:55 +00:00
committed by claude[bot]
parent 9584933c98
commit acea695069
+69 -64
View File
@@ -47,25 +47,45 @@ const setRooms = async (rooms, openings = [], walls = []) => {
const settle = () => page.evaluate(() => new Promise((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(resolve))));
const screenPt = (x, y) => page.evaluate(([px, py]) => {
const card = window.__card;
const stage = card.renderRoot.querySelector('.stage');
const svg = stage.querySelector('svg');
const point = new DOMPoint(px, py).matrixTransform(svg.getScreenCTM());
return [point.x, point.y];
}, [x, y]);
/**
* #533: перевод координат живёт внутри того же кадра, что и отправка события.
*
* Прежде смок считал экранные точки заранее, одним вызовом на весь жест, а
* карточка переводит их обратно в момент события — от текущего размера стейджа
* и текущего вида. Стоило раскладке осесть между замером и жестом, и 34 экранных
* пикселя превращались уже не в 50 единиц плана: ресайз коммитил не ту величину,
* а свидетель сообщал об этом четырьмя немыми `expected true, got false`.
* Раннер ловил это, локальная машина — нет.
*
* Координаты сюда передаются В ЕДИНИЦАХ ПЛАНА; `nudge` — добавка в экранных
* пикселях для случаев, где проверяется именно подрагивание указателя.
*/
const pointer = (type, planX, planY, { cx, cy, pointerId = 77, nudge } = {}) =>
page.evaluate((args) => {
const card = window.__card;
const stage = card.renderRoot.querySelector('.stage');
const svg = stage.querySelector('svg');
const matrix = svg.getScreenCTM();
const point = new DOMPoint(args.planX, args.planY).matrixTransform(matrix);
const clientX = point.x + (args.nudge ? args.nudge[0] : 0);
const clientY = point.y + (args.nudge ? args.nudge[1] : 0);
const handles = [...card.renderRoot.querySelectorAll('.rszhandle')];
const target = args.cx == null ? handles.find((handle) => !handle.classList.contains('disabled'))
: handles.find((handle) => Math.abs(Number(handle.getAttribute('cx')) - args.cx) < 1
&& Math.abs(Number(handle.getAttribute('cy')) - args.cy) < 1);
target?.dispatchEvent(new PointerEvent(args.type, {
bubbles: true, cancelable: true, pointerId: args.pointerId,
clientX, clientY, pointerType: 'mouse', buttons: args.type === 'pointerup' ? 0 : 1,
}));
return { sent: !!target, scale: matrix.a, client: [clientX, clientY],
stage: [stage.clientWidth, stage.clientHeight] };
}, { type, planX, planY, cx, cy, pointerId, nudge });
const pointer = (type, clientX, clientY, { cx, cy, pointerId = 77 } = {}) => page.evaluate((args) => {
const handles = [...window.__card.renderRoot.querySelectorAll('.rszhandle')];
const target = args.cx == null ? handles.find((handle) => !handle.classList.contains('disabled'))
: handles.find((handle) => Math.abs(Number(handle.getAttribute('cx')) - args.cx) < 1
&& Math.abs(Number(handle.getAttribute('cy')) - args.cy) < 1);
target?.dispatchEvent(new PointerEvent(args.type, {
bubbles: true, cancelable: true, pointerId: args.pointerId,
clientX: args.clientX, clientY: args.clientY, pointerType: 'mouse', buttons: args.type === 'pointerup' ? 0 : 1,
}));
return !!target;
}, { type, clientX, clientY, cx, cy, pointerId });
/** Жест обязан доехать: молчаливый `find` без цели — это зелёный смок ни о чём. */
const sent = (name, result) => {
check(name, result.sent, true);
return result;
};
const roomPoly = (id, live = false) => page.evaluate(({ id, live }) => {
const card = window.__card;
@@ -106,23 +126,21 @@ check('safe_resize.shared_enabled_handles', await page.evaluate(() =>
[...window.__card.renderRoot.querySelectorAll('.rszhandle')]
.filter((handle) => Math.abs(Number(handle.getAttribute('cx')) - 400) < 1
&& handle.getAttribute('aria-disabled') === 'false').length), 2);
const [sx, sy] = await screenPt(400, 250);
const [tx] = await screenPt(450, 250);
const safeResizeScreen = await page.evaluate(() => {
const stage = window.__card.renderRoot.querySelector('.stage');
const svg = stage.querySelector('svg');
const matrix = svg.getScreenCTM();
return { stage: [stage.clientWidth, stage.clientHeight], viewBox: svg.getAttribute('viewBox'),
matrix: matrix ? [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f] : null };
});
await pointer('pointerdown', sx, sy, { cx: 400, cy: 250 });
const grab = sent('safe_resize.down_sent', await pointer('pointerdown', 400, 250, { cx: 400, cy: 250 }));
check('safe_resize.drag_started', await page.evaluate(() => window.__card._resize.dragging), true);
await pointer('pointermove', tx, sy);
const moved = sent('safe_resize.move_sent', await pointer('pointermove', 450, 250, { cx: 400, cy: 250 }));
// #533: если отображение экран↔план поехало между захватом и движением, жест
// коммитит не ту величину. Пусть это будет одна строка с числами, а не четыре
// немых `expected true, got false`.
check(`safe_resize.mapping_stable ${grab.scale} vs ${moved.scale}`
+ ` stage ${grab.stage} vs ${moved.stage}`,
Math.abs(grab.scale - moved.scale) < 1e-6, true);
const safeResizeScreen = { down: grab, move: moved };
await settle();
const safeResizePreviewLeft = await edgeX('left', 1, true);
check('safe_resize.preview_moved', Math.abs(safeResizePreviewLeft - 450) < 6, true);
check('safe_resize.preview_not_persisted', Math.abs((await edgeX('left', 1, false)) - 400) < 1e-6, true);
await pointer('pointerup', tx, sy);
sent('safe_resize.up_sent', await pointer('pointerup', 450, 250, { cx: 400, cy: 250 }));
await settle();
const safeResizeCommitLeft = await edgeX('left', 1, false);
const safeResizeCommitRight = await edgeX('right', 3, false);
@@ -191,8 +209,7 @@ const disabledActivation = await page.evaluate(() => {
check('safe_resize.disabled_click_reason', disabledActivation.click, disabledActivation.expected);
check('safe_resize.disabled_enter_reason', disabledActivation.enter, disabledActivation.expected);
check('safe_resize.disabled_space_reason', disabledActivation.space, disabledActivation.expected);
const [dx, dy] = await screenPt(331, 245);
await pointer('pointerdown', dx, dy, { cx: 331, cy: 245, pointerId: 78 });
await pointer('pointerdown', 331, 245, { cx: 331, cy: 245, pointerId: 78 });
await settle();
check('safe_resize.disabled_no_drag', await page.evaluate(() => window.__card._resize.dragging), false);
check('safe_resize.disabled_zero_history', await page.evaluate(() => window.__card._geometryHistory.size), historyBefore);
@@ -224,11 +241,9 @@ const mixedHandle = await page.evaluate(() => {
});
check('safe_resize.mixed_role_disabled', mixedHandle.disabled, 'true');
check('safe_resize.mixed_role_reason', /only part of this wall|часть этой стены/i.test(mixedHandle.label), true);
const [mx, my] = await screenPt(100, 928);
const [, mixed43] = await screenPt(100, 971);
await pointer('pointerdown', mx, my, { cx: 100, cy: 928, pointerId: 82 });
await pointer('pointermove', mx, mixed43, { pointerId: 82 });
await pointer('pointerup', mx, mixed43, { pointerId: 82 });
await pointer('pointerdown', 100, 928, { cx: 100, cy: 928, pointerId: 82 });
await pointer('pointermove', 100, 971, { cx: 100, cy: 928, pointerId: 82 });
await pointer('pointerup', 100, 971, { cx: 100, cy: 928, pointerId: 82 });
await settle();
check('safe_resize.mixed_role_no_drag', await page.evaluate(() => window.__card._resize.dragging), false);
check('safe_resize.mixed_role_geometry_exact', JSON.stringify(await roomPoly('mixed-main')), mixedBefore);
@@ -245,11 +260,9 @@ await setRooms([
{ key: '0.000000,0.162500@1.5708', cm: 30, a: [0, 0.125], b: [0, 0.2] },
]);
await enter();
const [rx, ry] = await screenPt(50, 100);
const [, rangeFar] = await screenPt(50, 150);
await pointer('pointerdown', rx, ry, { cx: 50, cy: 100, pointerId: 83 });
await pointer('pointermove', rx, rangeFar, { pointerId: 83 });
await pointer('pointerup', rx, rangeFar, { pointerId: 83 });
await pointer('pointerdown', 50, 100, { cx: 50, cy: 100, pointerId: 83 });
await pointer('pointermove', 50, 150, { cx: 50, cy: 100, pointerId: 83 });
await pointer('pointerup', 50, 150, { cx: 50, cy: 100, pointerId: 83 });
await settle();
const rangePoly = await roomPoly('range-main');
check('safe_resize.owner_boundary_clamped', Math.abs(rangePoly[2][1] * 1000 - 125) < 1, true);
@@ -276,11 +289,9 @@ await setRooms([
{ id: 'irregular', name: 'irregular', area: null, poly: [[400, 100], [700, 100], [700, 200], [650, 200], [650, 400], [400, 400]].map(([x, y]) => [x / 1000, y / 1000]) },
]);
await enter();
const [ix, iy] = await screenPt(400, 250);
const [farX] = await screenPt(720, 250);
await pointer('pointerdown', ix, iy, { cx: 400, cy: 250, pointerId: 79 });
await pointer('pointermove', farX, iy, { pointerId: 79 });
await pointer('pointerup', farX, iy, { pointerId: 79 });
await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 79 });
await pointer('pointermove', 720, 250, { cx: 400, cy: 250, pointerId: 79 });
await pointer('pointerup', 720, 250, { cx: 400, cy: 250, pointerId: 79 });
await settle();
const irregular = await roomPoly('irregular');
check('safe_resize.corner_clamped', Math.abs(irregular[5][0] * 1000 - 625) < 6, true);
@@ -301,17 +312,15 @@ await page.evaluate(() => {
};
});
const preflightBefore = JSON.stringify(await roomPoly('preflight'));
const [px, py] = await screenPt(400, 250);
const [preflightX] = await screenPt(500, 250);
await pointer('pointerdown', px, py, { cx: 400, cy: 250, pointerId: 80 });
await pointer('pointermove', preflightX, py, { pointerId: 80 });
await pointer('pointermove', preflightX + 20, py, { pointerId: 80 });
await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 80 });
await pointer('pointermove', 500, 250, { cx: 400, cy: 250, pointerId: 80 });
await pointer('pointermove', 500, 250, { cx: 400, cy: 250, pointerId: 80, nudge: [20, 0] });
await settle();
check('safe_resize.preflight_visible_reason', await page.evaluate(() =>
/last safe position|последн/i.test(window.__card._toast)), true);
check('safe_resize.preflight_reason_once', await page.evaluate(() =>
window.__card.__resizeRejectToasts), 1);
await pointer('pointerup', preflightX, py, { pointerId: 80 });
await pointer('pointerup', 500, 250, { cx: 400, cy: 250, pointerId: 80 });
await settle();
await page.evaluate(() => {
const card = window.__card;
@@ -330,10 +339,8 @@ check('safe_resize.preflight_zero_write', await page.evaluate(() => window.__car
await setRooms([rect('commit-preflight', 100, 100, 400, 400)]);
await enter();
const commitPreflightBefore = JSON.stringify(await roomPoly('commit-preflight'));
const [cpx, cpy] = await screenPt(400, 250);
const [commitPreflightX] = await screenPt(500, 250);
await pointer('pointerdown', cpx, cpy, { cx: 400, cy: 250, pointerId: 84 });
await pointer('pointermove', commitPreflightX, cpy, { pointerId: 84 });
await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 84 });
await pointer('pointermove', 500, 250, { cx: 400, cy: 250, pointerId: 84 });
await settle();
check('safe_resize.commit_preflight_preview_exists',
Math.abs((await edgeX('commit-preflight', 1, true)) - 500) < 6, true);
@@ -342,7 +349,7 @@ await page.evaluate(() => {
card.__resizeCommitPreflight = card._checkSpacePhysicalGeometry;
card._checkSpacePhysicalGeometry = () => ({ ok: false, status: 'failed' });
});
await pointer('pointerup', commitPreflightX, cpy, { pointerId: 84 });
await pointer('pointerup', 500, 250, { cx: 400, cy: 250, pointerId: 84 });
await settle();
await page.evaluate(() => {
const card = window.__card;
@@ -358,14 +365,12 @@ check('safe_resize.commit_preflight_zero_write',
await setRooms([rect('solo', 100, 100, 400, 400)]);
await enter();
const cancelBefore = JSON.stringify(await roomPoly('solo'));
const [cx, cy] = await screenPt(400, 250);
const [moveX] = await screenPt(500, 250);
await pointer('pointerdown', cx, cy, { cx: 400, cy: 250, pointerId: 81 });
await pointer('pointermove', moveX, cy, { pointerId: 81 });
await pointer('pointercancel', moveX, cy, { pointerId: 81 });
await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 81 });
await pointer('pointermove', 500, 250, { cx: 400, cy: 250, pointerId: 81 });
await pointer('pointercancel', 500, 250, { cx: 400, cy: 250, pointerId: 81 });
await settle();
check('safe_resize.cancel_geometry', JSON.stringify(await roomPoly('solo')), cancelBefore);
check('safe_resize.cancel_zero_write', await page.evaluate(() => window.__card._geometryHistory.size), 0);
await finish(browser, { done: true, safeResizeScreen, safeResizePoints: [sx, sy, tx],
await finish(browser, { done: true, safeResizeScreen,
safeResizeResult: [safeResizePreviewLeft, safeResizeCommitLeft, safeResizeCommitRight, safeResizeOpeningX] });