mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
Гард «uncaught exception внутри карточки» жил в demo/serve.mjs с 2026-07-27 и не срабатывал ни разу в самом частом случае. Счётчик читался синхронно, а Playwright доставляет pageerror асинхронно по CDP: если исключение возникло после последнего обращения смока к странице, счётчик к моменту проверки нулевой, а browser.close() уносит недоставленное событие. В логе это видно дословно — EXC печатается после результата и до OK. finish() теперь делает round-trip по открытым страницам перед чтением счётчика. Страницы регистрируются там, где создаются: ссылок на них у finish(browser, out) нет, а менять сигнатуру нельзя — так её зовут 205 смоков. Medium-1 жёлтого ревью ТЗ закрыт расширением, а не оговоркой. Страницы, созданные смоком после launch(), регистрация в launchInternal не покрывает: smoke_zoom_flash печатал своё EXC2 мимо счётчика, три страницы smoke_svg_sandbox не имели слушателя вовсе. Документировать слепую зону в задаче, которая существует ради устранения слепой зоны, значит закрыть issue, оставив дефект. Наружу отдана одна функция watchPage(page): подписка и регистрация неразделимы, иначе появится страница, чьи исключения считаются, а доставки не ждёт никто. Разрыв оказался шире, чем в ревью: проверка по всему набору нашла ещё два файла со своей подпиской — smoke_cold_view_toggle и smoke_cold_view_vacuum. Они не слепая зона, их страница приходит из launchColdView и уже зарегистрирована, а свой счётчик они превращают в отдельное утверждение. Поэтому инвариант сформулирован как «ни одна страница не создаётся мимо гарда» и закреплён по всему набору, а не по двум названным файлам. reportPageErrors() из #407 стал асинхронным: второй читатель счётчика обязан ждать доставку так же, как finish(). Пять смоков получили await. Фикстура smoke_danger_confirmation приведена к объявленному типу: без binding и bindingMode _bindingHasHaPage падал на undefined.split(':') — два исключения, которых гард не видел. Дефекта поведения нет, все 15 мест в src/, создающих диалог, binding пишут; врала фикстура. Два отступления от ТЗ, каждое по измеренной причине. Пробы лежат в demo/guard/, а не demo/fixtures/: последний входит в корпус sourceFingerprint, и каждый файл там объявил бы устаревшими бандл, скриншот-индекс и golden-индекс — пробы же не касаются ни одного пикселя. Поведение доказывается в job со браузером, а не в npm test: job «Фронтенд» браузеры не ставит, и тест молча скипался бы — тот самый тихий успех, против которого вся задача. Issue: #404 User-Visible: no
74 lines
3.6 KiB
JavaScript
74 lines
3.6 KiB
JavaScript
/** Regression smoke for #59: a modal must terminate the held view gesture. */
|
|
import { launch, reportPageErrors } from './serve.mjs';
|
|
|
|
const { page, browser } = await launch({ width: 1000, height: 820 }, 1);
|
|
try {
|
|
const result = await page.evaluate(async () => {
|
|
const card = window.__card;
|
|
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
const until = async (predicate, timeout = 5000) => {
|
|
const started = performance.now();
|
|
while (!predicate()) {
|
|
if (performance.now() - started > timeout) throw new Error('long-press smoke timed out');
|
|
await wait(20);
|
|
}
|
|
};
|
|
card._setMode('view');
|
|
await card.updateComplete;
|
|
const device = card.renderRoot.querySelector('.stage .dev');
|
|
const stage = card.renderRoot.querySelector('.stage');
|
|
if (!device || !stage) throw new Error('fixture has no view marker/stage');
|
|
|
|
device.dispatchEvent(new PointerEvent('pointerdown', {
|
|
bubbles: true, composed: true, pointerId: 5901, pointerType: 'mouse',
|
|
clientX: 240, clientY: 260, button: 0, buttons: 1,
|
|
}));
|
|
if (!card._panStart || !card._pointers.has(5901))
|
|
throw new Error('precondition failed: stage gesture did not start');
|
|
await until(() => !!card._infoCard, 1500);
|
|
await card.updateComplete;
|
|
const clearedOnOpen = !card._panStart && !card._pinchStart
|
|
&& card._pointers.size === 0 && !card._swipeStart;
|
|
|
|
const dialog = card.renderRoot.querySelector('hp-dialog');
|
|
dialog?.dispatchEvent(new CustomEvent('hp-close', { bubbles: true, composed: true }));
|
|
await card.updateComplete;
|
|
const before = JSON.stringify(card._view);
|
|
stage.dispatchEvent(new PointerEvent('pointermove', {
|
|
bubbles: true, composed: true, pointerId: 5901, pointerType: 'mouse',
|
|
clientX: 520, clientY: 470, buttons: 0,
|
|
}));
|
|
await card.updateComplete;
|
|
const idleAfterClose = !card._infoCard && !card._panStart
|
|
&& card._pointers.size === 0 && JSON.stringify(card._view) === before;
|
|
|
|
// One clean click on the stage must not consume a stale marker press or
|
|
// reopen the device card. Device-specific short-click actions (toggle,
|
|
// more-info, cover, run) are covered by their dedicated smokes.
|
|
stage.dispatchEvent(new PointerEvent('pointerdown', {
|
|
bubbles: true, composed: true, pointerId: 5902, pointerType: 'mouse',
|
|
clientX: 740, clientY: 680, button: 0, buttons: 1,
|
|
}));
|
|
stage.dispatchEvent(new PointerEvent('pointerup', {
|
|
bubbles: true, composed: true, pointerId: 5902, pointerType: 'mouse',
|
|
clientX: 740, clientY: 680, button: 0, buttons: 0,
|
|
}));
|
|
stage.dispatchEvent(new MouseEvent('click', {
|
|
bubbles: true, composed: true, clientX: 740, clientY: 680, button: 0,
|
|
}));
|
|
await card.updateComplete;
|
|
const normalNextClick = !card._infoCard && !card._holdFired
|
|
&& !card._panStart && card._pointers.size === 0;
|
|
return { clearedOnOpen, idleAfterClose, normalNextClick };
|
|
});
|
|
if (!Object.values(result).every(Boolean))
|
|
throw new Error(`long-press gesture regression: ${JSON.stringify(result)}`);
|
|
// #407: проверки выше бросают на своей регрессии, но про исключения внутри
|
|
// карточки не спрашивает ни одна. Бросаем и здесь — тогда `finally` закроет
|
|
// браузер, а строка успеха не напечатается после «FAILED».
|
|
if (await reportPageErrors()) throw new Error('uncaught exception inside the card — see EXC above');
|
|
console.log(JSON.stringify({ ok: true, ...result }));
|
|
} finally {
|
|
await browser.close();
|
|
}
|