docs(hygiene): снять чек-листы testing-notes и дизайн-материалы выпущенных задач (#681)

Волна 4 эпика #674.

docs/testing-notes/: восемь ручных чек-листов по поверхностям и индекс
удалены — ни одного отмеченного пункта, ручной фазы в PROCESS.md §2 нет.
Правило #650 (пустое совпадение --test-name-pattern) перенесено в
TESTING.md; пункты [manual] без автоматического свидетеля сведены в раздел
«Чего не проверяет автоматика» (реальный HA, сенсорное устройство, ресурсы
сервера, несколько клиентов, визуальная оценка, пользовательское
содержимое). В TESTING.md снят блок чек-листов v1.43–1.44 и приложения по
issue в разделе golden (#197/#249/#272/#275/#288/#261) — сцены объявлены в
demo/golden/matrix.mjs. Остался реестр браузерных гвардов #659
(mutation-browser-guards.md). test/testing-notes-index.test.mjs →
test/testing-doc.test.mjs: лимит 800 строк, правила #85, раздел ручных
проверок и живые ссылки TESTING.md; каталог testing-notes содержит только
реестр. Мутант testing-notes-index-drops-section (удалял строку индекса) →
testing-doc-drops-manual-section. golden-matrix: копия 67 id сцен #242/#250
в чек-листе снята, список и способ его измерения — в demo/golden/matrix.mjs.

docs/design/505-summary-panel удалён вместе с
demo/capture_summary_panel_505.mjs и маршрутом /reference/ фикстуры
диалога. docs/design/600-settings-dialogs: reference/, screenshots/,
pairs/, ARCHIVE-README, ISSUE-FORM, OPEN-POINTS удалены; SPEC,
IMPLEMENTATION-GUIDE, field-maps, ACCEPTANCE остаются; вывод
capture_design_pairs_600.mjs и verify_ha_form_shell_609.mjs --capture —
в artifacts/. docs/design: 68 файлов / 4,08 МБ → 13 / 0,70 МБ.
README-ha-dialog-505.md → README-ha-dialog.md (путь в release-review.yml);
demo/guard/README.md: запись гварда — в verify-guard.mjs, не в README.

docs/design/649-25d-stage6 не тронут — пункт после стабильного v1.78.0.

Issue: #681
User-Visible: no
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
This commit is contained in:
Claude
2026-09-27 21:27:57 +03:00
parent c8fc8838d8
commit 708ed850f3
84 changed files with 243 additions and 9900 deletions
+3 -2
View File
@@ -1,8 +1,9 @@
// #600 AC1: парные кадры продукта для сравнения с референсом
// docs/design/600-settings-dialogs/ (ACCEPTANCE.md). Диагностическая съёмка, не
// golden: кадры не принимаются golden:accept и не участвуют в golden:verify.
// Кадры — артефакт проверки, в репозиторий не коммитятся (#681).
//
// node demo/capture_design_pairs_600.mjs [--out=docs/design/600-settings-dialogs/pairs]
// node demo/capture_design_pairs_600.mjs [--out=artifacts/design-pairs-600]
//
// Четыре диалога × две темы, ширина поверхности 560, скроллер раскрыт на всю
// высоту, чтобы кадр показывал форму целиком, как макет референса. Состояния —
@@ -14,7 +15,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { launch } from './serve.mjs';
const out = resolve(process.argv.find((a) => a.startsWith('--out='))?.slice(6) || 'docs/design/600-settings-dialogs/pairs');
const out = resolve(process.argv.find((a) => a.startsWith('--out='))?.slice(6) || 'artifacts/design-pairs-600');
mkdirSync(out, { recursive: true });
const THEMES = {
-317
View File
@@ -1,317 +0,0 @@
/** Explicit #505 diagnostic evidence, not a golden-baseline producer or smoke. */
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { sourceFingerprint } from '../scripts/source-fingerprint.mjs';
import { assertFreshDemoBundle } from './bundle-freshness.mjs';
import { HA_DIALOG_PIN, prepareHaDialogAssets } from './helpers/ha-dialog-assets.mjs';
import { launchHaDialogFixture } from './helpers/ha-dialog-fixture.mjs';
const repo = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const options = { output: join(repo, 'artifacts/summary-panel-505') };
for (let index = 2; index < process.argv.length; index++) {
const arg = process.argv[index];
if (arg === '--probe-only') options.probeOnly = true;
else if (['--output', '--ha-cache', '--ha-wheel', '--only'].includes(arg)) {
const value = process.argv[++index];
if (!value || value.startsWith('--')) throw new Error(`Missing value for ${arg}`);
if (arg === '--only') options.only = value.split(',');
else options[{ '--output': 'output', '--ha-cache': 'cacheDir', '--ha-wheel': 'wheelPath' }[arg]] = resolve(value);
} else throw new Error(`Unknown option ${arg}`);
}
// Fail before the optional 124 MB download. Captures never bypass freshness.
const fingerprint = sourceFingerprint(repo);
if (!options.probeOnly) {
const manifest = JSON.parse(readFileSync(join(repo, 'demo/srv/assets/houseplan-assets.json')));
assert.equal(manifest.fingerprint, fingerprint, 'Stale demo bundle: run npm run bundle:sync before capturing');
}
mkdirSync(options.output, { recursive: true });
const report = {
issue: 505, diagnosticOnly: true, platform: process.platform,
sourceSha: execFileSync('git', ['rev-parse', 'HEAD'], { cwd: repo, encoding: 'utf8' }).trim(),
sourceFingerprint: fingerprint, probeOnly: Boolean(options.probeOnly),
selectedCases: options.only || 'full matrix',
haFrontend: HA_DIALOG_PIN.version, wheelSha256: HA_DIALOG_PIN.sha256,
boundaries: 'Loopback synthetic demo only. No core/auth/HA server. Official app loader exposed at runtime; component factories/CSS unmodified.',
comparison: 'Equivalent one block, three default system rows and one entity row. Reference row text is normalized to observed product values; reference CSS/files are unchanged. Floor-plan/fake HA chrome pixels are not parity targets.',
captures: [],
};
const saveReport = () => writeFileSync(join(options.output, 'report.json'), JSON.stringify(report, null, 2));
const assets = await prepareHaDialogAssets(options);
async function probe() {
const fixture = await launchHaDialogFixture({ assets });
try {
const { page } = fixture;
report.chromium = fixture.browser.version();
await page.goto(`${fixture.url}/ha505-probe.html`);
await page.waitForFunction(() => document.querySelector('ha-dialog')?.shadowRoot?.querySelector('wa-dialog')?.shadowRoot?.querySelector('dialog')?.matches(':modal'));
await page.waitForTimeout(300);
const read = () => page.evaluate(() => {
const ha = document.querySelector('ha-dialog');
const wa = ha.shadowRoot.querySelector('wa-dialog');
const surface = wa.shadowRoot.querySelector('dialog');
return { width: surface.getBoundingClientRect().width, modal: surface.matches(':modal'),
radius: getComputedStyle(surface).borderRadius, preset: ha.width,
inheritedMd: getComputedStyle(ha).getPropertyValue('--ha-dialog-width-md') };
});
const small = await read();
await page.evaluate(async () => { const ha = document.querySelector('ha-dialog'); ha.width = 'medium'; await ha.updateComplete; });
await page.waitForTimeout(50);
const medium = await read();
await page.evaluate(() => document.querySelector('#parent').style.setProperty('--ha-dialog-width-md', '920px'));
await page.waitForTimeout(50);
const inherited = await read();
await page.screenshot({ path: join(options.output, 'authentic-ha-width-920.png') });
await page.setViewportSize({ width: 390, height: 844 });
await page.waitForTimeout(100);
const mobile = await read();
await page.screenshot({ path: join(options.output, 'authentic-ha-mobile-390.png') });
assert.equal(small.width, 320); assert.equal(medium.width, 580);
assert.equal(inherited.width, 920); assert.equal(mobile.width, 390);
await fixture.assertClean();
report.probe = { small, medium, inherited, mobile, provenance: fixture.provenance,
pageErrors: fixture.errors, externalRequests: fixture.externalRequests, websocketAttempts: fixture.websocketAttempts };
saveReport();
} finally { await fixture.close(); }
}
const rowLabels = ['Количество устройств', 'Общая площадь комнат', 'Текущие дата и время', 'Температура кухни'];
const productConfig = {
version: 1, title: 'Сводная информация', show_on_mobile: true,
blocks: [{ id: 'block-1', title: 'Общее', visible: true, scope: { type: 'all' },
values: rowLabels.map((label, index) => ({ id: `value-${index + 1}`, label,
source: index < 3 ? { type: 'system', key: ['device_count', 'total_area', 'datetime'][index] }
: { type: 'entity', entity_id: 'sensor.kitchen_temperature' } })) }],
};
const referenceConfig = { title: productConfig.title, showOnMobile: true, blocks: [{
...productConfig.blocks[0], scope: { type: 'all', spaceId: null },
values: rowLabels.map((label, index) => ({ id: `value-${index + 1}`, label,
source: index < 3 ? { type: 'system', key: ['device_count', 'room_area', 'date_time'][index], name: label }
: { type: 'entity', entityId: 'sensor.kitchen_temperature' } })),
}] };
const panelCases = [
{ name: 'desktop-light', width: 1600, height: 1000, theme: 'light' },
{ name: 'desktop-dark', width: 1600, height: 1000, theme: 'dark' },
{ name: 'bottom-portrait', width: 800, height: 1100, theme: 'light' },
{ name: 'kiosk', width: 1600, height: 1000, theme: 'light', kiosk: true },
{ name: 'mobile-view', width: 390, height: 844, theme: 'light' },
];
const settingCases = ['native', 'real-ha'].flatMap((shell) => [
{ name: 'desktop', width: 1600, height: 1000 },
{ name: 'narrow-320', width: 320, height: 844 },
{ name: 'narrow-390', width: 390, height: 844 },
].flatMap((size) => ['light', 'dark'].map((theme) => ({ ...size,
name: `settings-${shell}-${size.name}-${theme}`, theme, shell, settings: true }))));
const edgeCases = ['native', 'real-ha'].flatMap((shell) => [
{ name: `edge-${shell}-effective-zoom-200`, width: 800, height: 500,
adaptation: '800×500 effective viewport proxy for 1600×1000 at 200%; not actual browser zoom' },
{ name: `edge-${shell}-text-200-long-de`, width: 1600, height: 1000, textSize: 32, language: 'de',
adaptation: 'Root font-size 32px enlarged-text stress, long German entity name/id; not browser zoom' },
{ name: `edge-${shell}-text-200-long-de-390`, width: 390, height: 844, textSize: 32, language: 'de',
adaptation: '390px plus root font-size32px and long German source' },
].map((scenario) => ({ ...scenario, shell, settings: true, theme: 'light' })));
function scenarioConfig(scenario, reference = false) {
const config = structuredClone(reference ? referenceConfig : productConfig);
if (scenario.language === 'de') {
config.title = 'Zusammenfassung'; config.blocks[0].title = 'Allgemein';
const row = config.blocks[0].values[3];
row.label = 'Küchentemperatur und langfristiger Messwert';
row.source = { type: 'entity', [reference ? 'entityId' : 'entity_id']:
'sensor.kueche_erdgeschoss_langfristige_temperaturmessung_mit_besonders_langer_quellenkennung_505' };
}
return config;
}
async function configureProduct(page, scenario) {
await page.waitForFunction(() => window.__card?._summary && window.__card?._model?.length > 0);
await assertFreshDemoBundle(page, repo);
await page.waitForFunction(() => window.__card._booting === false && !window.__card._bootFading);
await page.evaluate(async ({ scenario, config }) => {
const card = window.__card;
Object.assign(document.querySelector('#host').style, { width: '100%', height: '100vh', padding: '0', margin: '0', maxWidth: 'none' });
Object.assign(document.body.style, { margin: '0', padding: '0' });
card.panelHost = true;
card.setAttribute('panel-host', '');
Object.assign(card.style, { width: '100%', height: '100%' });
card.setConfig({ ...card._config, language: scenario.language || 'ru', kiosk: Boolean(scenario.kiosk) });
card._serverCfg = { ...card._serverCfg, settings: { ...card._serverCfg.settings, summary_panel: config } };
card._cfgEpoch++;
card._haSummaryPanelApi = 1;
card._serverCanWrite = true;
card.narrow = scenario.width <= 450;
const dark = scenario.theme === 'dark';
const colors = dark ? ['#3ea6ff', '#e6e7eb', '#9aa4ad', '#202126', '#3a3d45', '#11151b', '#2b2d33']
: ['#0b73b8', '#202124', '#5f6368', '#ffffff', '#d7d9de', '#eef1f4', '#e7eaee'];
['primary-color', 'primary-text-color', 'secondary-text-color', 'card-background-color', 'divider-color', 'primary-background-color', 'secondary-background-color']
.forEach((key, index) => document.documentElement.style.setProperty(`--${key}`, colors[index]));
document.documentElement.style.setProperty('--ha-card-background', colors[3]);
document.documentElement.style.setProperty('--app-header-background-color', colors[3]);
document.documentElement.style.setProperty('--app-header-text-color', colors[1]);
document.documentElement.style.setProperty('--text-primary-color', '#ffffff');
document.documentElement.style.colorScheme = scenario.theme;
if (scenario.textSize) document.documentElement.style.fontSize = `${scenario.textSize}px`;
document.body.style.background = colors[5];
const entityId = config.blocks[0].values[3].source.entity_id;
card.hass = { ...card.hass, language: scenario.language || 'ru', locale: { ...card.hass.locale, language: scenario.language || 'ru' },
config: { ...card.hass.config, time_zone: 'Europe/Moscow', unit_system: { ...card.hass.config?.unit_system, length: 'm' } },
themes: { ...card.hass.themes, darkMode: dark },
states: { ...card.hass.states, [entityId]: { entity_id: entityId, state: '20.6', attributes: {
friendly_name: scenario.language === 'de' ? 'Küchentemperatur im Erdgeschoss mit langfristiger Messwertüberwachung und besonders langer Quellenbezeichnung' : 'Kitchen temperature', unit_of_measurement: '°C' } } },
};
card._summary.updated();
card._summary.saveLocal({ show: true });
card.requestUpdate();
await card.updateComplete;
card._summary.resized();
}, { scenario, config: scenarioConfig(scenario) });
await page.waitForFunction(() => window.__card._booting === false && !window.__card._bootFading && window.__card._devices.length > 0);
if (!scenario.settings) await page.locator('.summary-overlay').waitFor({ state: 'visible' });
await page.waitForTimeout(350);
if (scenario.settings) {
await page.evaluate(async () => {
const card = window.__card;
if (card._ensureEditorRuntime) assertRuntime(await card._ensureEditorRuntime());
function assertRuntime(ready) { if (!ready) throw new Error('Editor runtime failed to load'); }
await card._summary.openDialog();
await card.updateComplete;
});
await page.locator('hp-dialog[data-kind="summary"] .summary-editor').waitFor({ state: 'visible' });
await page.waitForTimeout(300);
}
}
async function capture(scenario) {
console.log(`Capturing ${scenario.name}`);
const fixture = await launchHaDialogFixture({ assets, authentic: scenario.shell === 'real-ha',
viewport: { width: scenario.width, height: scenario.height }, colorScheme: scenario.theme });
const record = { ...scenario, files: {} };
try {
const { page } = fixture;
// Let Date.now advance: the real card boot-veil quiescence uses it.
await page.clock.setSystemTime(new Date('2026-09-09T09:00:00Z'));
await page.goto(`${fixture.url}/product.html`);
await configureProduct(page, scenario);
const values = await page.evaluate((config) => config.blocks[0].values.map((value) => window.__card._summary.value(value)), scenarioConfig(scenario));
assert.equal(values.length, 4, 'Equivalent four-row product fixture');
record.product = await page.evaluate((settings) => {
const root = window.__card.shadowRoot;
const box = (node) => node ? { ...node.getBoundingClientRect().toJSON(), scrollWidth: node.scrollWidth, clientWidth: node.clientWidth } : null;
const panel = root.querySelector('.summary-overlay');
const hp = root.querySelector('hp-dialog[data-kind="summary"]');
const ha = hp?.shadowRoot.querySelector('ha-dialog');
const wa = ha?.shadowRoot.querySelector('wa-dialog');
const surface = wa?.shadowRoot.querySelector('dialog') || hp?.shadowRoot.querySelector('.surface');
const footer = hp?.querySelector('[slot="footer"]');
const editor = hp?.querySelector('.summary-editor');
const insideViewport = (node) => { const r = node?.getBoundingClientRect(); return !!r && r.left >= -1 && r.right <= innerWidth + 1 && r.top >= -1 && r.bottom <= innerHeight + 1; };
return { panel: box(panel), control: box(root.querySelector('.summary-control')), stage: box(root.querySelector('.stage')),
side: panel?.classList.contains('bottom') ? 'bottom' : 'right', rows: panel?.querySelectorAll('.summary-value').length,
settings: settings ? { surface: box(surface), editor: box(editor), footer: box(footer),
actualHA: Boolean(wa), modal: surface?.matches(':modal'), radius: getComputedStyle(surface).borderRadius,
border: getComputedStyle(surface).border, shadow: getComputedStyle(surface).boxShadow,
rootFont: getComputedStyle(document.documentElement).fontSize,
titleFont: getComputedStyle(hp.shadowRoot.querySelector('.title')).fontSize,
sourceRows: [...editor.querySelectorAll('.summary-source')].map((node) => ({ box: box(node), text: node.textContent.trim() })),
checks: { surfaceInsideViewport: insideViewport(surface), footerVisible: insideViewport(footer),
editorNoHorizontalOverflow: editor.scrollWidth <= editor.clientWidth + 1,
surfaceNoHorizontalOverflow: surface.scrollWidth <= surface.clientWidth + 1,
footerNoHorizontalOverflow: footer.scrollWidth <= footer.clientWidth + 1 },
close: box(ha?.shadowRoot.querySelector('ha-icon-button') || hp.shadowRoot.querySelector('.close')) } : null };
}, Boolean(scenario.settings));
const shot = async (side, area, locator) => {
const name = `${scenario.name}-${side}-${area}.png`;
if (locator) await locator.screenshot({ path: join(options.output, name) });
else await page.screenshot({ path: join(options.output, name) });
record.files[`${side}-${area}`] = name;
};
await shot('product', 'viewport');
if (scenario.settings) await shot('product', 'dialog', page.locator(scenario.shell === 'real-ha'
? 'hp-dialog[data-kind="summary"] ha-dialog wa-dialog dialog' : 'hp-dialog[data-kind="summary"] .surface'));
else {
await shot('product', 'panel', page.locator('.summary-overlay'));
await shot('product', 'control', page.locator('.summary-control'));
}
if (scenario.language === 'de') {
// The genuine HA owns scrolling in its shadow .body; the native wrapper
// owns it in the editor. Follow the composed tree instead of assuming.
const measureSource = async (align) => page.locator('.summary-source').last().evaluate((source, align) => {
const label = source.querySelector('strong');
(align === 'start' ? label : source).scrollIntoView({ block: align, inline: 'nearest', behavior: 'instant' });
const hp = source.closest('hp-dialog');
const ha = hp.shadowRoot.querySelector('ha-dialog');
const header = ha?.shadowRoot.querySelector('ha-dialog-header') || hp.shadowRoot.querySelector('.header');
const footer = hp.querySelector('[slot="footer"]');
let bandTop = Math.max(0, header?.getBoundingClientRect().bottom || 0);
let bandBottom = Math.min(innerHeight, footer.getBoundingClientRect().top);
const visible = (node) => { const box = node.getBoundingClientRect().toJSON();
const visibleHeight = Math.max(0, Math.min(box.bottom, bandBottom) - Math.max(box.top, bandTop));
return { box, visibleHeight, visibleFraction: visibleHeight / box.height }; };
const scrollOwners = [];
for (let node = source; node; node = node.assignedSlot || node.parentElement || node.getRootNode()?.host) {
if (node.scrollHeight > node.clientHeight + 1 && /auto|scroll/.test(getComputedStyle(node).overflowY)) {
const clip = node.getBoundingClientRect();
bandTop = Math.max(bandTop, clip.top); bandBottom = Math.min(bandBottom, clip.bottom);
scrollOwners.push({ tag: node.localName, class: node.className, scrollTop: node.scrollTop,
scrollHeight: node.scrollHeight, clientHeight: node.clientHeight });
}
}
return { align, bandTop, bandBottom, label: visible(label), source: visible(source), scrollOwners };
}, align);
record.product.longSourceStart = await measureSource('start');
assert.ok(record.product.longSourceStart.label.visibleHeight > 0, 'Long source label must actually appear between header and footer');
await shot('product', 'long-source-scrolled');
record.product.longSourceEnd = await measureSource('end');
assert.ok(record.product.longSourceEnd.source.visibleHeight > 0, 'Long source end must actually be visible');
await shot('product', 'long-source-end');
}
await fixture.assertClean();
await page.addInitScript(({ config, theme, kiosk, textSize }) => {
localStorage.setItem('houseplan-dashboard5-config', JSON.stringify(config));
localStorage.setItem('houseplan-dashboard5-enabled', 'true');
localStorage.setItem('houseplan-dashboard5-theme', theme);
localStorage.setItem('houseplan-dashboard5-kiosk', String(Boolean(kiosk)));
if (textSize) document.addEventListener('DOMContentLoaded', () => { document.documentElement.style.fontSize = `${textSize}px`; });
}, { config: scenarioConfig(scenario, true), theme: scenario.theme, kiosk: scenario.kiosk, textSize: scenario.textSize });
await page.goto(`${fixture.url}/reference/index.html`);
await page.locator('#dashboard3-panel').waitFor({ state: 'visible' });
await page.waitForTimeout(350);
await page.locator('.dashboard3-value dd').evaluateAll((nodes, text) => {
if (nodes.length !== text.length) throw new Error('Equivalent reference row count mismatch');
nodes.forEach((node, index) => { node.textContent = text[index]; });
}, values);
if (scenario.settings) {
await page.locator('.dashboard3-settings-toggle').click();
await page.locator('.dashboard3-settings-dialog__window').waitFor({ state: 'visible' });
}
record.reference = await page.evaluate(() => {
const box = (selector) => document.querySelector(selector)?.getBoundingClientRect().toJSON();
return { panel: box('#dashboard3-panel'), control: box('.dashboard5-summary-control'),
dialog: box('.dashboard3-settings-dialog__window'), normalizedValueText: [...document.querySelectorAll('.dashboard3-value dd')].map((node) => node.textContent) };
});
await shot('reference', 'viewport');
if (scenario.settings) await shot('reference', 'dialog', page.locator('.dashboard3-settings-dialog__window'));
else {
await shot('reference', 'panel', page.locator('#dashboard3-panel'));
await shot('reference', 'control', page.locator('.dashboard5-summary-control'));
}
await fixture.assertClean();
record.pageErrors = fixture.errors; record.externalRequests = fixture.externalRequests;
record.websocketAttempts = fixture.websocketAttempts;
report.captures.push(record); saveReport();
} finally { await fixture.close(); }
}
try {
await probe();
const cases = [...panelCases, ...settingCases, ...edgeCases];
if (options.only) for (const name of options.only) assert.ok(cases.some((scenario) => scenario.name === name), `Unknown case ${name}`);
if (!options.probeOnly) for (const scenario of cases.filter((scenario) => !options.only || options.only.includes(scenario.name))) await capture(scenario);
report.status = 'complete'; saveReport();
console.log(`Diagnostic evidence: ${join(options.output, 'report.json')}`);
} catch (error) {
report.status = 'failed'; report.error = error.stack || String(error); saveReport(); throw error;
}
+7 -3
View File
@@ -107,9 +107,13 @@ const stage3VacuumTrail = {
const stage3RequiredOverlays = ['device', 'room-label', 'opening-lock'];
/** Exact existing baseline impact measured against origin/dev with the same
* Chromium build. Keep this list reviewable: shared visual fixtures make an
* opening-only change visible in editor/dialog captures too. */
/** Exact existing baseline impact of the opening-symbol change (#242/#250, matrix
* v37): measured by comparing `actualSha256` for HEAD and origin/dev under the
* same Chromium build (baseline status alone was not used — dev already had
* unrelated pending candidates). Every listed frame uses a shared fixture with
* an affected opening or keeps that plan behind an editor/dialog; no other
* existing frame changed. Keep this list reviewable: this is its only copy
* since the testing-notes checklists were removed (#681). */
export const OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT = Object.freeze([
'isometric-geometry-view-dark',
'isometric-geometry-view-light',
+3 -2
View File
@@ -24,6 +24,7 @@
одно исключение внутри страницы и обязан выйти с кодом 1.
Новый benchmark, открывающий страницу Playwright, обязан получить такой же
режим и запись здесь: этого требует `test/smoke-harness-contract.test.mjs`, и
режим и запись в `verify-guard.mjs` (`file: '../<имя>.mjs', args:
['--guard-probe']`): этого требует `test/smoke-harness-contract.test.mjs`, и
требует статически — потому что динамическое доказательство возможно только
там, где есть браузер.
там, где есть браузер. Этот README записи не заменяет.
-124
View File
@@ -1,124 +0,0 @@
# Offline authentic HA dialog evidence (#505)
This is an explicit diagnostic, **not** a `smoke_*` entry, a golden-baseline
producer, or a Home Assistant server. It never connects to a real HA instance.
Issue/spec: [#505](https://github.com/Matysh/houseplan-card/issues/505),
[`505-summary-panel-design-parity.md`](../../docs/specs/505-summary-panel-design-parity.md).
The same pinned fixture also verifies #607 against the real HA close button:
```sh
npm run bundle:sync
node demo/verify_ha_dialog_discard_recovery.mjs
```
That diagnostic opens Device, Room, Space and General settings in turn. For
each form it edits a field, presses HA's own close button, chooses Continue,
proves that the same modal and draft are interactive again, then covers
Discard and a clean close. It also fails on duplicate `hp-close` events,
external requests, WebSockets or browser errors. The ordinary native-fallback
contract remains in `demo/smoke_dialog_modal_recovery.mjs`.
Issue #609 uses that same unmodified pinned `ha-dialog` to verify the shared
settings-form shell rather than the close lifecycle:
```sh
npm run bundle:sync
node demo/verify_ha_form_shell_609.mjs --capture
```
It opens the real Room settings form and checks the settled WebAwesome surface:
560 px desktop width, the 940 px/viewport height cap, zero duplicate HA body
padding, one HA-owned scroller, a distinct form canvas, footer containment and
edge-to-edge layouts at 480×800 and 390×844. It also keeps HA's own fullscreen
behaviour for a 1280×480 viewport. `--capture` writes the three reviewed diagnostic
images under `docs/design/600-settings-dialogs/pairs/`; without it the command
is read-only. The authentic diagnostic explicitly applies the light palette,
then repeats desktop geometry in the dark palette with a 32 px root font and
proves that canvas contrast, the single HA scroller and the footer survive.
The fast Validate witness is
`demo/smoke_ha_form_shell_parity.mjs`; neither diagnostic is a golden producer.
From a checkout with the usual `npm ci` dependencies and Playwright Chromium:
```sh
node demo/capture_summary_panel_505.mjs --probe-only
node demo/capture_summary_panel_505.mjs --output artifacts/summary-panel-505
```
The second command requires an already fresh `npm run bundle:sync` result.
It never builds or changes product/reference files, and refuses stale source
fingerprints even when `HP_ALLOW_STALE_BUNDLE` is set. For reviewer work, use
`--output "$RUNNER_TEMP/summary-panel-505"` to keep all evidence outside the checkout.
Use `--only edge-real-ha-text-200-long-de-390` for a named focused recapture;
comma-separated names are accepted and recorded as a partial selection.
The first explicit run downloads the official 124,294,469-byte
`home-assistant-frontend==20260729.7` wheel from the exact allowlisted PyPI file
URL; version, size and SHA-256 are pinned in `ha-dialog-assets.mjs`, checked
before extraction, and checked against `tests_backend/requirements.txt`.
The wheel is **not installed**. Only allowlisted modern JS/static assets are
extracted to the OS temporary cache. The deterministic cache manifest has its
own pinned hash; existing extracted bytes are then checked against its per-file
hashes each run, so edited cache metadata is not trusted. Use a new cache directory
after corruption; the tool does not delete existing data.
To reuse an already downloaded official wheel or select a disposable cache:
```sh
node demo/capture_summary_panel_505.mjs --probe-only --ha-wheel /path/to/home_assistant_frontend-20260729.7-py3-none-any.whl --ha-cache /path/to/cache --output /path/to/evidence
```
`HP_HA_DIALOG_WHEEL` also supplies a local wheel. All paths are caller-selected;
there are no author-machine paths in the helper. Cache contents and screenshots
are not committed.
## Authenticity and safety boundary
The official `app.d53ce8172fc8c85d.js` is served with one explicit runtime
instrumentation: its final `o(91535);` becomes
`window.__ha505Require=o;o(91535);`, exposing its otherwise private module
loader. The downloaded wheel and the `ha-dialog`, WebAwesome, Lit and other
component factories/styles remain unmodified. Numbered official ESM chunks
register their original factories with that loader; there is no dialog stub.
This is a genuine-component diagnostic, **not** a complete production HA boot.
No `core.*` entry, authentication root or HA server is loaded. The server binds
only `127.0.0.1` on an ephemeral port. A restrictive CSP, browser request
allowlist, blocked service workers and throwing WebSocket constructor prohibit
external/HA requests; any attempted external request, WebSocket or page error
fails the run. The synthetic demo HTML is adapted only in its served response
to await the authentic component bootstrap and avoid duplicate icon/card tag
registration. No production HA configuration, devices or services are touched.
## Evidence and limits
`report.json` records source SHA/fingerprint, Chromium/platform, wheel pin/hash,
measured surface/editor/footer/control geometry, actual shadow-component
presence, typography and network errors. The standalone probe proves the
small320/medium580/public inherited-width920 contract plus mobile390 fullscreen.
It does not load House Plan; the full settings cases exercise actual `hp-dialog`.
The full command writes paired viewport/region PNGs for five panel contexts
(desktop light/dark, portrait bottom, kiosk, mobile View), native/real-HA
settings at desktop/320/390 in both themes, and enlarged-text/long-German
source edge cases. One default block has three system rows plus one entity row
on both sides. Reference displayed values are normalized to measured product
values **only as fixture text**, not CSS; original reference files stay intact.
The archived prototype UI is Russian; German stress changes fixture data and
product locale, not the archived prototype UI translation.
Long-source start/end images use `scrollIntoView` and record the actual composed
scroll owner plus source/label visibility between header and footer. This matters
because genuine HA scrolls its shadow body, not necessarily `.summary-editor`.
The 800×500 case is explicitly an effective-viewport proxy for a 1600×1000
display at 200%, **not actual browser zoom**. Separate 32px root-font cases
exercise enlarged text. HA normally uses a 14px root, so the report includes
actual computed font sizes. Different fake HA chrome/floor-plan pixels are not
parity targets. Fullscreen HA rules, HA typography/theme, larger touch targets
and the native disabled Save action remain the spec's allowed adaptations.
`status: complete` means capture completed, not design acceptance. Inspect the
PNGs and the settings `checks`; these files are diagnostic evidence outside
canonical `demo/golden/baselines` and docs screenshot manifests. Regenerate on
the reviewer's pinned checkout and record the exact command/report paths.
+76
View File
@@ -0,0 +1,76 @@
# Offline authentic HA dialog fixture (#505, #607, #609)
`demo/helpers/ha-dialog-fixture.mjs` serves the synthetic demo together with the
official, pinned Home Assistant `ha-dialog` so a diagnostic can exercise the
real component. It is **not** a `smoke_*` entry, a golden-baseline producer, or
a Home Assistant server, and it never connects to a real HA instance. It was
built for the #505 summary-panel parity work
([`505-summary-panel-design-parity.md`](../../docs/specs/505-summary-panel-design-parity.md));
that one-off capture script and its designer prototype were removed by #681 and
remain in git history. Two diagnostics use the fixture today.
#607 verifies the real HA close button:
```sh
npm run bundle:sync
node demo/verify_ha_dialog_discard_recovery.mjs
```
That diagnostic opens Device, Room, Space and General settings in turn. For
each form it edits a field, presses HA's own close button, chooses Continue,
proves that the same modal and draft are interactive again, then covers
Discard and a clean close. It also fails on duplicate `hp-close` events,
external requests, WebSockets or browser errors. The ordinary native-fallback
contract remains in `demo/smoke_dialog_modal_recovery.mjs`.
Issue #609 uses that same unmodified pinned `ha-dialog` to verify the shared
settings-form shell rather than the close lifecycle:
```sh
npm run bundle:sync
node demo/verify_ha_form_shell_609.mjs --capture
```
It opens the real Room settings form and checks the settled WebAwesome surface:
560 px desktop width, the 940 px/viewport height cap, zero duplicate HA body
padding, one HA-owned scroller, a distinct form canvas, footer containment and
edge-to-edge layouts at 480×800 and 390×844. It also keeps HA's own fullscreen
behaviour for a 1280×480 viewport. `--capture` writes the diagnostic images into
the ignored `artifacts/ha-form-shell-609/`; without it the command is
read-only. The authentic diagnostic explicitly applies the light palette,
then repeats desktop geometry in the dark palette with a 32 px root font and
proves that canvas contrast, the single HA scroller and the footer survive.
The fast Validate witness is
`demo/smoke_ha_form_shell_parity.mjs`; neither diagnostic is a golden producer.
The first explicit run downloads the official 124,294,469-byte
`home-assistant-frontend==20260729.7` wheel from the exact allowlisted PyPI file
URL; version, size and SHA-256 are pinned in `ha-dialog-assets.mjs`, checked
before extraction, and checked against `tests_backend/requirements.txt`.
The wheel is **not installed**. Only allowlisted modern JS/static assets are
extracted to the OS temporary cache. The deterministic cache manifest has its
own pinned hash; existing extracted bytes are then checked against its per-file
hashes each run, so edited cache metadata is not trusted. After corruption,
remove that temporary cache directory; the tool does not delete existing data.
To reuse an already downloaded official wheel, point `HP_HA_DIALOG_WHEEL` at
it. There are no author-machine paths in the helper; cache contents and
screenshots are never committed.
## Authenticity and safety boundary
The official `app.d53ce8172fc8c85d.js` is served with one explicit runtime
instrumentation: its final `o(91535);` becomes
`window.__ha505Require=o;o(91535);`, exposing its otherwise private module
loader. The downloaded wheel and the `ha-dialog`, WebAwesome, Lit and other
component factories/styles remain unmodified. Numbered official ESM chunks
register their original factories with that loader; there is no dialog stub.
This is a genuine-component diagnostic, **not** a complete production HA boot.
No `core.*` entry, authentication root or HA server is loaded. The server binds
only `127.0.0.1` on an ephemeral port. A restrictive CSP, browser request
allowlist, blocked service workers and throwing WebSocket constructor prohibit
external/HA requests; any attempted external request, WebSocket or page error
fails the run. The synthetic demo HTML is adapted only in its served response
to await the authentic component bootstrap and avoid duplicate icon/card tag
registration. No production HA configuration, devices or services are touched.
+1 -4
View File
@@ -70,10 +70,7 @@ export async function launchHaDialogFixture({
} else {
let base = join(REPO, 'demo/srv'), relative = pathname;
if (pathname === '/product.html') relative = '/demo.html';
if (pathname.startsWith('/reference/')) {
base = join(REPO, 'docs/design/505-summary-panel/reference');
relative = pathname.slice('/reference'.length);
} else if (ha && (pathname.startsWith('/frontend_latest/') || pathname.startsWith('/static/'))) base = ha.root;
if (ha && (pathname.startsWith('/frontend_latest/') || pathname.startsWith('/static/'))) base = ha.root;
const file = resolve(base, '.' + relative);
if (!file.startsWith(resolve(base) + sep) || !existsSync(file) || !statSync(file).isFile()) {
response.writeHead(404).end('Not found'); return;
+4 -1
View File
@@ -3,13 +3,16 @@
// Room settings form, and verifies the public ha-dialog geometry on desktop
// and both sides of HA's built-in 450 px mobile breakpoint.
import assert from 'node:assert/strict';
import { mkdirSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { assertFreshDemoBundle } from './bundle-freshness.mjs';
import { launchHaDialogFixture } from './helpers/ha-dialog-fixture.mjs';
const capture = process.argv.includes('--capture');
const pairDir = fileURLToPath(new URL('../docs/design/600-settings-dialogs/pairs/', import.meta.url));
// `--capture` writes diagnostic images into ignored artifacts/, never into docs (#681).
const pairDir = fileURLToPath(new URL('../artifacts/ha-form-shell-609/', import.meta.url));
if (capture) mkdirSync(pairDir, { recursive: true });
const fixture = await launchHaDialogFixture({
authentic: true,
viewport: { width: 1600, height: 1000 },