mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 19:58:50 +00:00
fix: six smokes never asked whether the card threw
Счётчик исключений внутри карточки живёт в demo/serve.mjs, и читала его одна функция — finish(). Шесть смоков её не вызывали вовсе: у трёх своя развязка (`if (!ok) process.exit(1)`), у двух throw из try/finally, у smoke_entry_stale ни того ни другого. Необработанное исключение во время этих шести проходило незамеченным всегда — в лог печаталось EXC, а прогон оставался зелёным. smoke_entry_stale был хуже остальных: он складывал неудачи в _failures через check/checkAll, но их никто не печатал и код возврата не выставлял. То есть смок не мог провалиться в принципе — ровно паттерн «печатали булевы значения и всегда выходили нулём», который шапка serve.mjs описывает как исправленный в 2026-07-27. Добавлен reportPageErrors(): тот же вердикт, что у finish(), для смоков со своей логикой выхода. Каждый из шести теперь вердикт запрашивает, а smoke_entry_stale получил finish() и вместе с ним настоящий код возврата. Вердикт обязан ОСТАНАВЛИВАТЬ, а не только помечать. Первый заход выставлял process.exitCode, и отрицательный прогон напечатал «FAILED: 1 uncaught exception(s)» и следом «OK deep-link: …»: код был верным, вывод противоречивым, а читают вывод. Доказано отрицательным прогоном, а не рассуждением: на ветке experiment/407-negative smoke_deeplink получил намеренное исключение внутри карточки, шард 2/3 упал с exit code 1, в логе FAIL и FAILED без строки успеха. Ветка удалена. Гейт против повторения — test/smoke-harness-contract.test.mjs: он падает, если смок не запрашивает вердикт или запрашивает, не останавливаясь. На origin/dev до починки он находил ровно шесть файлов, после — ноль. Issue: #407 User-Visible: no
This commit is contained in:
@@ -31,6 +31,32 @@ export function checkAll(out, expected = {}) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Тот же вердикт по исключениям в карточке, что у `finish()`, для смоков со
|
||||
* своей логикой выхода (#407).
|
||||
*
|
||||
* Счётчик `_pageErrors` живёт здесь, а читал его только `finish()`. Шесть
|
||||
* смоков `finish()` не вызывали вовсе — у каждого была своя ручная развязка
|
||||
* (`if (!ok) process.exit(1)`) либо `throw` из try/finally, и ни одна не
|
||||
* спрашивала про исключения. То есть необработанное исключение внутри карточки
|
||||
* во время этих шести проходило незамеченным всегда: в лог печаталось `EXC`, а
|
||||
* прогон оставался зелёным.
|
||||
*
|
||||
* Функция ничего не бросает намеренно. Смок с ручной развязкой продолжает
|
||||
* решать сам, что печатать и когда выходить; здесь только выставляется код
|
||||
* возврата, который дальше уже не отнять — `process.exitCode` переживёт любой
|
||||
* последующий `console.log`.
|
||||
*
|
||||
* @returns true, если карточка бросала — чтобы вызывающий мог добавить своё
|
||||
* сообщение, не считая исключения заново.
|
||||
*/
|
||||
export function reportPageErrors() {
|
||||
if (!_pageErrors) return false;
|
||||
console.error(`FAILED: ${_pageErrors} uncaught exception(s) inside the card`);
|
||||
process.exitCode = 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Print the result, report failures, close the browser, set the exit code. */
|
||||
export async function finish(browser, out) {
|
||||
if (out !== undefined) console.log(JSON.stringify(out, null, 1));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Smoke: the full houseplan-card honours the #space=<id> deep-link (hashchange).
|
||||
import { launch } from './serve.mjs';
|
||||
import { launch, reportPageErrors } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 820, height: 760 }, 1);
|
||||
const res = await page.evaluate(async () => {
|
||||
const card = window.__card;
|
||||
@@ -22,5 +22,8 @@ const res = await page.evaluate(async () => {
|
||||
await browser.close();
|
||||
const ok = res.after === res.target && res.afterBad === res.target;
|
||||
console.log(JSON.stringify(res));
|
||||
// #407: своя развязка про исключения в карточке не спрашивает. Вердикт обязан
|
||||
// именно остановить: иначе строка успеха печатается после «FAILED».
|
||||
if (reportPageErrors()) process.exit(1);
|
||||
if (!ok) { console.error('FAIL deeplink smoke'); process.exit(1); }
|
||||
console.log('OK deep-link: full card switches to #space=<id>, ignores invalid ids');
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// and `await import(entry)` + `createElement(...).setConfig(...)` stays valid.
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { chromium } from 'playwright';
|
||||
import { check, checkAll } from './serve.mjs';
|
||||
import { check, checkAll, finish } from './serve.mjs';
|
||||
|
||||
const entry = readFileSync('dist/houseplan-card.js');
|
||||
const page404 = `<!doctype html><meta charset="utf-8"><body><script type="module">
|
||||
@@ -52,5 +52,9 @@ const out = {
|
||||
&& fr.text.includes('recharger la page'),
|
||||
};
|
||||
checkAll(out);
|
||||
check('smoke_entry_stale', Object.values(out).every(Boolean));
|
||||
if (Object.values(out).every(Boolean)) console.log('OK');
|
||||
// #407: до этого смок не мог провалиться в принципе. `check`/`checkAll`
|
||||
// складывали неудачи в `_failures`, но их никто не печатал и код возврата не
|
||||
// выставлял — ровно тот паттерн «печатали булевы значения и всегда выходили
|
||||
// нулём», который описан в шапке serve.mjs как исправленный в 2026-07-27.
|
||||
// Браузеры закрыты внутри run(), поэтому finish() получает undefined.
|
||||
await finish(undefined, out);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Browser pixel smoke for the isolated additive group (#19). */
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { launch } from './serve.mjs';
|
||||
import { launch, reportPageErrors } from './serve.mjs';
|
||||
|
||||
const fixture = JSON.parse(readFileSync(
|
||||
new URL('../test/fixtures/glow/additive-pools.json', import.meta.url), 'utf8',
|
||||
@@ -218,6 +218,10 @@ try {
|
||||
throw new Error(`custom data fill changed: ${out.customFill}/${out.customOpacity}`);
|
||||
if (out.customBase || out.customBaseTunnel || !out.customLayerOrder)
|
||||
throw new Error(`data fill was tinted/reordered by Glow base: full=${out.customBase}, tunnel=${out.customBaseTunnel}, order=${out.customLayerOrder}`);
|
||||
// #407: проверки выше бросают на своей регрессии, но про исключения внутри
|
||||
// карточки не спрашивает ни одна. Бросаем и здесь — тогда `finally` закроет
|
||||
// браузер, а строка успеха не напечатается после «FAILED».
|
||||
if (reportPageErrors()) throw new Error('uncaught exception inside the card — see EXC above');
|
||||
console.log(JSON.stringify({ ok: true, blend: out.blend, pools: out.pools, staticParity: true, staticPools: out.staticPools }));
|
||||
} finally {
|
||||
await browser.close();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Asserts device icon badges sit exactly on their anchor point (no content-box/border drift).
|
||||
import { launch } from './serve.mjs';
|
||||
import { launch, reportPageErrors } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 900, height: 1000 }, 2);
|
||||
const bad = await page.evaluate(() => {
|
||||
const c = window.__card;
|
||||
@@ -24,5 +24,8 @@ const bad = await page.evaluate(() => {
|
||||
return bad;
|
||||
});
|
||||
await browser.close();
|
||||
// #407: своя развязка про исключения в карточке не спрашивает. Вердикт обязан
|
||||
// именно остановить: иначе строка успеха печатается после «FAILED».
|
||||
if (reportPageErrors()) process.exit(1);
|
||||
if (bad.length) { console.error('FAIL icon-center: badges off anchor', JSON.stringify(bad.slice(0, 5))); process.exit(1); }
|
||||
console.log('OK icon-center: all device badges centred on their anchor');
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** Regression smoke for #59: a modal must terminate the held view gesture. */
|
||||
import { launch } from './serve.mjs';
|
||||
import { launch, reportPageErrors } from './serve.mjs';
|
||||
|
||||
const { page, browser } = await launch({ width: 1000, height: 820 }, 1);
|
||||
try {
|
||||
@@ -63,6 +63,10 @@ try {
|
||||
});
|
||||
if (!Object.values(result).every(Boolean))
|
||||
throw new Error(`long-press gesture regression: ${JSON.stringify(result)}`);
|
||||
// #407: проверки выше бросают на своей регрессии, но про исключения внутри
|
||||
// карточки не спрашивает ни одна. Бросаем и здесь — тогда `finally` закроет
|
||||
// браузер, а строка успеха не напечатается после «FAILED».
|
||||
if (reportPageErrors()) throw new Error('uncaught exception inside the card — see EXC above');
|
||||
console.log(JSON.stringify({ ok: true, ...result }));
|
||||
} finally {
|
||||
await browser.close();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Smoke: houseplan-space-card renders a live, non-interactive schematic + deep-link button.
|
||||
import { launch } from './serve.mjs';
|
||||
import { launch, reportPageErrors } from './serve.mjs';
|
||||
const { page, browser } = await launch({ width: 900, height: 900 }, 1);
|
||||
const res = await page.evaluate(async () => {
|
||||
await customElements.whenDefined('houseplan-space-card');
|
||||
@@ -243,5 +243,8 @@ const ok =
|
||||
typeof res.deepLink === 'string' && res.deepLink.includes('#space=') &&
|
||||
res.errorShown;
|
||||
console.log(JSON.stringify(res));
|
||||
// #407: своя развязка про исключения в карточке не спрашивает. Вердикт обязан
|
||||
// именно остановить: иначе строка успеха печатается после «FAILED».
|
||||
if (reportPageErrors()) process.exit(1);
|
||||
if (!ok) { console.error('FAIL space-card smoke'); process.exit(1); }
|
||||
console.log('OK space-card: live shared marker face, pointer-events:none, deep-link button, error card');
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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 \(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 function reportPageErrors\(\)/);
|
||||
assert.equal((serve.match(/_pageErrors\+\+/g) || []).length, 1,
|
||||
'счётчик инкрементируется в одном месте');
|
||||
const entryStale = read('smoke_entry_stale.mjs');
|
||||
assert.match(entryStale, /await finish\(/, 'smoke_entry_stale обязан выносить вердикт');
|
||||
});
|
||||
Reference in New Issue
Block a user