mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +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
67 lines
3.2 KiB
JavaScript
67 lines
3.2 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Отрицательный прогон гарда исключений (#404).
|
||
*
|
||
* Гард объявлен в `demo/serve.mjs` с 2026-07-27 и до этой задачи не срабатывал
|
||
* ни разу: счётчик читался раньше, чем Playwright доставлял `pageerror`. Такое
|
||
* ловится только запуском — «проверка, которая не умеет падать» выглядит
|
||
* идентично работающей во всём, кроме исхода.
|
||
*
|
||
* Почему не тест в `test/`: пробам нужен настоящий Chromium, а job «Фронтенд»,
|
||
* где идёт `npm test`, браузеры не ставит. Тест там молча скипался бы — то есть
|
||
* ровно тот тихий успех, против которого всё это и делается. Поэтому проверка
|
||
* живёт в job «Смоки в браузере», где браузер есть, и вызывается один раз.
|
||
*/
|
||
import { spawnSync } from 'node:child_process';
|
||
import { dirname, resolve } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const HERE = dirname(fileURLToPath(import.meta.url));
|
||
|
||
/** Каждая проба: чего ждём от кода возврата и что обязано быть в выводе. */
|
||
const PROBES = [
|
||
{
|
||
file: 'guard_tail_exception.mjs',
|
||
expectExit: 1,
|
||
expectOutput: /uncaught exception\(s\) inside the card/,
|
||
because: 'исключение в хвосте — та самая слепая зона, из-за которой заводился #404',
|
||
},
|
||
{
|
||
file: 'guard_tail_rejection.mjs',
|
||
expectExit: 1,
|
||
expectOutput: /uncaught exception\(s\) inside the card/,
|
||
because: 'отклонение промиса приходит тем же каналом; на этом держится связь с #405',
|
||
},
|
||
{
|
||
file: 'guard_closed_page.mjs',
|
||
expectExit: 0,
|
||
expectOutput: /OK/,
|
||
because: 'round-trip к закрытой странице не имеет права ронять вердикт',
|
||
},
|
||
];
|
||
|
||
let failed = 0;
|
||
for (const probe of PROBES) {
|
||
const run = spawnSync(process.execPath, [resolve(HERE, probe.file)], {
|
||
encoding: 'utf8', cwd: resolve(HERE, '../..'), timeout: 90_000,
|
||
});
|
||
const output = `${run.stdout || ''}${run.stderr || ''}`;
|
||
const exitOk = run.status === probe.expectExit;
|
||
const textOk = probe.expectOutput.test(output);
|
||
if (exitOk && textOk) {
|
||
console.log(`ok ${probe.file} → exit ${run.status}`);
|
||
continue;
|
||
}
|
||
failed += 1;
|
||
console.error(`FAIL ${probe.file}: ${probe.because}`);
|
||
console.error(` ожидался exit ${probe.expectExit}, получен ${run.status}`);
|
||
if (!textOk) console.error(` в выводе нет ${probe.expectOutput}`);
|
||
console.error(output.split('\n').slice(-12).map((line) => ` | ${line}`).join('\n'));
|
||
}
|
||
|
||
if (failed) {
|
||
console.error(`\nгард исключений не доказан: проб провалено ${failed} из ${PROBES.length}`);
|
||
process.exit(1);
|
||
}
|
||
console.log(`\nгард исключений доказан на ${PROBES.length} пробах`);
|