mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Stabilize beta.5 transition validation
This commit is contained in:
@@ -174,6 +174,11 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
await wait(15);
|
||||
}
|
||||
};
|
||||
const settleMode = async (card) => {
|
||||
await until(() => !card._modeTransitionBusy);
|
||||
await card.updateComplete;
|
||||
await frame();
|
||||
};
|
||||
window.__goldenCard?.remove?.();
|
||||
window.__card?.remove?.();
|
||||
localStorage.clear();
|
||||
@@ -239,6 +244,7 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
if (scenario.mode) {
|
||||
card._setMode(scenario.mode);
|
||||
await card.updateComplete;
|
||||
await settleMode(card);
|
||||
}
|
||||
if (Number.isFinite(scenario.zoom)) {
|
||||
card._applyView(scenario.zoom, 500, 500);
|
||||
@@ -304,6 +310,7 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
if (scenario.dialog === 'device') {
|
||||
card._setMode('devices');
|
||||
await card.updateComplete;
|
||||
await settleMode(card);
|
||||
const device = card._devices.find((item) => item.id === scenario.deviceId);
|
||||
if (!device) throw new Error(`golden device missing: ${scenario.deviceId}`);
|
||||
card._openMarkerDialog(device);
|
||||
@@ -380,6 +387,7 @@ export async function prepareGoldenScenario(page, scenario) {
|
||||
card._setMode('decor');
|
||||
card._decorTool = 'select';
|
||||
await card.updateComplete;
|
||||
await settleMode(card);
|
||||
const shape = card._decorList.find((item) => item.kind === 'line');
|
||||
if (!shape) throw new Error('golden decor line missing');
|
||||
card._decorShapeDbl(new MouseEvent('dblclick'), shape);
|
||||
|
||||
+16
-4
@@ -8,6 +8,12 @@ const res = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
const stageBg = async () => {
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
@@ -90,9 +96,11 @@ const res = await page.evaluate(async () => {
|
||||
|
||||
// 8) editors keep their own canvas (inline bg only in view/kiosk)
|
||||
c._setMode('decor');
|
||||
await settleMode();
|
||||
const editorBg = await waitStageBg((value) => value !== rgb('#0a2a4a'));
|
||||
out.editorUnpainted = editorBg !== rgb('#0a2a4a');
|
||||
c._setMode('view');
|
||||
await settleMode();
|
||||
out.viewPaintedAgain = (await waitStageBg((value) => value === rgb('#0a2a4a'))) === rgb('#0a2a4a');
|
||||
|
||||
// 9) kiosk instance respects the setting
|
||||
@@ -165,6 +173,12 @@ const paper = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const upd = async () => { c.requestUpdate(); await c.updateComplete; };
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
c._serverCfg.settings = { ...(c._serverCfg.settings || {}), bg_color: '#ff00ff' };
|
||||
c._cfgRev = (c._cfgRev || 0) + 1;
|
||||
await upd();
|
||||
@@ -189,12 +203,10 @@ const paper = await page.evaluate(async () => {
|
||||
out.imageBelowRooms = idxOf('image') < idxOf('.room, .room-outline');
|
||||
// editors keep the paper too (their canvas ignores bg_color anyway)
|
||||
c._setMode('decor');
|
||||
await upd();
|
||||
await new Promise((resolve) => setTimeout(resolve, 220));
|
||||
await upd(); await settleMode();
|
||||
out.editorKeepsPaper = !!sr().querySelector('.stage svg .hp-paper');
|
||||
c._setMode('view');
|
||||
await upd();
|
||||
await new Promise((resolve) => setTimeout(resolve, 220));
|
||||
await upd(); await settleMode();
|
||||
// daynight night: the paper dims via the brightness filter ONLY, never alpha
|
||||
c._serverCfg.settings.bg_mode = 'daynight';
|
||||
c._serverCfg.settings.north_deg = 0;
|
||||
|
||||
+11
-2
@@ -4,11 +4,17 @@ const res = await page.evaluate(async () => {
|
||||
const out = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
const esc = async () => { window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' })); await c.updateComplete; };
|
||||
// 1) третья вкладка есть
|
||||
const tabs = [...sr().querySelectorAll('.modetab')];
|
||||
out.threeTabs = tabs.length === 3;
|
||||
tabs[2].click(); await c.updateComplete;
|
||||
tabs[2].click(); await settleMode();
|
||||
out.decorMode = c._mode === 'decor';
|
||||
out.decorBar = !!sr().querySelector('.editbar.decorbar');
|
||||
// The compact colour/opacity control must escape the clipped editor/dialog
|
||||
@@ -253,7 +259,10 @@ const res = await page.evaluate(async () => {
|
||||
c._redoGeometry(); await c.updateComplete;
|
||||
out.lineStyleUndoRedo = undoSolid
|
||||
&& c._decorList.find((x) => x.id === 'dcprobe')?.line_style === 'dashed';
|
||||
const selectHit = sr().querySelector('.decorlayer line.dselecthit');
|
||||
// Selection handles intentionally sit above the hit proxy. Clear the frame
|
||||
// so this assertion measures the dashed line's gap, not an endpoint handle.
|
||||
c._decorSel = null; c.requestUpdate(); await c.updateComplete;
|
||||
const selectHit = sr().querySelector('.decorlayer line.dselecthit[data-id="dcprobe"]');
|
||||
const dashedPaint = probe();
|
||||
let gapPicked = null;
|
||||
if (selectHit && dashedPaint) {
|
||||
|
||||
@@ -21,6 +21,12 @@ const res = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
// null-safe: на сборке ДО этих правок у надписи нет ни data-id, ни tspan —
|
||||
// смок должен показать список провалов, а не упасть с исключением
|
||||
const el = (id) => sr().querySelector(`.decorlayer text.dtext[data-id="${id}"]`);
|
||||
@@ -41,7 +47,7 @@ const res = await page.evaluate(async () => {
|
||||
cancelable: true, pointerId: 7, clientX, clientY, button: 0, isPrimary: true, ...extra }));
|
||||
};
|
||||
|
||||
sr().querySelectorAll('.modetab')[2].click(); await c.updateComplete;
|
||||
sr().querySelectorAll('.modetab')[2].click(); await settleMode();
|
||||
c._curSpaceCfg.decor = [];
|
||||
c._decorTool = 'select'; c._decorSel = null; await c.updateComplete;
|
||||
|
||||
|
||||
+19
-15
@@ -5,6 +5,12 @@ const res = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const tabs = () => [...sr().querySelectorAll('.modetab')];
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
// 1) две вкладки, Просмотра нет, крестиков в неактивных нет
|
||||
out.twoTabs = tabs().length === 3; // третья — Редактор подложки (v1.33.0)
|
||||
out.labels = tabs().map((t) => t.textContent.trim());
|
||||
@@ -28,10 +34,7 @@ const res = await page.evaluate(async () => {
|
||||
// 3) повторный клик по активной вкладке — ничего
|
||||
tabs()[0].click(); await c.updateComplete;
|
||||
out.reclickNoop = c._mode === 'plan';
|
||||
// Let the View -> editor grid transition finish first. A real user cannot
|
||||
// switch editors in the same zero-layout frame, and competing WAAPI/CSS
|
||||
// height animations in that frame are intentionally suppressed.
|
||||
await new Promise((resolve) => setTimeout(resolve, 220));
|
||||
await settleMode();
|
||||
// HP-UX-11: transient tool controls live in a stage overlay. Opening the
|
||||
// thickness controls must not resize/refit the stage or move pinned Close.
|
||||
const stageBeforeContext = sr().querySelector('.stage').getBoundingClientRect();
|
||||
@@ -117,30 +120,31 @@ const res = await page.evaluate(async () => {
|
||||
out.devBarBtns = sr().querySelectorAll('.editbar.devbar .btn:not(.barclose)').length === 3; // add/show-all/rules (v1.33.2: Reset removed)
|
||||
const swapChrome = sr().querySelector('.editorchrome');
|
||||
const swapInner = swapChrome.querySelector('.editorchrome-inner');
|
||||
out.editorSwapAnimatesHeight = swapChrome.getAnimations()
|
||||
.some((animation) => animation.id === 'hp-editor-height-swap');
|
||||
out.editorSwapAnimatesContent = swapInner.getAnimations()
|
||||
.some((animation) => animation.id === 'hp-editor-content-swap');
|
||||
out.editorSwapAnimatesHeight = c._modeTransitionBusy
|
||||
&& sr().querySelector('.stage').classList.contains('mode-transition');
|
||||
out.editorSwapAnimatesContent = c._modeTransitionBusy && !!swapInner
|
||||
&& (c._modeTransitionVisual?.toolbarContentOpacity ?? 1) < 1;
|
||||
await settleMode();
|
||||
// 5) инструменты устройств из шапки исчезли (в .bar их больше нет)
|
||||
out.headerCleanInDev = !sr().querySelector('.bar > .btn[title*="' + (c._t('title.add_device')) + '"]');
|
||||
// 6) крестик на панели → Просмотр
|
||||
sr().querySelector('.editbar .barclose').click(); await c.updateComplete;
|
||||
const chrome = sr().querySelector('.editorchrome');
|
||||
out.barCloseStarts = c._mode === 'view' && !chrome.classList.contains('open')
|
||||
&& chrome.getAttribute('aria-hidden') === 'true';
|
||||
// The last bar deliberately remains mounted while its grid row collapses.
|
||||
await new Promise((resolve) => setTimeout(resolve, 220));
|
||||
out.barCloseStarts = c._mode === 'view' && c._modeTransitionBusy
|
||||
&& sr().querySelector('.stage').inert
|
||||
&& sr().querySelector('.stage').classList.contains('mode-transition');
|
||||
await settleMode();
|
||||
out.barCloseWorks = c._mode === 'view'
|
||||
&& getComputedStyle(chrome).visibility === 'hidden'
|
||||
&& chrome.getBoundingClientRect().height < 1;
|
||||
// 7) крестик в самой вкладке → Просмотр (и не переключает режим)
|
||||
tabs()[1].click(); await c.updateComplete;
|
||||
tabs()[1].querySelector('.closex').click(); await c.updateComplete;
|
||||
tabs()[1].click(); await settleMode();
|
||||
tabs()[1].querySelector('.closex').click(); await settleMode();
|
||||
out.tabCrossWorks = c._mode === 'view';
|
||||
|
||||
// Layout-independent editor history: physical KeyZ works when `key` is the
|
||||
// Russian «я», while an input keeps its native browser history.
|
||||
tabs()[0].click(); await c.updateComplete;
|
||||
tabs()[0].click(); await settleMode();
|
||||
c._tool = 'draw'; c.requestUpdate(); await c.updateComplete;
|
||||
const realUndo = c._undoGeometry;
|
||||
const realRedo = c._redoGeometry;
|
||||
|
||||
@@ -7,6 +7,12 @@ const out = await page.evaluate(async () => {
|
||||
const o = {};
|
||||
const c = window.__card;
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
const setFill = async (mode) => {
|
||||
const cfg = JSON.parse(JSON.stringify(c._serverCfg));
|
||||
const f1 = cfg.spaces.find((s) => s.id === 'f1');
|
||||
@@ -47,18 +53,18 @@ const out = await page.evaluate(async () => {
|
||||
o.morphInGlow = le?.querySelector('ha-icon')?.getAttribute('icon')?.includes('lightbulb') ?? false;
|
||||
|
||||
// --- редакторы используют ту же жёлтую подложку независимо от слоя glow --
|
||||
c._setMode('devices'); c.requestUpdate(); await c.updateComplete;
|
||||
c._setMode('devices'); await settleMode();
|
||||
le = devEl('d_lamp');
|
||||
o.devicesEditorLampYellow = !!le && le.classList.contains('on');
|
||||
o.devicesEditorHasGlow = !!sr().querySelector('.stage svg radialGradient, .stage svg [id*=glow]');
|
||||
c._setMode('plan'); c.requestUpdate(); await c.updateComplete;
|
||||
c._setMode('plan'); await settleMode();
|
||||
// ИМЕННО лампа (mutation-проверка аудита HP-1521-01: розетка тоже on и
|
||||
// удовлетворяла бы поиску «любой .dev.on», пряча регресс самой лампы)
|
||||
le = devEl('d_lamp');
|
||||
o.planEditorLampYellow = !!le && le.classList.contains('on'); // слой скрыт — жёлтый вернулся
|
||||
o.planEditorKettleStillOn = !!devEl('d_kettle')?.classList.contains('on');
|
||||
o.planEditorNoGlow = !sr().querySelector('.stage svg radialGradient, .stage svg [id*=glow]');
|
||||
c._setMode('view'); c.requestUpdate(); await c.updateComplete;
|
||||
c._setMode('view'); await settleMode();
|
||||
|
||||
// --- выключенная лампа тёмная в обоих режимах ---------------------------
|
||||
c.hass = { ...c.hass, states: { ...c.hass.states,
|
||||
|
||||
@@ -44,7 +44,7 @@ await page.evaluate(async () => {
|
||||
out.viewHoldInfo = await page.evaluate(() => { const r = !!window.__card._infoCard; window.__card._infoCard = null; window.__card._holdFired = false; return r; });
|
||||
// 3) режим План
|
||||
await page.evaluate(() => window.__card._setMode('plan'));
|
||||
await page.waitForTimeout(200);
|
||||
await page.waitForFunction(() => window.__card._modeTransitionBusy === false);
|
||||
out.plan = await st();
|
||||
out.planIconsHidden = await page.evaluate(() => {
|
||||
const sr = window.__card.shadowRoot || window.__card.renderRoot;
|
||||
@@ -53,7 +53,7 @@ out.planIconsHidden = await page.evaluate(() => {
|
||||
});
|
||||
// 4) режим Устройства: drag работает, клик открывает редактор
|
||||
await page.evaluate(() => window.__card._setMode('devices'));
|
||||
await page.waitForTimeout(200);
|
||||
await page.waitForFunction(() => window.__card._modeTransitionBusy === false);
|
||||
out.devices = await st();
|
||||
out.devDragWorks = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
@@ -78,6 +78,7 @@ out.devClickOpensEditor = await page.evaluate(async () => {
|
||||
});
|
||||
// 5) назад в view
|
||||
await page.evaluate(() => window.__card._setMode('view'));
|
||||
await page.waitForFunction(() => window.__card._modeTransitionBusy === false);
|
||||
out.backToView = (await st()).mode;
|
||||
// значения зафиксированы прогоном на v1.43.1 и сверены с кодом (audit T1)
|
||||
checkAll(out, {
|
||||
|
||||
@@ -15,6 +15,12 @@ const result = await page.evaluate(async () => {
|
||||
c.requestUpdate();
|
||||
await c.updateComplete;
|
||||
};
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
const pitch = 1 / 240;
|
||||
const wallKey = (a, b) => {
|
||||
const q = (v) => Math.round(v / pitch) * pitch;
|
||||
@@ -57,7 +63,7 @@ const result = await page.evaluate(async () => {
|
||||
},
|
||||
};
|
||||
c._setMode('view');
|
||||
await update();
|
||||
await update(); await settleMode();
|
||||
|
||||
const tunnels = (layer) => [...root().querySelectorAll(
|
||||
`.opening-tunnels[data-layer="${layer}"] [data-hp="opening-tunnel"]`,
|
||||
@@ -109,7 +115,7 @@ const result = await page.evaluate(async () => {
|
||||
&& tunnels('data').length === 2 && tunnels('glow-base').length === 2;
|
||||
|
||||
c._setMode('decor');
|
||||
await update();
|
||||
await update(); await settleMode();
|
||||
out.backdropUsesSingleGroupOpacity = tunnels('data').length === 2
|
||||
&& tunnels('glow-base').length === 2
|
||||
&& [...root().querySelectorAll('.opening-tunnels')]
|
||||
@@ -120,7 +126,7 @@ const result = await page.evaluate(async () => {
|
||||
&& byId('tunnel-gate', 'glow-base')?.getAttribute('fill-opacity') === '0.42';
|
||||
|
||||
c._setMode('plan');
|
||||
await update();
|
||||
await update(); await settleMode();
|
||||
out.planGestureCannotLeaveStalePatch = tunnels('data').length === 0
|
||||
&& tunnels('glow-base').length === 0;
|
||||
|
||||
@@ -136,7 +142,7 @@ const result = await page.evaluate(async () => {
|
||||
sp.openings = [{ id: 'atomic-door', type: 'door', x: 0.5, y: 0.2, angle: 0, length: 0.6 }];
|
||||
sp.settings.hide_openings = true;
|
||||
sp.settings.show_borders = true;
|
||||
await update();
|
||||
await update(); await settleMode();
|
||||
const sameDepth = byId('atomic-door', 'data');
|
||||
out.equalAtomicPiecesAreOneSurface = sameDepth?.tagName.toLowerCase() === 'path'
|
||||
&& ((sameDepth.getAttribute('d') || '').match(/\bM /g) || []).length === 2;
|
||||
|
||||
@@ -13,6 +13,12 @@ const res = await page.evaluate(async () => {
|
||||
const sr = () => c.shadowRoot || c.renderRoot;
|
||||
const sp = () => c._serverCfg.spaces.find((s) => s.id === c._space);
|
||||
const upd = async () => { c._cfgEpoch++; c.requestUpdate(); await c.updateComplete; };
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
|
||||
sp().settings = { ...(sp().settings || {}), show_borders: true };
|
||||
delete sp().walls;
|
||||
@@ -22,7 +28,7 @@ const res = await page.evaluate(async () => {
|
||||
|
||||
c._setMode('plan');
|
||||
c._tool = 'wallthick';
|
||||
await upd();
|
||||
await upd(); await settleMode();
|
||||
c._wallThickClick([550, 250]);
|
||||
await upd();
|
||||
if (c._wallDialog) {
|
||||
@@ -53,13 +59,13 @@ const res = await page.evaluate(async () => {
|
||||
out.savedVirtualAboveRealInEditor = !!editorBody && !!editorVirtual
|
||||
&& !!(editorBody.compareDocumentPosition(editorVirtual) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
c._setMode('view');
|
||||
await upd();
|
||||
await upd(); await settleMode();
|
||||
const viewBody = sr().querySelector('.wallbodies');
|
||||
const viewVirtual = sr().querySelector('.openwalls');
|
||||
out.savedVirtualBelowRealInView = !!viewBody && !!viewVirtual
|
||||
&& !!(viewVirtual.compareDocumentPosition(viewBody) & Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
c._setMode('decor');
|
||||
await upd();
|
||||
await upd(); await settleMode();
|
||||
const decorBody = sr().querySelector('.wallbodies');
|
||||
const decorVirtual = sr().querySelector('.openwalls');
|
||||
out.realWallsFadeBehindDecor = !!decorBody
|
||||
@@ -67,7 +73,7 @@ const res = await page.evaluate(async () => {
|
||||
out.virtualWallsFadeBehindDecor = !!decorVirtual
|
||||
&& getComputedStyle(decorVirtual).opacity === '0.35';
|
||||
c._setMode('plan');
|
||||
await upd();
|
||||
await upd(); await settleMode();
|
||||
out.atomicThicknessBefore = c._intervalCm([550, 145, 550, 195]) === 20
|
||||
&& c._intervalCm([550, 205, 550, 295]) === 0
|
||||
&& c._intervalCm([550, 305, 550, 455]) === 20;
|
||||
|
||||
@@ -34,7 +34,7 @@ const res = await page.evaluate(async () => {
|
||||
|
||||
await setSun(180, 40);
|
||||
out.dayComputedWhite = comp() === 'rgb(255, 255, 255)';
|
||||
out.dayNoDim = dim().includes('brightness(1.000)');
|
||||
out.dayNoDim = !dim().includes('brightness(') || dim().includes('brightness(1.000)');
|
||||
out.dayRays = polys() > 0;
|
||||
|
||||
// the whole point: a NEW hass, nothing else. No reload, no requestUpdate.
|
||||
|
||||
@@ -37,7 +37,7 @@ const res = await page.evaluate(async () => {
|
||||
};
|
||||
const settle = async (c) => {
|
||||
const t0 = performance.now();
|
||||
while (c._booting && performance.now() - t0 < 2500) await sleep(30);
|
||||
while ((c._booting || c._modeTransitionBusy) && performance.now() - t0 < 2500) await sleep(30);
|
||||
await sleep(250);
|
||||
};
|
||||
const near = (a, b) => Math.abs(a - b) < 1e-6;
|
||||
@@ -54,7 +54,7 @@ const res = await page.evaluate(async () => {
|
||||
|
||||
const B = mk(); // вторая карточка, тот же конфиг
|
||||
await settle(B);
|
||||
B._setMode('devices'); await B.updateComplete; await sleep(120);
|
||||
B._setMode('devices'); await B.updateComplete; await settle(B);
|
||||
B._applyView(3.35, 200, 200);
|
||||
B.requestUpdate(); await B.updateComplete; await sleep(60);
|
||||
const zoomB = B._zoom;
|
||||
|
||||
+35
-22
@@ -16,13 +16,13 @@ const stageBox = () => page.evaluate(() => {
|
||||
const vh = await page.evaluate(() => window.innerHeight);
|
||||
const inView = await stageBox();
|
||||
await page.evaluate(() => window.__card._setMode('plan'));
|
||||
await page.waitForTimeout(400);
|
||||
await page.waitForFunction(() => window.__card._modeTransitionBusy === false);
|
||||
const inPlan = await stageBox();
|
||||
out.viewFitsViewport = inView.bottom <= vh + 2;
|
||||
out.editorFitsViewport = inPlan.bottom <= vh + 2; // used to overflow by ~90px
|
||||
out.editorStageShrinks = inPlan.top > inView.top && inPlan.bottom <= inView.bottom + 2;
|
||||
await page.evaluate(() => window.__card._setMode('view'));
|
||||
await page.waitForTimeout(300);
|
||||
await page.waitForFunction(() => window.__card._modeTransitionBusy === false);
|
||||
|
||||
// -- zoom out below the base fit -----------------------------------------
|
||||
out.zoomOut = await page.evaluate(() => {
|
||||
@@ -56,16 +56,21 @@ await page.evaluate(() => window.__card._resetZoom());
|
||||
// (zoom AND center) comes back; the editor keeps its own zoom while open.
|
||||
out.editorZoomNotSaved = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const raf2 = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
const center = () => { const v = c._view; return [v.x + v.w / 2, v.y + v.h / 2]; };
|
||||
c._setMode('view');
|
||||
c._resetZoom();
|
||||
c._zoomAt(10, 10, 1.6); c._saveZoom(); // off-center on purpose
|
||||
const want = { zoom: c._zoom, c: center() };
|
||||
c._setMode('devices'); await raf2();
|
||||
c._setMode('devices'); await settleMode();
|
||||
c._zoomAt(10, 10, 2.5); c._saveZoom(); // off-center too: the center must not leak either
|
||||
const editorZoomFree = Math.abs(c._zoom - 2.5) < 0.01; // zooming inside stays
|
||||
c._setMode('view'); await raf2();
|
||||
c._setMode('view'); await settleMode();
|
||||
const got = { zoom: c._zoom, c: center() };
|
||||
const ls = JSON.parse(localStorage.getItem('houseplan_card_zoom_v1') || '{}');
|
||||
return {
|
||||
@@ -84,21 +89,19 @@ delete out.editorZoomNotSaved;
|
||||
// -- editor entered and left without touching zoom: no jump ---------------
|
||||
out.untouchedEditorNoJump = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const raf2 = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
c._setMode('view');
|
||||
// The previous case has only waited two frames after leaving an editor.
|
||||
// Let its 180 ms chrome collapse and ResizeObservers settle before taking a
|
||||
// new baseline; otherwise this case measures the previous transition, not
|
||||
// the untouched editor round-trip it is meant to verify.
|
||||
await new Promise((resolve) => setTimeout(resolve, 240));
|
||||
await settleMode();
|
||||
c._resetZoom();
|
||||
c._zoomAt(30, 30, 1.6); c._saveZoom();
|
||||
const before = { ...c._view };
|
||||
c._setMode('plan'); await raf2();
|
||||
c._setMode('view'); await raf2();
|
||||
// Header ResizeObserver publishes its final measured height one frame after
|
||||
// the 180 ms chrome collapse, then the stage observer performs the last fit.
|
||||
await new Promise((resolve) => setTimeout(resolve, 320));
|
||||
c._setMode('plan'); await settleMode();
|
||||
c._setMode('view'); await settleMode();
|
||||
const v = c._view;
|
||||
const beforeCenter = [before.x + before.w / 2, before.y + before.h / 2];
|
||||
const afterCenter = [v.x + v.w / 2, v.y + v.h / 2];
|
||||
@@ -130,12 +133,17 @@ await page.evaluate(() => window.__card._resetZoom());
|
||||
out.editorZoomNeverInSpaceStore = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const raf2 = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
c._setMode('view');
|
||||
c._resetZoom(); c._zoomAt(10, 10, 1.6); c._saveZoom(); // floor 1 view zoom
|
||||
c._slideTo('garden', 'left'); await wait(350);
|
||||
c._resetZoom(); c._zoomAt(10, 10, 1.4); c._saveZoom(); // floor 2 view zoom
|
||||
c._setMode('devices'); await raf2();
|
||||
c._setMode('devices'); await settleMode();
|
||||
c._zoomAt(10, 10, 5.0); c._saveZoom(); // 500% — a working tool
|
||||
const lsInEditor = JSON.parse(localStorage.getItem('houseplan_card_zoom_v1') || '{}');
|
||||
c._setMode('view');
|
||||
@@ -167,20 +175,25 @@ delete out.editorZoomNeverInSpaceStore;
|
||||
out.crossFloorEditorExit = await page.evaluate(async () => {
|
||||
const c = window.__card;
|
||||
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const raf2 = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
|
||||
const settleMode = async () => {
|
||||
const started = performance.now();
|
||||
do { await new Promise((resolve) => requestAnimationFrame(resolve)); }
|
||||
while (c._modeTransitionBusy && performance.now() - started < 1500);
|
||||
await c.updateComplete;
|
||||
};
|
||||
const sr = c.shadowRoot || c.renderRoot;
|
||||
c._setMode('view');
|
||||
c._resetZoom(); c._zoomAt(10, 10, 1.6); c._saveZoom(); // f1 view zoom
|
||||
c._slideTo('garden', 'left'); await wait(350);
|
||||
c._resetZoom(); c._zoomAt(10, 10, 1.4); c._saveZoom(); // garden view zoom
|
||||
c._slideTo('f1', 'right'); await wait(350);
|
||||
c._setMode('devices'); await raf2();
|
||||
c._setMode('devices'); await settleMode();
|
||||
// этаж меняется НАСТОЯЩЕЙ вкладкой — именно этот путь под подозрением
|
||||
const idx = c._model.findIndex((s) => s.id === 'garden');
|
||||
sr.querySelectorAll('.tabs .tab')[idx].click();
|
||||
await c.updateComplete; await raf2();
|
||||
await c.updateComplete; await settleMode();
|
||||
c._zoomAt(10, 10, 5.0); c._saveZoom(); // 500% — рабочий инструмент
|
||||
c._setMode('view'); await raf2(); await wait(60);
|
||||
c._setMode('view'); await settleMode();
|
||||
const stillGarden = c._space === 'garden';
|
||||
const gotZoom = c._zoom;
|
||||
const v = c._view; const vb = c._baseVb();
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user