mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 13:48:57 +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
5.1 KiB
JavaScript
74 lines
5.1 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { readFileSync, readdirSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
// #407. Счётчик исключений внутри карточки живёт в demo/serve.mjs, и до этой
|
||
// задачи его читала одна функция — finish(). Шесть смоков её не вызывали вовсе:
|
||
// у каждого была своя развязка, и ни одна не спрашивала про исключения. То есть
|
||
// необработанное исключение во время этих шести проходило незамеченным всегда —
|
||
// в лог печаталось `EXC`, а прогон оставался зелёным.
|
||
//
|
||
// Проверка статическая и грубая: она не доказывает, что вердикт вынесен
|
||
// правильно, а доказывает, что его вообще запрашивают. Этого достаточно, потому
|
||
// что пропуск здесь молчаливый — а молчаливый пропуск и есть то, что стоит
|
||
// ловить механизмом.
|
||
|
||
const DEMO = fileURLToPath(new URL('../demo/', import.meta.url));
|
||
const smokes = () => readdirSync(DEMO)
|
||
.filter((name) => name.startsWith('smoke_') && name.endsWith('.mjs'));
|
||
const read = (name) => readFileSync(new URL(name, `file://${DEMO}`), 'utf8');
|
||
|
||
test('каждый смок спрашивает вердикт по исключениям в карточке (#407)', () => {
|
||
const silent = smokes().filter((name) => {
|
||
const text = read(name);
|
||
return !/\bfinish\s*\(/.test(text) && !/\breportPageErrors\s*\(/.test(text);
|
||
});
|
||
assert.deepEqual(silent, [],
|
||
'смок обязан либо вызвать finish(), либо спросить reportPageErrors():'
|
||
+ ' иначе исключение внутри карточки во время него не увидит никто,'
|
||
+ ' а `EXC` в логе прогон зелёным быть не мешает (#407)');
|
||
});
|
||
|
||
test('вердикт по исключениям запрашивается после прогона, а не до (#407)', () => {
|
||
// Вызов раньше самих действий бессмысленен: счётчик к тому моменту нулевой.
|
||
// Проверяется дешёвым признаком — вердикт не может быть первым обращением к
|
||
// странице в файле.
|
||
for (const name of smokes()) {
|
||
const text = read(name);
|
||
const verdict = Math.max(text.search(/\breportPageErrors\s*\(/), text.search(/\bawait finish\s*\(/));
|
||
if (verdict < 0) continue;
|
||
const firstAction = text.search(/\b(page|card)\.|\blaunch\s*\(/);
|
||
if (firstAction < 0) continue;
|
||
assert.ok(verdict > firstAction,
|
||
`${name}: вердикт по исключениям запрашивается прежде, чем что-то произошло`);
|
||
}
|
||
});
|
||
|
||
test('вердикт не только выставляет код, но и останавливает смок (#407)', () => {
|
||
// Первый заход этой задачи выставлял только process.exitCode, и отрицательный
|
||
// прогон напечатал «FAILED: 1 uncaught exception» и следом «OK deep-link: …».
|
||
// Код возврата был верным, а вывод — противоречивым; читают же вывод.
|
||
for (const name of smokes()) {
|
||
const text = read(name);
|
||
if (!/\breportPageErrors\s*\(/.test(text)) continue;
|
||
assert.match(text, /if \(await reportPageErrors\(\)\)\s*(process\.exit\(1\)|throw )/,
|
||
`${name}: вердикт по исключениям обязан останавливать смок, а не только`
|
||
+ ' помечать его — иначе после «FAILED» печатается строка успеха');
|
||
}
|
||
});
|
||
|
||
test('serve.mjs остаётся единственным владельцем счётчика (#407)', () => {
|
||
// Смок, который завёл бы свой счётчик и свою проверку, вернул бы ровно ту
|
||
// ситуацию, которую задача чинит: у smoke_entry_stale собственный pageErrors
|
||
// существует законно (он и есть предмет проверки), но вердикт всё равно
|
||
// выносит finish().
|
||
const serve = readFileSync(new URL('../demo/serve.mjs', import.meta.url), 'utf8');
|
||
assert.match(serve, /export async function reportPageErrors\(\)/,
|
||
'вердикт стал асинхронным в #404: он ждёт доставки событий страницей');
|
||
assert.equal((serve.match(/_pageErrors\+\+/g) || []).length, 1,
|
||
'счётчик инкрементируется в одном месте — внутри watchPage (#404)');
|
||
const entryStale = read('smoke_entry_stale.mjs');
|
||
assert.match(entryStale, /await finish\(/, 'smoke_entry_stale обязан выносить вердикт');
|
||
});
|