mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-06 14:39:22 +00:00
@@ -0,0 +1,135 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildBundleManifest, buildFingerprintPlugin, editorRuntimeRetryUrlPlugin,
|
||||
} from '../scripts/bundle-manifest.mjs';
|
||||
import { assertBundleBudget } from '../scripts/bundle-budget.mjs';
|
||||
import { compareBundleTrees, sha256Bytes, verifyBundleTree } from '../scripts/bundle-tree.mjs';
|
||||
import {
|
||||
minifyCssText, minifyStaticCssTemplates,
|
||||
} from '../scripts/css-template-minifier.mjs';
|
||||
|
||||
test('CSS template minifier preserves semantic whitespace, strings and functions', () => {
|
||||
const css = `
|
||||
/* owner note */
|
||||
.a .b, .c > .d { --pair: 1 2; width: calc(100% - 2px); }
|
||||
.quoted { content: "a b /* text */"; background: url("a b.png"); }
|
||||
.joined/**/.state { color: red; }
|
||||
`;
|
||||
assert.equal(
|
||||
minifyCssText(css),
|
||||
'.a .b,.c>.d{--pair:1 2;width:calc(100% - 2px);}.quoted{content:"a b /* text */";background:url("a b.png");}.joined.state{color:red;}',
|
||||
);
|
||||
assert.equal(
|
||||
minifyStaticCssTemplates('const s = css` .a { content: "\\`"; } `;', 'fixture.ts'),
|
||||
'const s = css`.a{content:"\\`";}`;',
|
||||
);
|
||||
});
|
||||
|
||||
test('CSS template minifier fails closed on interpolation and malformed input', () => {
|
||||
assert.throws(
|
||||
() => minifyStaticCssTemplates('const s = css`color:${value}`;', 'fixture.ts'),
|
||||
/interpolated css template/,
|
||||
);
|
||||
assert.throws(() => minifyCssText('.a{/* nope'), /unclosed CSS comment/);
|
||||
assert.throws(() => minifyCssText('.a{content:"nope}'), /unclosed CSS string/);
|
||||
});
|
||||
|
||||
test('bundle manifest separates static initial graph from dynamic editor graph', () => {
|
||||
const manifest = buildBundleManifest({
|
||||
'houseplan-card.js': {
|
||||
type: 'chunk', fileName: 'houseplan-card.js', code: 'entry', isEntry: true,
|
||||
imports: ['shared.js'], dynamicImports: [
|
||||
'houseplan-assets/editor.js', 'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
|
||||
],
|
||||
},
|
||||
'shared.js': {
|
||||
type: 'chunk', fileName: 'shared.js', code: 'shared', isEntry: false,
|
||||
imports: [], dynamicImports: [],
|
||||
},
|
||||
'houseplan-assets/editor.js': {
|
||||
type: 'chunk', fileName: 'houseplan-assets/editor.js', code: 'editor', isEntry: false,
|
||||
imports: ['shared.js'], dynamicImports: [],
|
||||
},
|
||||
'houseplan-assets/houseplan-onboarding-runtime-HASH.js': {
|
||||
type: 'chunk', fileName: 'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
|
||||
code: 'onboarding', isEntry: false,
|
||||
imports: ['shared.js'], dynamicImports: [],
|
||||
},
|
||||
}, 'fingerprint');
|
||||
assert.deepEqual(manifest.initialViewFiles, ['houseplan-card.js', 'shared.js']);
|
||||
assert.deepEqual(manifest.lazyEditorFiles, ['houseplan-assets/editor.js']);
|
||||
assert.deepEqual(manifest.lazyOnboardingFiles, [
|
||||
'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
|
||||
]);
|
||||
assert.deepEqual(manifest.lazyFiles, [
|
||||
'houseplan-assets/editor.js', 'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
|
||||
]);
|
||||
assert.doesNotThrow(() => assertBundleBudget(manifest, 1_000_000));
|
||||
assert.throws(() => assertBundleBudget(manifest, 1), /exceeds/);
|
||||
});
|
||||
|
||||
test('build fingerprint is embedded for Windows and POSIX source ids', () => {
|
||||
const plugin = buildFingerprintPlugin('exact-build');
|
||||
for (const id of ['C:\\repo\\src\\houseplan-card.ts', '/repo/src/houseplan-editor-runtime.ts']) {
|
||||
const transformed = plugin.transform(
|
||||
'export const fingerprint = "__HOUSEPLAN_SOURCE_FINGERPRINT__";', id,
|
||||
);
|
||||
assert.match(transformed.code, /"exact-build"/);
|
||||
}
|
||||
});
|
||||
|
||||
test('retry URL points at the content-hashed runtime chunk after naming', () => {
|
||||
const plugin = editorRuntimeRetryUrlPlugin();
|
||||
const bundle = {
|
||||
'houseplan-assets/houseplan-card.js': {
|
||||
type: 'chunk', fileName: 'houseplan-assets/houseplan-card.js',
|
||||
code: 'new URL("__HOUSEPLAN_EDITOR_RETRY_ASSET__", import.meta.url);'
|
||||
+ 'new URL("__HOUSEPLAN_ONBOARDING_RETRY_ASSET__", import.meta.url)', modules: {},
|
||||
},
|
||||
'houseplan-assets/houseplan-editor-runtime-HASH.js': {
|
||||
type: 'chunk', fileName: 'houseplan-assets/houseplan-editor-runtime-HASH.js', code: '',
|
||||
modules: { '/repo/src/houseplan-editor-runtime.ts': {} },
|
||||
},
|
||||
'houseplan-assets/houseplan-onboarding-runtime-HASH.js': {
|
||||
type: 'chunk', fileName: 'houseplan-assets/houseplan-onboarding-runtime-HASH.js', code: '',
|
||||
modules: { '/repo/src/houseplan-onboarding-runtime.ts': {} },
|
||||
},
|
||||
};
|
||||
plugin.generateBundle({}, bundle);
|
||||
assert.equal(
|
||||
bundle['houseplan-assets/houseplan-card.js'].code,
|
||||
'new URL("./houseplan-editor-runtime-HASH.js", import.meta.url);'
|
||||
+ 'new URL("./houseplan-onboarding-runtime-HASH.js", import.meta.url)',
|
||||
);
|
||||
});
|
||||
|
||||
test('bundle tree verification fails for a missing or tampered manifest asset', () => {
|
||||
const temp = mkdtempSync(join(tmpdir(), 'houseplan-bundle-tree-'));
|
||||
const source = join(temp, 'source');
|
||||
const target = join(temp, 'target');
|
||||
try {
|
||||
for (const root of [source, target]) {
|
||||
mkdirSync(join(root, 'houseplan-assets'), { recursive: true });
|
||||
writeFileSync(join(root, 'houseplan-card.js'), 'entry');
|
||||
writeFileSync(join(root, 'houseplan-assets', 'editor-hash.js'), 'editor');
|
||||
const files = ['houseplan-card.js', 'houseplan-assets/editor-hash.js'].map((path) => ({
|
||||
path, sha256: sha256Bytes(readFileSync(join(root, path))),
|
||||
}));
|
||||
writeFileSync(join(root, 'houseplan-assets.json'), `${JSON.stringify({
|
||||
schema: 1, fingerprint: 'fixture', entry: 'houseplan-card.js', files,
|
||||
})}\n`);
|
||||
}
|
||||
assert.doesNotThrow(() => compareBundleTrees(source, target));
|
||||
writeFileSync(join(target, 'houseplan-assets', 'editor-hash.js'), 'tampered');
|
||||
assert.throws(() => verifyBundleTree(target), /manifest hash mismatch/);
|
||||
rmSync(join(target, 'houseplan-assets', 'editor-hash.js'));
|
||||
assert.throws(() => verifyBundleTree(target), /manifest asset is missing/);
|
||||
} finally {
|
||||
rmSync(temp, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { resolve } from 'node:path';
|
||||
@@ -44,6 +45,35 @@ test('bundle freshness rejects a missing or stale fingerprint', async () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('bundle freshness verifies every manifest-listed demo asset', async () => {
|
||||
const root = fixtureRoot();
|
||||
try {
|
||||
const expected = sourceFingerprint(root);
|
||||
const assets = resolve(root, 'demo/srv/assets');
|
||||
mkdirSync(resolve(assets, 'houseplan-assets'), { recursive: true });
|
||||
const files = [
|
||||
['houseplan-card.js', 'entry'],
|
||||
['houseplan-assets/editor-HASH.js', 'editor'],
|
||||
];
|
||||
for (const [name, contents] of files) writeFileSync(resolve(assets, name), contents, 'utf8');
|
||||
writeFileSync(resolve(assets, 'houseplan-assets.json'), JSON.stringify({
|
||||
schema: 1,
|
||||
fingerprint: expected,
|
||||
files: files.map(([path, contents]) => ({
|
||||
path,
|
||||
sha256: createHash('sha256').update(contents).digest('hex'),
|
||||
})),
|
||||
}), 'utf8');
|
||||
const page = { evaluate: async () => expected };
|
||||
assert.equal(await assertFreshDemoBundle(page, root), expected);
|
||||
|
||||
writeFileSync(resolve(assets, 'houseplan-assets/editor-HASH.js'), 'tampered', 'utf8');
|
||||
await assert.rejects(assertFreshDemoBundle(page, root), /hash mismatch/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test('bundle freshness uses the target tree fingerprint contract', async () => {
|
||||
const root = fixtureRoot();
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
hexToRgb, hsvToHex, hsvToRgb, normalizeHexColor, normalizeHue, rgbToHex, rgbToHsv,
|
||||
} from '../test-build/color-picker.js';
|
||||
import { readAllStylesSource } from './styles-source.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
test('hex drafts accept only three or six digits and normalize on commit', () => {
|
||||
assert.equal(normalizeHexColor('#AbC'), '#aabbcc');
|
||||
@@ -40,7 +41,7 @@ test('HSV helpers wrap hue, clamp finite dimensions and keep grayscale achromati
|
||||
|
||||
test('the shared component keeps its API and contains no nested native color picker', () => {
|
||||
const component = readFileSync(new URL('../src/hp-color-opacity.ts', import.meta.url), 'utf8');
|
||||
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const card = readHouseplanProductionSource();
|
||||
const sourceFiles = (directory) => readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const target = new URL(entry.name + (entry.isDirectory() ? '/' : ''), directory);
|
||||
return entry.isDirectory() ? sourceFiles(target) : entry.name.endsWith('.ts') ? [target] : [];
|
||||
@@ -80,7 +81,7 @@ test('the hue range exposes one cyclic spectrum without restyling other ranges',
|
||||
});
|
||||
|
||||
test('activity color and ripple size keep independent readable rows', () => {
|
||||
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const card = readHouseplanProductionSource();
|
||||
const styles = readAllStylesSource();
|
||||
const start = card.indexOf("d.display === 'icon_ripple'");
|
||||
const end = card.indexOf("marker.activity_alarm_note", start);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
import {
|
||||
COORDINATE_DECIMALS,
|
||||
@@ -110,11 +111,15 @@ test('one position changes only x/y and preserves future metadata (#224)', () =>
|
||||
});
|
||||
|
||||
test('frontend write paths adopt canonical candidates before persistence (#224)', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
assert.match(source, /enqueueSerializedWrite\(this\._writeChain, async \(\) =>/);
|
||||
assert.match(
|
||||
source,
|
||||
/const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);[\s\S]*config: candidate/,
|
||||
/const candidate = canonicalizeConfigGeometry\(this\._serverCfg\);/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
/const canonicalCandidate = canonicalizeConfigGeometry\(candidate\);[\s\S]*config: canonicalCandidate/,
|
||||
);
|
||||
assert.match(
|
||||
source,
|
||||
|
||||
@@ -3,11 +3,14 @@ import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
import { readAllStylesSource } from './styles-source.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
// #266: styles.ts is being split into surface files — style greps must
|
||||
// see the whole sheet, other sources keep the direct read.
|
||||
const source = (name) => name === 'styles.ts'
|
||||
? readAllStylesSource()
|
||||
: name === 'houseplan-card.ts'
|
||||
? readHouseplanProductionSource()
|
||||
: readFileSync(new URL(`../src/${name}`, import.meta.url), 'utf8');
|
||||
|
||||
test('issue 213 resolves the effective base before the face without a late visual factor', () => {
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||
import test from 'node:test';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { EditorRuntimeLoader } from '../test-build/editor-runtime-loader.js';
|
||||
|
||||
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
const deferred = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
const promise = new Promise((ok, fail) => { resolve = ok; reject = fail; });
|
||||
return { promise, resolve, reject };
|
||||
};
|
||||
|
||||
test('editor runtime loader deduplicates concurrent intent and installs atomically', async () => {
|
||||
const pending = deferred();
|
||||
const states = [];
|
||||
const installed = [];
|
||||
let loads = 0;
|
||||
const loader = new EditorRuntimeLoader({
|
||||
expectedFingerprint: 'same',
|
||||
load: async () => { loads++; return pending.promise; },
|
||||
install: (runtime) => installed.push(runtime),
|
||||
stateChanged: (state) => states.push(state),
|
||||
});
|
||||
|
||||
const first = loader.ensure();
|
||||
const second = loader.ensure();
|
||||
assert.equal(loads, 1);
|
||||
assert.equal(loader.state, 'loading');
|
||||
assert.equal(installed.length, 0);
|
||||
|
||||
pending.resolve({ fingerprint: 'same', create: () => ({ ready: true }) });
|
||||
assert.deepEqual(await Promise.all([first, second]), [true, true]);
|
||||
assert.equal(loads, 1);
|
||||
assert.deepEqual(installed, [{ ready: true }]);
|
||||
assert.deepEqual(states, ['loading', 'ready']);
|
||||
});
|
||||
|
||||
test('editor runtime loader retries the first failure exactly once', async () => {
|
||||
const attempts = [];
|
||||
const loader = new EditorRuntimeLoader({
|
||||
expectedFingerprint: 'same',
|
||||
load: async (attempt) => {
|
||||
attempts.push(attempt);
|
||||
if (attempt === 0) throw new Error('404');
|
||||
return { fingerprint: 'same', create: () => 'runtime' };
|
||||
},
|
||||
install: () => {},
|
||||
});
|
||||
|
||||
assert.equal(await loader.ensure(), true);
|
||||
assert.deepEqual(attempts, [0, 1]);
|
||||
assert.equal(await loader.ensure(), true);
|
||||
assert.deepEqual(attempts, [0, 1], 'a ready loader never imports again');
|
||||
});
|
||||
|
||||
test('fingerprint mismatch fails closed after one retry and remains terminal', async () => {
|
||||
const attempts = [];
|
||||
const installed = [];
|
||||
const failures = [];
|
||||
const loader = new EditorRuntimeLoader({
|
||||
expectedFingerprint: 'entry',
|
||||
load: async (attempt) => {
|
||||
attempts.push(attempt);
|
||||
return { fingerprint: 'other', create: () => 'mixed-runtime' };
|
||||
},
|
||||
install: (runtime) => installed.push(runtime),
|
||||
failed: (error) => failures.push(String(error)),
|
||||
});
|
||||
|
||||
assert.equal(await loader.ensure(), false);
|
||||
assert.deepEqual(attempts, [0, 1]);
|
||||
assert.deepEqual(installed, []);
|
||||
assert.equal(loader.state, 'failed');
|
||||
assert.match(failures[0], /fingerprint mismatch/);
|
||||
|
||||
assert.equal(await loader.ensure(), false);
|
||||
assert.deepEqual(attempts, [0, 1], 'terminal failure requires a page refresh');
|
||||
});
|
||||
|
||||
test('lazy runtime boundary keeps the eager entry independent and does not grow any usage', () => {
|
||||
const card = readFileSync(join(repoRoot, 'src', 'houseplan-card.ts'), 'utf8');
|
||||
assert.doesNotMatch(
|
||||
card,
|
||||
/^import(?!\s+type\b)[^;]*houseplan-editor-runtime[^;]*;/m,
|
||||
'the eager card must not statically import the editor runtime',
|
||||
);
|
||||
const sources = [];
|
||||
const visit = (directory) => {
|
||||
for (const name of readdirSync(directory)) {
|
||||
const path = join(directory, name);
|
||||
if (statSync(path).isDirectory()) visit(path);
|
||||
else if (/\.ts$/.test(name)) sources.push(readFileSync(path, 'utf8'));
|
||||
}
|
||||
};
|
||||
visit(join(repoRoot, 'src'));
|
||||
const anyCount = sources.reduce(
|
||||
(total, source) => total + (source.match(/\bany\b/g)?.length || 0), 0,
|
||||
);
|
||||
assert.ok(anyCount <= 1123, `src any count grew from 1123 to ${anyCount}`);
|
||||
});
|
||||
@@ -1,8 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const card = readHouseplanProductionSource();
|
||||
const editor = readFileSync(new URL('../src/editor.ts', import.meta.url), 'utf8');
|
||||
|
||||
test('active-space writes go through the fixed-floor mutation boundary', () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
GOLDEN_MATRIX_VERSION, GOLDEN_SCENARIOS, OPENING_SYMBOL_EXISTING_GOLDEN_IMPACT,
|
||||
} from '../demo/golden/matrix.mjs';
|
||||
import { fixtureWallKey } from '../demo/fixtures/visual-matrix.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
test('golden matrix has stable unique ids and bounded comparison thresholds', () => {
|
||||
assert.equal(Number.isInteger(GOLDEN_MATRIX_VERSION) && GOLDEN_MATRIX_VERSION > 0, true);
|
||||
@@ -739,7 +740,7 @@ test('golden overrides fail closed on misspelled fixture references', () => {
|
||||
});
|
||||
|
||||
test('a light source paints exactly one region: the floor it can see', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
// One region per source, and it is the visibility polygon clipped to floor.
|
||||
assert.match(source, /visibilityPolygon\(\[pos\.x, pos\.y\], R, occluders/);
|
||||
assert.match(source,
|
||||
@@ -772,7 +773,7 @@ test('a light source paints exactly one region: the floor it can see', () => {
|
||||
});
|
||||
|
||||
test('all destructive editor dialogs use the medium shell and shared responsive footer groups', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
for (const method of ['_renderOpeningDialog', '_renderPhysicalDialog', '_renderSpaceDialog']) {
|
||||
const start = source.indexOf(`private ${method}`);
|
||||
assert.notEqual(start, -1, method);
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Source-contract tests historically inspected one monolithic card file.
|
||||
* Editor implementations are now lazy, so expose the same logical production
|
||||
* surface with the implementation before its typed host stubs.
|
||||
*/
|
||||
export function readHouseplanProductionSource() {
|
||||
const runtime = readFileSync(
|
||||
new URL('../src/houseplan-editor-runtime.ts', import.meta.url),
|
||||
'utf8',
|
||||
);
|
||||
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const parse = (name, source) => ts.createSourceFile(
|
||||
name, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS,
|
||||
);
|
||||
const runtimeFile = parse('houseplan-editor-runtime.ts', runtime);
|
||||
const cardFile = parse('houseplan-card.ts', card);
|
||||
const classOf = (file, name) => file.statements.find(
|
||||
(statement) => ts.isClassDeclaration(statement) && statement.name?.text === name,
|
||||
);
|
||||
const runtimeClass = classOf(runtimeFile, 'HouseplanEditorRuntime');
|
||||
const cardClass = classOf(cardFile, 'HouseplanCard');
|
||||
if (!runtimeClass || !cardClass) throw new Error('Houseplan source classes are missing');
|
||||
const nameOf = (member, file) => member.name?.getText(file);
|
||||
const implementations = new Map(runtimeClass.members
|
||||
.map((member) => [nameOf(member, runtimeFile), member])
|
||||
.filter(([name]) => name));
|
||||
const replacements = [];
|
||||
for (const member of cardClass.members) {
|
||||
const name = nameOf(member, cardFile);
|
||||
const implementation = implementations.get(name);
|
||||
if (!implementation) continue;
|
||||
const baseText = card.slice(member.getStart(cardFile), member.end);
|
||||
if (!baseText.includes('_editorRuntime')) continue;
|
||||
const visibility = /^(private|protected|public)\b/.exec(baseText)?.[1] || 'private';
|
||||
const implementationText = runtime
|
||||
.slice(implementation.getStart(runtimeFile), implementation.end)
|
||||
.replace(/^public\b/, visibility)
|
||||
.replaceAll('this.host.', 'this.');
|
||||
replacements.push({
|
||||
from: member.getStart(cardFile),
|
||||
to: member.end,
|
||||
text: implementationText,
|
||||
});
|
||||
}
|
||||
let reconstructed = card;
|
||||
for (const replacement of replacements.sort((a, b) => b.from - a.from)) {
|
||||
reconstructed = reconstructed.slice(0, replacement.from)
|
||||
+ replacement.text
|
||||
+ reconstructed.slice(replacement.to);
|
||||
}
|
||||
return reconstructed;
|
||||
}
|
||||
+2
-1
@@ -12,13 +12,14 @@ import {
|
||||
resolveLanguageCode,
|
||||
} from '../test-build/i18n/registry.js';
|
||||
import { langOf } from '../test-build/i18n.js';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const dictionaries = new Map(
|
||||
LANGUAGE_REGISTRY.map(({ code, dictionary }) => [code, dictionary]),
|
||||
);
|
||||
const en = dictionaries.get('en');
|
||||
const ru = dictionaries.get('ru');
|
||||
const cardSource = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const cardSource = readHouseplanProductionSource();
|
||||
|
||||
test('i18n: registry codes and English fallback are valid', () => {
|
||||
const codes = LANGUAGE_REGISTRY.map(({ code }) => code);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
cmToUnits, newViolations,
|
||||
} from '../test-build/junction-limits.js';
|
||||
import { GRID_STEP_N } from '../test-build/space-geometry.js';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
// #329, решения владельца 2026-08-27. Пороги абсолютные: 15°, 6 стен,
|
||||
// max(20 см, толщина), 5 см, 25 см². cell_cm на них не влияет.
|
||||
@@ -383,9 +384,7 @@ test('#330 AC4: кэш baseline инвалидируется по конфиг-
|
||||
// приёмом, каким #293 пинит обвязку смока. Поведенческая половина AC4
|
||||
// (N move → N+1 вычислений в реальном жесте) живёт в
|
||||
// demo/smoke_junction_limits.mjs (resizeBaselineCachedPerGesture).
|
||||
const source = readFileSync(
|
||||
new URL('../src/houseplan-card.ts', import.meta.url), 'utf8',
|
||||
);
|
||||
const source = readHouseplanProductionSource();
|
||||
const method = source.slice(
|
||||
source.indexOf('private _junctionLimitsIntroduced('),
|
||||
source.indexOf('private _commitPhysicalGeometry('),
|
||||
|
||||
@@ -3,9 +3,10 @@ import { readdirSync, readFileSync } from 'node:fs';
|
||||
import { join, relative } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import test from 'node:test';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const srcRoot = fileURLToPath(new URL('../src/', import.meta.url));
|
||||
const source = readFileSync(join(srcRoot, 'houseplan-card.ts'), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
const walkTypescript = (directory) => readdirSync(directory, { withFileTypes: true })
|
||||
.flatMap((entry) => {
|
||||
const path = join(directory, entry.name);
|
||||
@@ -27,7 +28,7 @@ test('native selects explicitly project selection unless they are command menus'
|
||||
|
||||
// This is a command menu, not persisted state: it intentionally resets to
|
||||
// its empty first row after inserting an HA variable into decorative text.
|
||||
assert.deepEqual(unsafe, ['houseplan-card.ts:decor-live-attribute']);
|
||||
assert.deepEqual(unsafe, ['houseplan-editor-runtime.ts:decor-live-attribute']);
|
||||
});
|
||||
|
||||
test('persisted dynamic marker selects project their selected option explicitly', () => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { optimizePlans } from '../test-build/plan-optimizer.js';
|
||||
import { MULTI_WALL_NEAR_ORTHOGONAL_MAX_DEGREES } from '../test-build/wall-thickness.js';
|
||||
import { nearAxisProfile } from '../scripts/model-invariants.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const fixture = JSON.parse(readFileSync(
|
||||
new URL('./fixtures/279-near-orthogonal-junction.json', import.meta.url), 'utf8',
|
||||
@@ -21,7 +22,7 @@ const sourceOf = (file) => readFileSync(new URL(`../src/${file}`, import.meta.ur
|
||||
|
||||
test('#290 authoring, Optimize and renderer use one near-axis threshold source', () => {
|
||||
const renderer = sourceOf('wall-thickness.ts');
|
||||
const authoring = sourceOf('houseplan-card.ts');
|
||||
const authoring = readHouseplanProductionSource();
|
||||
const optimizer = sourceOf('plan-optimizer.ts');
|
||||
|
||||
assert.match(renderer, /import \{ NEAR_AXIS_MAX_DEGREES \} from '\.\/near-axis';/);
|
||||
|
||||
@@ -2,8 +2,9 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readAllStylesSource } from './styles-source.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const card = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const card = readHouseplanProductionSource();
|
||||
const styles = readAllStylesSource();
|
||||
const staticRender = readFileSync(new URL('../src/space-render.ts', import.meta.url), 'utf8');
|
||||
const backend = readFileSync(new URL('../custom_components/houseplan/websocket_api.py', import.meta.url), 'utf8');
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
|
||||
const methodBody = (name) => {
|
||||
const start = source.search(new RegExp(`(?:private|protected)\\s+(?:get\\s+|async\\s+)?${name}(?:\\s*=)?(?:\\(|:)`));
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
GLOW_CARD_CONTRACT,
|
||||
LARGE_HOUSE_CARD_CONTRACT,
|
||||
} from '../demo/performance/card-contract.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const directCardMembers = (relativePath) => {
|
||||
const source = readFileSync(new URL(`../${relativePath}`, import.meta.url), 'utf8');
|
||||
@@ -38,10 +39,10 @@ test('Glow benchmark declares every private card member it consumes', () => {
|
||||
});
|
||||
|
||||
test('performance contracts reference real production members', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
for (const contract of [LARGE_HOUSE_CARD_CONTRACT, GLOW_CARD_CONTRACT]) {
|
||||
for (const name of declaredMembers(contract)) {
|
||||
assert.match(source, new RegExp(`\\b(?:private\\s+(?:get\\s+)?|get\\s+)${name}\\b`),
|
||||
assert.match(source, new RegExp(`\\b(?:private\\s+(?:declare\\s+|get\\s+)?|get\\s+)${name}\\b`),
|
||||
`${contract.label} declares missing production member ${name}`);
|
||||
}
|
||||
}
|
||||
@@ -66,7 +67,7 @@ test('contract accepts recent optional fields only when their runtime type is va
|
||||
});
|
||||
|
||||
test('view render resolves structural wall cuts once per frame, not once per room', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
const start = source.indexOf('const allZeroCuts = this._openCuts();');
|
||||
const end = source.indexOf('${this._renderOpeningTunnelFills(space, roomFills)}', start);
|
||||
assert.ok(start >= 0 && end > start, 'view room-render block is present');
|
||||
@@ -87,7 +88,7 @@ test('view render resolves structural wall cuts once per frame, not once per roo
|
||||
});
|
||||
|
||||
test('room inner faces are structurally cached and shared by both fill layers', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
const helperStart = source.indexOf('private _innerRoomContour(');
|
||||
const helperEnd = source.indexOf('\n /**', helperStart);
|
||||
assert.ok(helperStart >= 0 && helperEnd > helperStart, 'inner-contour cache helper is present');
|
||||
@@ -113,7 +114,7 @@ test('room inner faces are structurally cached and shared by both fill layers',
|
||||
});
|
||||
|
||||
test('wall and light geometry reuse bounded caches before structural work', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
|
||||
const unionStart = source.indexOf('private _wallUnionGeometry()');
|
||||
const unionEnd = source.indexOf('\n /** Thick-wall spans', unionStart);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { readFileSync } from 'node:fs';
|
||||
import {
|
||||
createRenderDeviceSnapshot, presentationSnapshotKey, renderDeviceSnapshotPositions,
|
||||
} from '../test-build/render-device-snapshot.js';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
test('snapshot positions skip resolution until a renderable plan exists', () => {
|
||||
const devices = [{ id: 'one' }, { id: 'two' }];
|
||||
@@ -71,7 +72,7 @@ const methodBody = (source, name) => {
|
||||
};
|
||||
|
||||
test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
for (const name of [
|
||||
'_roomLqi', '_resolvedRoomFills', '_sunNow', '_renderSunRays', '_renderGlowLayer',
|
||||
'_renderVacuums', '_renderVacFit', '_renderDevice', '_roomTemp', '_roomHum', '_openingAmt',
|
||||
@@ -82,7 +83,7 @@ test('atomic plan render paths do not bypass RenderDeviceSnapshot with this.hass
|
||||
});
|
||||
|
||||
test('the card gates snapshot positions on the render model', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
const capture = methodBody(source, '_captureRenderDeviceSnapshot');
|
||||
assert.match(
|
||||
capture,
|
||||
@@ -97,7 +98,7 @@ test('the card gates snapshot positions on the render model', () => {
|
||||
});
|
||||
|
||||
test('opening references use their own availability policy without weakening plan tombstones', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
for (const name of [
|
||||
'_contactCandidates', '_lockCandidates', '_openingAmt', '_renderOpenings',
|
||||
'_renderOpeningLocks', '_renderOpeningInfoCard', '_lockAction',
|
||||
@@ -125,7 +126,7 @@ test('opening references use their own availability policy without weakening pla
|
||||
});
|
||||
|
||||
test('lock actuation remains guarded inside the one sanctioned opening-card method', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
const action = methodBody(source, '_lockAction');
|
||||
const guardAt = action.indexOf('_openingEntityAvailable(entityId)');
|
||||
const confirmAt = action.indexOf('confirm(');
|
||||
@@ -136,7 +137,7 @@ test('lock actuation remains guarded inside the one sanctioned opening-card meth
|
||||
});
|
||||
|
||||
test('marker delete/re-add and opening save remain separate config transactions', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
const saveOpening = methodBody(source, '_saveOpening');
|
||||
const saveMarker = methodBody(source, '_saveMarker');
|
||||
assert.match(saveOpening, /sp\.openings/);
|
||||
|
||||
@@ -2,10 +2,12 @@ import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import { readAllStylesSource } from './styles-source.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const card = fs.readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const card = readHouseplanProductionSource();
|
||||
const resize = fs.readFileSync(new URL('../src/resize.ts', import.meta.url), 'utf8');
|
||||
const controller = fs.readFileSync(new URL('../src/resize-controller.ts', import.meta.url), 'utf8');
|
||||
const editorRuntime = fs.readFileSync(new URL('../src/houseplan-editor-runtime.ts', import.meta.url), 'utf8');
|
||||
const wallInvariant = fs.readFileSync(new URL('../src/wall-record-preservation.ts', import.meta.url), 'utf8');
|
||||
const invariantCli = fs.readFileSync(new URL('../scripts/model-invariants.mjs', import.meta.url), 'utf8');
|
||||
const styles = readAllStylesSource();
|
||||
@@ -99,7 +101,7 @@ test('#264 Resize controller is the sole mutable gesture owner', () => {
|
||||
for (const mirror of ['_rszSel', '_rszDrag', '_rszPreview', '_rszLive', '_rszEligibilityCache']) {
|
||||
assert.equal(card.includes(`private ${mirror}`), false, `${mirror} remains a root state mirror`);
|
||||
}
|
||||
assert.match(card, /new ResizeController</);
|
||||
assert.match(editorRuntime, /new ResizeController</);
|
||||
assert.match(controller, /class ResizeController/);
|
||||
assert.doesNotMatch(controller, /LitElement|PointerEvent|requestUpdate|_serverCfg|_writeConfig/);
|
||||
});
|
||||
|
||||
@@ -43,7 +43,7 @@ const UNIT_FORMATTING = new RegExp(
|
||||
*/
|
||||
const FORMATTERS = [
|
||||
{ file: 'src/logic.ts', anchor: 'export function formatLength(', lines: 12 },
|
||||
{ file: 'src/resize.ts', anchor: 'export function formatArea(', lines: 8 },
|
||||
{ file: 'src/area-format.ts', anchor: 'export function formatArea(', lines: 8 },
|
||||
];
|
||||
|
||||
const sourceFiles = (root) => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from '../test-build/wall-segment-model.js';
|
||||
import { wallKey } from '../test-build/wall-thickness.js';
|
||||
import { GRID_STEP_N } from '../test-build/space-geometry.js';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const rectangle = (id, x1 = 0, y1 = 0, x2 = 1, y2 = 1) => ({
|
||||
id,
|
||||
@@ -377,7 +378,7 @@ test('initial migration resolves a reserved deterministic id with the documented
|
||||
});
|
||||
|
||||
test('every frontend structural transaction crosses the wall identity barrier', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
const optimizer = readFileSync(new URL('../src/plan-optimizer.ts', import.meta.url), 'utf8');
|
||||
const methodSource = (name) => {
|
||||
const start = source.indexOf(`private ${name}(`);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import { spaceModels } from '../test-build/space-geometry.js';
|
||||
import { wallBodiesGeometry, wallBodiesUnionPath } from '../test-build/wall-thickness.js';
|
||||
import { checkPhysicalGeometry } from '../scripts/model-invariants.mjs';
|
||||
import { readHouseplanProductionSource } from './houseplan-source.mjs';
|
||||
|
||||
const fixture = JSON.parse(readFileSync(
|
||||
new URL('./fixtures/278-wall-union-isolation.json', import.meta.url), 'utf8',
|
||||
@@ -98,7 +99,7 @@ test('#278 physical fingerprint ignores decor but covers every strict writer fie
|
||||
});
|
||||
|
||||
test('#278 production source routes physical writers through one barrier and decor around it', () => {
|
||||
const source = readFileSync(new URL('../src/houseplan-card.ts', import.meta.url), 'utf8');
|
||||
const source = readHouseplanProductionSource();
|
||||
for (const historyKey of [
|
||||
'draft_segment', 'wall_chain_finish', 'column_add', 'physical_edit', 'physical_delete',
|
||||
'physical_move', 'resize_room', 'wall_thickness',
|
||||
|
||||
Reference in New Issue
Block a user