mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-07 15:09:30 +00:00
Count delivered page errors after awaited browser close. Exercise the public harness lifecycle with deterministic negative subprocess cases and retain the unchanged Chromium probes for the real transport. Move the premature-snapshot mutation witness to the deterministic Node suite. Issue: #776 User-Visible: no
123 lines
7.9 KiB
JavaScript
123 lines
7.9 KiB
JavaScript
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
// #404. Гард «uncaught exception внутри карточки» существовал с 2026-07-27 и не
|
||
// срабатывал ни разу: счётчик читался синхронно, а Playwright доставляет
|
||
// pageerror асинхронно. Поведение доказывается запуском проб —
|
||
// demo/guard/verify-guard.mjs в job со браузером. Здесь закреплено то, что
|
||
// запуском этих проб не проверить: место проб и наличие вызова в CI.
|
||
// Порядок доставки/закрытия/вердикта проверяется исполнением публичного API в
|
||
// smoke-harness-lifecycle.test.mjs (#776), а не текстом реализации.
|
||
|
||
const ROOT = fileURLToPath(new URL('../', import.meta.url));
|
||
const read = (rel) => readFileSync(new URL(rel, `file://${ROOT}`), 'utf8');
|
||
|
||
test('пробы гарда лежат вне маски смоков и вне корпуса отпечатка (#404)', () => {
|
||
const guard = readdirSync(new URL('demo/guard/', `file://${ROOT}`));
|
||
assert.ok(guard.includes('verify-guard.mjs'));
|
||
const probes = guard.filter((name) => name.startsWith('guard_'));
|
||
assert.deepEqual(probes.sort(), [
|
||
'guard_closed_page.mjs',
|
||
'guard_report_page_errors.mjs',
|
||
'guard_tail_exception.mjs',
|
||
'guard_tail_rejection.mjs',
|
||
], 'четыре адресные пробы гарда обязаны оставаться обнаружимыми');
|
||
|
||
// Маска demo/smoke_*.mjs — то, что гоняют шарды CI. Проба, попавшая туда,
|
||
// покрасит шард по построению: она обязана падать.
|
||
for (const name of probes) {
|
||
assert.equal(name.startsWith('smoke_'), false, `${name} попала бы в шарды CI`);
|
||
}
|
||
|
||
// demo/fixtures/**/*.mjs входит в корпус sourceFingerprint: каждый файл там
|
||
// объявляет устаревшими бандл, скриншот-индекс и golden-индекс. Пробы гарда
|
||
// не касаются ни одного пикселя — платить за них пересъёмкой нечем.
|
||
for (const name of probes.concat('verify-guard.mjs')) {
|
||
assert.equal(existsSync(new URL(`demo/fixtures/${name}`, `file://${ROOT}`)), false,
|
||
`${name} в demo/fixtures/ протухал бы отпечаток исходников`);
|
||
}
|
||
});
|
||
|
||
test('пробы вызываются в job с браузером и служат guard мутантов (#404)', () => {
|
||
const workflow = read('.github/workflows/validate.yml');
|
||
const smoke = workflow.slice(workflow.indexOf('\n smoke:\n'), workflow.indexOf('\n smoke_done:\n'));
|
||
assert.match(smoke, /node demo\/guard\/verify-guard\.mjs/,
|
||
'проверка обязана идти там, где есть Chromium: в job «Фронтенд» npm test браузер не ставит,'
|
||
+ ' и тест на пробах молча скипался бы');
|
||
assert.match(smoke, /if: matrix\.shard == 1/, 'один раз, а не в каждом шарде');
|
||
|
||
const mutants = read('scripts/mutation-registry.mjs');
|
||
// Только browser guards: premature snapshot с #776 доказывается без гонки
|
||
// в smoke-harness-lifecycle.test.mjs. Настоящий канал Playwright остаётся
|
||
// предметом verify-guard и остальных browser guards.
|
||
const guarded = [
|
||
'smoke-guard-forgets-to-register-pages',
|
||
'report-page-errors-skips-round-trip',
|
||
'benchmark-page-verdict-unwatched',
|
||
];
|
||
for (const id of guarded) {
|
||
assert.match(mutants, new RegExp(`id: '${id}'`), `мутант ${id} не зарегистрирован`);
|
||
}
|
||
assert.equal(
|
||
(mutants.match(/node demo\/guard\/verify-guard\.mjs/g) || []).length,
|
||
guarded.length,
|
||
'мутант на пробах гарда есть, а в списке выше его нет — список отстал от реестра',
|
||
);
|
||
});
|
||
|
||
test('#434 smoke job and each smoke file have independent time bounds', () => {
|
||
const workflow = read('.github/workflows/validate.yml');
|
||
const smoke = workflow.slice(workflow.indexOf('\n smoke:\n'), workflow.indexOf('\n smoke_done:\n'));
|
||
assert.match(smoke, /\n timeout-minutes: 20\n/,
|
||
'job smoke must not inherit the six-hour GitHub Actions default');
|
||
assert.match(smoke, /timeout --kill-after=10s 180s node "\$f"/,
|
||
'one hung file must be killed while the remaining shard files still run');
|
||
assert.match(smoke, /if \[ "\$status" -eq 124 \]; then\s*\n\s*echo "diagnostic smoke-timeout:/,
|
||
'a timeout needs a distinct diagnostic rather than a generic test failure');
|
||
});
|
||
|
||
test('страницы, созданные вне launch(), подписаны общим гардом (#404, Medium-1)', () => {
|
||
// Ревью ТЗ нашло разрыв: smoke_zoom_flash открывал вторую страницу со своей
|
||
// подпиской, печатавшей EXC2 мимо счётчика, а три страницы smoke_svg_sandbox
|
||
// не имели слушателя вовсе. Регистрация в launchInternal их не покрывает.
|
||
const zoom = read('demo/smoke_zoom_flash.mjs');
|
||
assert.match(zoom, /watchPage\(await ctx\.newPage\(\)\)/);
|
||
assert.equal(/p2\.on\('pageerror'/.test(zoom), false,
|
||
'своя подписка мимо счётчика убрана (EXC2 остался только в объяснении)');
|
||
|
||
const sandbox = read('demo/smoke_svg_sandbox.mjs');
|
||
assert.equal((sandbox.match(/watchPage\(await ctx\.newPage\(\)\)/g) || []).length, 3);
|
||
|
||
// Общий инвариант — не «никто не подписывается сам», а «ни одна страница не
|
||
// создаётся мимо гарда». Своя подписка поверх гарда законна и полезна:
|
||
// smoke_cold_view_toggle и smoke_cold_view_vacuum считают исключения ещё и
|
||
// отдельным утверждением `noPageErrors`, а их страница приходит из
|
||
// launchColdView, то есть уже зарегистрирована.
|
||
const demo = fileURLToPath(new URL('demo/', `file://${ROOT}`));
|
||
const smokes = readdirSync(demo)
|
||
.filter((name) => name.startsWith('smoke_') && name.endsWith('.mjs'));
|
||
const unwatched = [];
|
||
for (const name of smokes) {
|
||
const text = readFileSync(demo + name, 'utf8');
|
||
const total = (text.match(/newPage\(\)/g) || []).length;
|
||
const watched = (text.match(/watchPage\(await [\w.]+\.newPage\(\)\)/g) || []).length;
|
||
if (total > watched) unwatched.push(name);
|
||
}
|
||
assert.deepEqual(unwatched, ['smoke_entry_stale.mjs'],
|
||
'страница, созданная мимо watchPage, остаётся слепой зоной гарда.'
|
||
+ ' Единственное законное исключение — smoke_entry_stale: он намеренно'
|
||
+ ' поднимает свой браузер, ведёт свой счётчик исключений (он и есть'
|
||
+ ' предмет проверки #353) и выносит вердикт через finish()');
|
||
|
||
// Свои подписки допускаются только поверх общего гарда: страница обязана
|
||
// приходить из launch()/launchColdView(), иначе счётчик гарда её не видит.
|
||
for (const name of smokes) {
|
||
const text = readFileSync(demo + name, 'utf8');
|
||
if (!/\.on\('pageerror'/.test(text) || name === 'smoke_entry_stale.mjs') continue;
|
||
assert.match(text, /launch(ColdView)?[,}]|launch(ColdView)?\(/,
|
||
`${name}: своя подписка на pageerror поверх страницы, которую гард не знает`);
|
||
}
|
||
});
|