mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
387 lines
21 KiB
JavaScript
387 lines
21 KiB
JavaScript
// Safe fixed-topology Resize (#277): production pointer handlers, disabled
|
|
// reasons, exact shared preview/commit, one Undo, corner clamp and zero-write
|
|
// cancellation. The smoke loads the tracked production bundle.
|
|
import { launch, check, finish } from './serve.mjs';
|
|
|
|
const { page, browser } = await launch();
|
|
|
|
const enter = async (tool = 'resize') => {
|
|
await page.evaluate((next) => {
|
|
const card = window.__card;
|
|
if (!card._markup) card._setMode('plan');
|
|
card._tool = next;
|
|
card._resize.reset();
|
|
card.requestUpdate();
|
|
return card.updateComplete;
|
|
}, tool);
|
|
await page.waitForTimeout(40);
|
|
};
|
|
|
|
const setRooms = async (rooms, openings = [], walls = []) => {
|
|
await page.evaluate(({ rooms, openings, walls }) => {
|
|
const card = window.__card;
|
|
const space = card._serverCfg.spaces.find((candidate) => candidate.id === card._space);
|
|
// Each scenario below injects the deliberately compact pre-identity
|
|
// projection. Mark it as such so the production transaction exercises the
|
|
// same atomic v8 -> v9 materialisation as a real legacy document instead
|
|
// of presenting invalid v9 data with missing wall_ids.
|
|
card._serverCfg.model_version = 8;
|
|
space.rooms = rooms;
|
|
space.openings = openings;
|
|
if (walls.length) space.walls = walls;
|
|
else delete space.walls;
|
|
delete space.wall_segments;
|
|
delete space.open_spans;
|
|
delete space.partitions;
|
|
delete space.room_drafts;
|
|
delete space.wall_columns;
|
|
card._geometryHistory.clear();
|
|
card._resize.reset();
|
|
card._cfgEpoch++;
|
|
card.requestUpdate();
|
|
return card.updateComplete;
|
|
}, { rooms, openings, walls });
|
|
await page.waitForTimeout(40);
|
|
};
|
|
|
|
const settle = () => page.evaluate(() => new Promise((resolve) =>
|
|
requestAnimationFrame(() => requestAnimationFrame(resolve))));
|
|
|
|
/**
|
|
* #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')];
|
|
// Synthetic PointerEvent cannot establish native pointer capture. Preserve
|
|
// the down target ourselves so preview re-rendering may move its cx/cy
|
|
// before pointerup without turning a valid gesture into a missing target.
|
|
// This mirrors the real browser contract exercised by capturePointer().
|
|
const captured = window.__hpResizeSmokeTargets ||= new Map();
|
|
const found = 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);
|
|
const reusedCapture = args.type !== 'pointerdown' && captured.has(args.pointerId);
|
|
const target = args.type === 'pointerdown' ? found : (captured.get(args.pointerId) || found);
|
|
if (args.type === 'pointerdown' && target) captured.set(args.pointerId, target);
|
|
target?.dispatchEvent(new PointerEvent(args.type, {
|
|
bubbles: true, cancelable: true, pointerId: args.pointerId,
|
|
clientX, clientY, pointerType: 'mouse', buttons: args.type === 'pointerup' ? 0 : 1,
|
|
}));
|
|
if (args.type === 'pointerup' || args.type === 'pointercancel') captured.delete(args.pointerId);
|
|
return { sent: !!target, reusedCapture, scale: matrix.a, client: [clientX, clientY],
|
|
stage: [stage.clientWidth, stage.clientHeight] };
|
|
}, { type, planX, planY, cx, cy, pointerId, nudge });
|
|
|
|
/** Жест обязан доехать: молчаливый `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;
|
|
const space = live ? card._curSpaceCfg : card._serverCfg.spaces.find((candidate) => candidate.id === card._space);
|
|
return space.rooms.find((room) => room.id === id)?.poly || null;
|
|
}, { id, live });
|
|
|
|
const edgeX = async (id, edge, live = false) => {
|
|
const poly = await roomPoly(id, live);
|
|
return ((poly[edge][0] + poly[(edge + 1) % poly.length][0]) / 2) * 1000;
|
|
};
|
|
|
|
const rect = (id, x0, y0, x1, y1) => ({
|
|
id, name: id, area: null,
|
|
poly: [[x0, y0], [x1, y0], [x1, y1], [x0, y1]].map(([x, y]) => [x / 1000, y / 1000]),
|
|
});
|
|
|
|
// No Resize interaction leaks into another tool.
|
|
await enter('draw');
|
|
check('safe_resize.hidden_outside_tool', await page.evaluate(() =>
|
|
window.__card.renderRoot.querySelectorAll('.rszhandle').length), 0);
|
|
|
|
// Exact shared pair: only two rooms move, preview stays out of _serverCfg,
|
|
// moving opening travels once, release writes one history command.
|
|
await setRooms([
|
|
rect('left', 100, 100, 400, 400),
|
|
rect('right', 400, 100, 700, 400),
|
|
rect('third', 850, 100, 950, 400),
|
|
], [{ id: 'moving-door', type: 'door', x: 0.4, y: 0.25, angle: 90, length: 0.08 }], [{
|
|
key: '0.400000,0.250000@1.5708', cm: 15, a: [0.4, 0.1], b: [0.4, 0.4],
|
|
}]);
|
|
await enter();
|
|
check('safe_resize.handles_visible', await page.evaluate(() =>
|
|
window.__card.renderRoot.querySelectorAll('.rszhandle').length), 12);
|
|
check('safe_resize.no_corner_scale', await page.evaluate(() =>
|
|
window.__card.renderRoot.querySelectorAll('.rszcorner,.rszframe,.rszknob').length), 0);
|
|
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 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);
|
|
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);
|
|
const released = sent('safe_resize.up_sent', await pointer('pointerup', 450, 250, { cx: 400, cy: 250 }));
|
|
check('safe_resize.up_reuses_pointer_capture', released.reusedCapture, true);
|
|
await settle();
|
|
const safeResizeCommitLeft = await edgeX('left', 1, false);
|
|
const safeResizeCommitRight = await edgeX('right', 3, false);
|
|
check('safe_resize.commit_left', Math.abs(safeResizeCommitLeft - 450) < 6, true);
|
|
check('safe_resize.commit_right', Math.abs(safeResizeCommitRight - 450) < 6, true);
|
|
check('safe_resize.third_static', Math.abs((await edgeX('third', 1, false)) - 950) < 1e-6, true);
|
|
const safeResizeOpeningX = await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const space = card._serverCfg.spaces.find((candidate) => candidate.id === card._space);
|
|
return space.openings.find((opening) => opening.id === 'moving-door').x;
|
|
});
|
|
check('safe_resize.opening_once', Math.abs(safeResizeOpeningX - 0.45) < 0.006, true);
|
|
check('safe_resize.one_undo', await page.evaluate(() => window.__card._geometryHistory.size), 1);
|
|
await page.keyboard.press('Control+z');
|
|
await settle();
|
|
check('safe_resize.undo_exact', Math.abs((await edgeX('left', 1, false)) - 400) < 1e-6, true);
|
|
|
|
// The anonymized regression topology has one long boundary owned by two
|
|
// neighbours. It is a visible, accessible disabled handle and captures no
|
|
// pointer / creates no write.
|
|
await setRooms([
|
|
{ id: 'main', name: 'main', area: null, poly: [[0, 0], [662, 0], [662, -5], [782, -5], [782, 250], [662, 250], [662, 245], [0, 245]].map(([x, y]) => [x / 1000, y / 1000]) },
|
|
rect('a', 0, 100, 339, 245),
|
|
rect('b', 339, 100, 662, 245),
|
|
]);
|
|
await enter();
|
|
const disabled = await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const circles = [...card.renderRoot.querySelectorAll('.rszhandle[aria-disabled="true"]')];
|
|
const target = circles.find((circle) => circle.getAttribute('aria-label')?.includes('part'))
|
|
|| circles[0];
|
|
return {
|
|
count: circles.length,
|
|
label: target?.getAttribute('aria-label') || '',
|
|
tab: target?.getAttribute('tabindex'),
|
|
};
|
|
});
|
|
check('safe_resize.disabled_visible', disabled.count > 0, true);
|
|
check('safe_resize.disabled_reason', /part|част/i.test(disabled.label), true);
|
|
check('safe_resize.disabled_focusable', disabled.tab, '0');
|
|
const historyBefore = await page.evaluate(() => window.__card._geometryHistory.size);
|
|
await page.evaluate(() => {
|
|
const card = window.__card;
|
|
card.__resizeDisabledWrite = card._writeConfig;
|
|
card.__resizeDisabledWrites = 0;
|
|
card._writeConfig = async () => { card.__resizeDisabledWrites++; };
|
|
});
|
|
const disabledActivation = await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const target = [...card.renderRoot.querySelectorAll('.rszhandle[aria-disabled="true"]')]
|
|
.find((circle) => circle.getAttribute('aria-label')?.includes('part'))
|
|
|| card.renderRoot.querySelector('.rszhandle[aria-disabled="true"]');
|
|
const expected = target?.getAttribute('aria-label') || '';
|
|
const activate = (event) => {
|
|
card._toast = '';
|
|
target?.dispatchEvent(event);
|
|
return card._toast;
|
|
};
|
|
return {
|
|
expected,
|
|
click: activate(new MouseEvent('click', { bubbles: true, cancelable: true })),
|
|
enter: activate(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })),
|
|
space: activate(new KeyboardEvent('keydown', { key: ' ', bubbles: true, cancelable: true })),
|
|
};
|
|
});
|
|
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);
|
|
sent('disabled.pointerdown_sent', 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);
|
|
check('safe_resize.disabled_zero_write', await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const writes = card.__resizeDisabledWrites;
|
|
card._writeConfig = card.__resizeDisabledWrite;
|
|
delete card.__resizeDisabledWrite;
|
|
delete card.__resizeDisabledWrites;
|
|
return writes;
|
|
}), 0);
|
|
|
|
// #289: the selected outer wall would lengthen two formerly shared side walls
|
|
// by exactly 43 grid steps. Both directions are disabled before capture: grow
|
|
// would append outer material to a shared run, shrink would leave the
|
|
// neighbour's continuation outer.
|
|
await setRooms([
|
|
rect('mixed-main', 0, 0, 200, 928),
|
|
rect('mixed-right', 200, 0, 400, 928),
|
|
rect('mixed-left', -200, 0, 0, 928),
|
|
]);
|
|
await enter();
|
|
const mixedBefore = JSON.stringify(await roomPoly('mixed-main'));
|
|
const mixedHandle = await page.evaluate(() => {
|
|
const target = [...window.__card.renderRoot.querySelectorAll('.rszhandle')]
|
|
.find((handle) => Math.abs(Number(handle.getAttribute('cx')) - 100) < 1
|
|
&& Math.abs(Number(handle.getAttribute('cy')) - 928) < 1);
|
|
return { disabled: target?.getAttribute('aria-disabled'), label: target?.getAttribute('aria-label') || '' };
|
|
});
|
|
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);
|
|
sent('mixed_role.pointerdown_sent', await pointer('pointerdown', 100, 928, { cx: 100, cy: 928, pointerId: 82 }));
|
|
sent('mixed_role.pointermove_sent', await pointer('pointermove', 100, 971, { cx: 100, cy: 928, pointerId: 82 }));
|
|
sent('mixed_role.pointerup_sent', 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);
|
|
check('safe_resize.mixed_role_zero_write', await page.evaluate(() => window.__card._geometryHistory.size), 0);
|
|
|
|
// An outer side may grow through empty space to another exact grid node. It
|
|
// stops before it would overlap a different room's outer edge and turn both
|
|
// runs into one newly shared stretch.
|
|
await setRooms([
|
|
rect('range-main', 0, 0, 100, 100),
|
|
rect('range-future-left', -100, 125, 0, 200),
|
|
], [], [
|
|
{ key: '0.000000,0.050000@1.5708', cm: 30, a: [0, 0], b: [0, 0.1] },
|
|
{ key: '0.000000,0.162500@1.5708', cm: 30, a: [0, 0.125], b: [0, 0.2] },
|
|
]);
|
|
await enter();
|
|
sent('owner_boundary.pointerdown_sent', await pointer('pointerdown', 50, 100, { cx: 50, cy: 100, pointerId: 83 }));
|
|
sent('owner_boundary.pointermove_sent', await pointer('pointermove', 50, 150, { cx: 50, cy: 100, pointerId: 83 }));
|
|
sent('owner_boundary.pointerup_sent', 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);
|
|
check('safe_resize.owner_boundary_topology', rangePoly.length, 4);
|
|
check('safe_resize.owner_boundary_no_mixed_role', await page.evaluate(() => {
|
|
const card = window.__card;
|
|
const walls = card._serverCfg.spaces.find((space) => space.id === card._space).walls || [];
|
|
const boundary = 0.125;
|
|
return walls.every((wall) => {
|
|
const ys = [wall?.a?.[1], wall?.b?.[1]].filter(Number.isFinite);
|
|
if (ys.length !== 2) return true;
|
|
return !(Math.min(...ys) < boundary - 1e-9 && Math.max(...ys) > boundary + 1e-9);
|
|
});
|
|
}), true);
|
|
check('safe_resize.owner_boundary_cm_preserved', await page.evaluate(() => {
|
|
const walls = window.__card._serverCfg.spaces.find((space) => space.id === window.__card._space).walls || [];
|
|
return JSON.stringify(walls.map((wall) => wall.cm).sort((a, b) => a - b));
|
|
}), '[30,30]');
|
|
|
|
// Irregular exact pair reaches the first safe grid position before its inner
|
|
// corner; neither polygon gains or loses a vertex.
|
|
await setRooms([
|
|
rect('left', 100, 100, 400, 400),
|
|
{ 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();
|
|
sent('corner_clamped.pointerdown_sent', await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 79 }));
|
|
sent('corner_clamped.pointermove_sent', await pointer('pointermove', 720, 250, { cx: 400, cy: 250, pointerId: 79 }));
|
|
sent('corner_clamped.pointerup_sent', 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);
|
|
check('safe_resize.corner_topology', irregular.length, 6);
|
|
|
|
// Exact production preflight is fail-closed before preview/commit.
|
|
await setRooms([rect('preflight', 100, 100, 400, 400)]);
|
|
await enter();
|
|
await page.evaluate(() => {
|
|
const card = window.__card;
|
|
card.__resizePreflight = card._checkSpacePhysicalGeometry;
|
|
card.__resizeShowToast = card._showToast;
|
|
card.__resizeRejectToasts = 0;
|
|
card._checkSpacePhysicalGeometry = () => ({ ok: false, status: 'failed' });
|
|
card._showToast = function resizeRejectToast(message) {
|
|
if (/last safe position|последн/i.test(message)) card.__resizeRejectToasts++;
|
|
return card.__resizeShowToast.call(card, message);
|
|
};
|
|
});
|
|
const preflightBefore = JSON.stringify(await roomPoly('preflight'));
|
|
sent('preflight.pointerdown_sent', await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 80 }));
|
|
sent('preflight.pointermove_sent', await pointer('pointermove', 500, 250, { cx: 400, cy: 250, pointerId: 80 }));
|
|
sent('preflight.pointermove2_sent', 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);
|
|
sent('preflight.pointerup_sent', await pointer('pointerup', 500, 250, { cx: 400, cy: 250, pointerId: 80 }));
|
|
await settle();
|
|
await page.evaluate(() => {
|
|
const card = window.__card;
|
|
card._checkSpacePhysicalGeometry = card.__resizePreflight;
|
|
card._showToast = card.__resizeShowToast;
|
|
delete card.__resizePreflight;
|
|
delete card.__resizeShowToast;
|
|
delete card.__resizeRejectToasts;
|
|
});
|
|
check('safe_resize.preflight_no_commit', JSON.stringify(await roomPoly('preflight')), preflightBefore);
|
|
check('safe_resize.preflight_zero_write', await page.evaluate(() => window.__card._geometryHistory.size), 0);
|
|
|
|
// The candidate may become invalid after a valid preview (for example a
|
|
// concurrent structural update). Pointerup repeats the common preflight and
|
|
// must not trust the fact that an overlay was shown.
|
|
await setRooms([rect('commit-preflight', 100, 100, 400, 400)]);
|
|
await enter();
|
|
const commitPreflightBefore = JSON.stringify(await roomPoly('commit-preflight'));
|
|
sent('commit_preflight.pointerdown_sent', await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 84 }));
|
|
sent('commit_preflight.pointermove_sent', 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);
|
|
await page.evaluate(() => {
|
|
const card = window.__card;
|
|
card.__resizeCommitPreflight = card._checkSpacePhysicalGeometry;
|
|
card._checkSpacePhysicalGeometry = () => ({ ok: false, status: 'failed' });
|
|
});
|
|
sent('commit_preflight.pointerup_sent', await pointer('pointerup', 500, 250, { cx: 400, cy: 250, pointerId: 84 }));
|
|
await settle();
|
|
await page.evaluate(() => {
|
|
const card = window.__card;
|
|
card._checkSpacePhysicalGeometry = card.__resizeCommitPreflight;
|
|
delete card.__resizeCommitPreflight;
|
|
});
|
|
check('safe_resize.commit_preflight_no_commit',
|
|
JSON.stringify(await roomPoly('commit-preflight')), commitPreflightBefore);
|
|
check('safe_resize.commit_preflight_zero_write',
|
|
await page.evaluate(() => window.__card._geometryHistory.size), 0);
|
|
|
|
// pointercancel follows the abort path: no persistence, history or hidden save.
|
|
await setRooms([rect('solo', 100, 100, 400, 400)]);
|
|
await enter();
|
|
const cancelBefore = JSON.stringify(await roomPoly('solo'));
|
|
sent('cancel.pointerdown_sent', await pointer('pointerdown', 400, 250, { cx: 400, cy: 250, pointerId: 81 }));
|
|
sent('cancel.pointermove_sent', await pointer('pointermove', 500, 250, { cx: 400, cy: 250, pointerId: 81 }));
|
|
sent('cancel.pointercancel_sent', 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,
|
|
safeResizeResult: [safeResizePreviewLeft, safeResizeCommitLeft, safeResizeCommitRight, safeResizeOpeningX] });
|