mirror of
https://github.com/Matysh/houseplan-card
synced 2026-10-04 05:41:34 +00:00
+289
-11
@@ -8,19 +8,61 @@ import test from 'node:test';
|
||||
|
||||
import {
|
||||
buildBundleManifest, buildFingerprintPlugin, editorRuntimeRetryUrlPlugin,
|
||||
entryFallbackPlugin,
|
||||
} from '../scripts/bundle-manifest.mjs';
|
||||
import {
|
||||
INITIAL_VIEW_CEILING_BAND, INITIAL_VIEW_GZIP_BUDGET, INITIAL_VIEW_GZIP_CEILING,
|
||||
INITIAL_PANEL_ONLY_GZIP_BUDGET, INITIAL_VIEW_CEILING_BAND,
|
||||
INITIAL_VIEW_GZIP_BUDGET, INITIAL_VIEW_GZIP_CEILING,
|
||||
LOW_HEADROOM_ACKNOWLEDGED_CEILING, LOW_HEADROOM_WARNING_BYTES,
|
||||
SUPPORT_LAZY_MARKERS,
|
||||
assertBundleBudget, assertSupportBundleOwnership, initialViewCeilingViolation,
|
||||
lowHeadroomWarning,
|
||||
} from '../scripts/bundle-budget.mjs';
|
||||
import { compareBundleTrees, sha256Bytes, verifyBundleTree } from '../scripts/bundle-tree.mjs';
|
||||
import {
|
||||
assertBundleManifest, compareBundleTrees, orderedBundlePayload, sha256Bytes,
|
||||
verifyBundleTree,
|
||||
} from '../scripts/bundle-tree.mjs';
|
||||
import {
|
||||
minifyCssText, minifyStaticCssTemplates,
|
||||
} from '../scripts/css-template-minifier.mjs';
|
||||
|
||||
const minimalTwoEntryBundle = () => ({
|
||||
'houseplan-panel.js': {
|
||||
type: 'chunk', fileName: 'houseplan-panel.js', code: 'panel', isEntry: true,
|
||||
facadeModuleId: '/repo/src/houseplan-panel.ts', imports: ['houseplan-card.js'],
|
||||
dynamicImports: [], modules: { '/repo/src/houseplan-panel.ts': {} },
|
||||
},
|
||||
'houseplan-card.js': {
|
||||
type: 'chunk', fileName: 'houseplan-card.js', code: 'card', isEntry: true,
|
||||
facadeModuleId: '/repo/src/houseplan-card.ts', imports: ['houseplan-assets/card-HASH.js'],
|
||||
dynamicImports: [], modules: { '/repo/src/houseplan-card.ts': {} },
|
||||
},
|
||||
'houseplan-assets/card-HASH.js': {
|
||||
type: 'chunk', fileName: 'houseplan-assets/card-HASH.js', code: 'implementation',
|
||||
isEntry: false, imports: [], dynamicImports: [], modules: {},
|
||||
},
|
||||
});
|
||||
|
||||
const minimalTwoEntryManifest = () => ({
|
||||
schema: 1,
|
||||
fingerprint: 'f'.repeat(64),
|
||||
entry: 'houseplan-card.js',
|
||||
panelEntry: 'houseplan-panel.js',
|
||||
initialViewFiles: ['houseplan-assets/card-HASH.js', 'houseplan-card.js'],
|
||||
initialViewGzipBytes: 11,
|
||||
initialPanelFiles: [
|
||||
'houseplan-assets/card-HASH.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
],
|
||||
initialPanelGzipBytes: 14,
|
||||
initialPanelOnlyFiles: ['houseplan-panel.js'],
|
||||
initialPanelOnlyGzipBytes: 3,
|
||||
files: [
|
||||
{ path: 'houseplan-assets/card-HASH.js', gzipBytes: 6, isEntry: false },
|
||||
{ path: 'houseplan-card.js', gzipBytes: 5, isEntry: true },
|
||||
{ path: 'houseplan-panel.js', gzipBytes: 3, isEntry: true },
|
||||
],
|
||||
});
|
||||
|
||||
test('CSS template minifier preserves semantic whitespace, strings and functions', () => {
|
||||
const css = `
|
||||
/* owner note */
|
||||
@@ -49,8 +91,14 @@ test('CSS template minifier fails closed on interpolation and malformed input',
|
||||
|
||||
test('bundle manifest separates static initial graph from dynamic editor graph', () => {
|
||||
const manifest = buildBundleManifest({
|
||||
'houseplan-panel.js': {
|
||||
type: 'chunk', fileName: 'houseplan-panel.js', code: 'panel', isEntry: true,
|
||||
facadeModuleId: '/repo/src/houseplan-panel.ts',
|
||||
imports: ['houseplan-card.js'], dynamicImports: [],
|
||||
},
|
||||
'houseplan-card.js': {
|
||||
type: 'chunk', fileName: 'houseplan-card.js', code: 'entry', isEntry: true,
|
||||
facadeModuleId: '/repo/src/houseplan-card.ts',
|
||||
imports: ['shared.js'], dynamicImports: [
|
||||
'houseplan-assets/editor.js', 'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
|
||||
'houseplan-assets/de-HASH.js', 'houseplan-assets/iso-scene-render-HASH.js',
|
||||
@@ -94,7 +142,18 @@ test('bundle manifest separates static initial graph from dynamic editor graph',
|
||||
modules: { '/repo/src/pdf/pdf-export.ts': {} },
|
||||
},
|
||||
}, 'fingerprint');
|
||||
assert.equal(manifest.entry, 'houseplan-card.js');
|
||||
assert.equal(manifest.panelEntry, 'houseplan-panel.js');
|
||||
assert.deepEqual(manifest.initialViewFiles, ['houseplan-card.js', 'shared.js']);
|
||||
assert.deepEqual(
|
||||
manifest.initialPanelFiles,
|
||||
['houseplan-card.js', 'houseplan-panel.js', 'shared.js'],
|
||||
);
|
||||
assert.deepEqual(manifest.initialPanelOnlyFiles, ['houseplan-panel.js']);
|
||||
assert.equal(
|
||||
manifest.initialPanelOnlyGzipBytes,
|
||||
manifest.files.find((file) => file.path === 'houseplan-panel.js').gzipBytes,
|
||||
);
|
||||
assert.deepEqual(manifest.lazyOnboardingFiles, [
|
||||
'houseplan-assets/houseplan-onboarding-runtime-HASH.js',
|
||||
]);
|
||||
@@ -112,6 +171,141 @@ test('bundle manifest separates static initial graph from dynamic editor graph',
|
||||
]);
|
||||
assert.doesNotThrow(() => assertBundleBudget(manifest, 1_000_000));
|
||||
assert.throws(() => assertBundleBudget(manifest, 1), /exceeds/);
|
||||
assert.throws(
|
||||
() => assertBundleBudget(manifest, 1_000_000, manifest.initialPanelOnlyGzipBytes - 1),
|
||||
/panel-only graph.*exceeds/,
|
||||
);
|
||||
});
|
||||
|
||||
test('#486 Rollup names both stable entries explicitly', async () => {
|
||||
const { default: config } = await import('../rollup.config.mjs');
|
||||
assert.deepEqual(config.input, {
|
||||
'houseplan-card': 'src/houseplan-card.ts',
|
||||
'houseplan-panel': 'src/houseplan-panel.ts',
|
||||
});
|
||||
assert.equal(config.output.entryFileNames, '[name].js');
|
||||
});
|
||||
|
||||
test('#486 manifest root selection is exact and independent of entry enumeration order', () => {
|
||||
const bundle = minimalTwoEntryBundle();
|
||||
const reversed = Object.fromEntries(Object.entries(bundle).reverse());
|
||||
const manifest = buildBundleManifest(reversed, 'fingerprint');
|
||||
assert.equal(manifest.entry, 'houseplan-card.js');
|
||||
assert.equal(manifest.panelEntry, 'houseplan-panel.js');
|
||||
assert.deepEqual(manifest.initialViewFiles, [
|
||||
'houseplan-assets/card-HASH.js', 'houseplan-card.js',
|
||||
]);
|
||||
assert.deepEqual(manifest.initialPanelFiles, [
|
||||
'houseplan-assets/card-HASH.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
]);
|
||||
|
||||
const wrongFacade = minimalTwoEntryBundle();
|
||||
wrongFacade['houseplan-panel.js'].facadeModuleId = '/repo/src/not-the-panel.ts';
|
||||
assert.throws(
|
||||
() => buildBundleManifest(wrongFacade, 'fingerprint'),
|
||||
/houseplan-panel\.js facade.*expected src\/houseplan-panel\.ts/,
|
||||
);
|
||||
const extraEntry = minimalTwoEntryBundle();
|
||||
extraEntry['unexpected.js'] = {
|
||||
type: 'chunk', fileName: 'unexpected.js', code: '', isEntry: true,
|
||||
facadeModuleId: '/repo/src/unexpected.ts', imports: [], dynamicImports: [], modules: {},
|
||||
};
|
||||
assert.throws(() => buildBundleManifest(extraEntry, 'fingerprint'), /entry count is 3/);
|
||||
});
|
||||
|
||||
test('#486 manifest graph validator rejects missing panel roots and duplicated card graphs', () => {
|
||||
const valid = minimalTwoEntryManifest();
|
||||
assert.equal(assertBundleManifest(valid), valid);
|
||||
|
||||
const missingPanel = structuredClone(valid);
|
||||
delete missingPanel.panelEntry;
|
||||
assert.throws(() => assertBundleManifest(missingPanel), /expected entries/);
|
||||
|
||||
const wrongEntryFlags = structuredClone(valid);
|
||||
wrongEntryFlags.files.find((file) => file.path === 'houseplan-panel.js').isEntry = false;
|
||||
assert.throws(() => assertBundleManifest(wrongEntryFlags), /isEntry inventory/);
|
||||
|
||||
const duplicateGraph = structuredClone(valid);
|
||||
duplicateGraph.files.push({
|
||||
path: 'houseplan-assets/card-copy-HASH.js', gzipBytes: 6, isEntry: false,
|
||||
});
|
||||
duplicateGraph.initialPanelFiles = [
|
||||
'houseplan-assets/card-copy-HASH.js', 'houseplan-panel.js',
|
||||
];
|
||||
duplicateGraph.initialPanelGzipBytes = 9;
|
||||
duplicateGraph.initialPanelOnlyFiles = [
|
||||
'houseplan-assets/card-copy-HASH.js', 'houseplan-panel.js',
|
||||
];
|
||||
duplicateGraph.initialPanelOnlyGzipBytes = 9;
|
||||
assert.throws(
|
||||
() => assertBundleManifest(duplicateGraph),
|
||||
/initial panel graph must contain both stable entries|not a subset/,
|
||||
);
|
||||
});
|
||||
|
||||
test('#486 sync payload orders dependencies before both stable entries', () => {
|
||||
assert.deepEqual(orderedBundlePayload(minimalTwoEntryManifest()), [
|
||||
'houseplan-assets/card-HASH.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
]);
|
||||
});
|
||||
|
||||
test('#486 both stable entries install a visible stale-load fallback', async () => {
|
||||
const bundle = minimalTwoEntryBundle();
|
||||
bundle['houseplan-card.js'].code =
|
||||
'export{x}from"./houseplan-assets/card-HASH.js";';
|
||||
bundle['houseplan-panel.js'].code =
|
||||
'import"./houseplan-assets/card-HASH.js";customElements.define("real-panel",class{});';
|
||||
entryFallbackPlugin().generateBundle({}, bundle);
|
||||
|
||||
const card = bundle['houseplan-card.js'].code;
|
||||
const panel = bundle['houseplan-panel.js'].code;
|
||||
assert.match(card, /try\{await import\("\.\/houseplan-assets\/card-HASH\.js"\)\}catch/);
|
||||
assert.match(card, /customElements\.define\("houseplan-card"/);
|
||||
assert.doesNotMatch(card, /export\{/);
|
||||
assert.match(panel, /try\{await import\("\.\/houseplan-card\.js"\)\}catch/);
|
||||
assert.match(panel, /customElements\.define\("houseplan-panel"/);
|
||||
assert.doesNotMatch(panel, /(?:^|;)import["']/);
|
||||
assert.deepEqual(bundle['houseplan-panel.js'].imports, ['houseplan-card.js']);
|
||||
|
||||
const priorCustomElements = Object.getOwnPropertyDescriptor(globalThis, 'customElements');
|
||||
const priorHTMLElement = Object.getOwnPropertyDescriptor(globalThis, 'HTMLElement');
|
||||
const priorNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator');
|
||||
const priorConsoleError = console.error;
|
||||
const registry = new Map();
|
||||
Object.defineProperty(globalThis, 'customElements', {
|
||||
configurable: true,
|
||||
value: { get: (name) => registry.get(name), define: (name, ctor) => registry.set(name, ctor) },
|
||||
});
|
||||
Object.defineProperty(globalThis, 'HTMLElement', {
|
||||
configurable: true,
|
||||
value: class { constructor() { this.style = {}; } },
|
||||
});
|
||||
Object.defineProperty(globalThis, 'navigator', {
|
||||
configurable: true, value: { language: 'en' },
|
||||
});
|
||||
console.error = () => {};
|
||||
try {
|
||||
await import(`data:text/javascript;base64,${Buffer.from(card).toString('base64')}`);
|
||||
assert.ok(registry.has('houseplan-card'));
|
||||
const staleCard = new (registry.get('houseplan-card'))();
|
||||
staleCard.connectedCallback();
|
||||
assert.match(staleCard.textContent, /reload the page/);
|
||||
await import(`data:text/javascript;base64,${Buffer.from(panel).toString('base64')}`);
|
||||
assert.ok(registry.has('houseplan-panel'));
|
||||
const stalePanel = new (registry.get('houseplan-panel'))();
|
||||
stalePanel.connectedCallback();
|
||||
assert.match(stalePanel.textContent, /reload the page/);
|
||||
} finally {
|
||||
console.error = priorConsoleError;
|
||||
for (const [name, descriptor] of [
|
||||
['customElements', priorCustomElements],
|
||||
['HTMLElement', priorHTMLElement],
|
||||
['navigator', priorNavigator],
|
||||
]) {
|
||||
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
|
||||
else delete globalThis[name];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('#423 support form copy belongs only to the lazy editor graph', () => {
|
||||
@@ -215,12 +409,30 @@ test('bundle tree verification fails for a missing or tampered manifest asset',
|
||||
for (const root of [source, target]) {
|
||||
mkdirSync(join(root, 'houseplan-assets'), { recursive: true });
|
||||
writeFileSync(join(root, 'houseplan-card.js'), 'entry');
|
||||
writeFileSync(join(root, 'houseplan-panel.js'), 'panel');
|
||||
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))),
|
||||
const files = [
|
||||
'houseplan-card.js', 'houseplan-panel.js', 'houseplan-assets/editor-hash.js',
|
||||
].map((path) => ({
|
||||
path,
|
||||
sha256: sha256Bytes(readFileSync(join(root, path))),
|
||||
gzipBytes: path === 'houseplan-panel.js' ? 2 : 3,
|
||||
isEntry: path === 'houseplan-card.js' || path === 'houseplan-panel.js',
|
||||
}));
|
||||
writeFileSync(join(root, 'houseplan-assets.json'), `${JSON.stringify({
|
||||
schema: 1, fingerprint: 'fixture', entry: 'houseplan-card.js', files,
|
||||
schema: 1,
|
||||
fingerprint: 'fixture',
|
||||
entry: 'houseplan-card.js',
|
||||
panelEntry: 'houseplan-panel.js',
|
||||
initialViewFiles: ['houseplan-assets/editor-hash.js', 'houseplan-card.js'],
|
||||
initialViewGzipBytes: 6,
|
||||
initialPanelFiles: [
|
||||
'houseplan-assets/editor-hash.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
],
|
||||
initialPanelGzipBytes: 8,
|
||||
initialPanelOnlyFiles: ['houseplan-panel.js'],
|
||||
initialPanelOnlyGzipBytes: 2,
|
||||
files,
|
||||
})}\n`);
|
||||
}
|
||||
assert.doesNotThrow(() => compareBundleTrees(source, target));
|
||||
@@ -254,6 +466,32 @@ test('entry facade fails loudly when the main chunk is unavailable (#353 AC3a)',
|
||||
);
|
||||
});
|
||||
|
||||
test('#486 panel entry routes through the card facade and fails loudly too', () => {
|
||||
const panel = readFileSync(new URL('../dist/houseplan-panel.js', import.meta.url), 'utf8');
|
||||
assert.match(
|
||||
panel,
|
||||
/^globalThis\.__HOUSEPLAN_BUILD_FINGERPRINT__="[0-9a-f]{64}";/,
|
||||
);
|
||||
assert.match(panel, /try\{await import\("\.\/houseplan-card\.js"\)\}catch\(/);
|
||||
assert.match(panel, /customElements\.define\("houseplan-panel",/);
|
||||
assert.match(panel, /reload the page/);
|
||||
assert.doesNotMatch(
|
||||
panel,
|
||||
/(?:^|;)import[\s{"']/m,
|
||||
'the panel must not keep a static edge that can abort before its fallback runs',
|
||||
);
|
||||
|
||||
const manifest = JSON.parse(
|
||||
readFileSync(new URL('../dist/houseplan-assets.json', import.meta.url), 'utf8'),
|
||||
);
|
||||
assert.ok(!manifest.initialViewFiles.includes(manifest.panelEntry));
|
||||
assert.ok(
|
||||
manifest.initialViewFiles.every((path) => manifest.initialPanelFiles.includes(path)),
|
||||
'the panel must reuse the exact card graph',
|
||||
);
|
||||
assert.ok(manifest.initialPanelOnlyGzipBytes <= INITIAL_PANEL_ONLY_GZIP_BUDGET);
|
||||
});
|
||||
|
||||
test('bundle tree verification rejects orphan chunks (#353 AC4)', async () => {
|
||||
const { mkdtempSync, writeFileSync, mkdirSync } = await import('node:fs');
|
||||
const { tmpdir } = await import('node:os');
|
||||
@@ -262,24 +500,51 @@ test('bundle tree verification rejects orphan chunks (#353 AC4)', async () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'hp-tree-'));
|
||||
mkdirSync(join(root, 'houseplan-assets'));
|
||||
const entryCode = 'try{await import("./houseplan-assets/main-abc.js")}catch(e){}';
|
||||
const panelCode = 'try{await import("./houseplan-card.js")}catch(e){}';
|
||||
const chunkCode = 'export const x = 1;';
|
||||
writeFileSync(join(root, 'houseplan-card.js'), entryCode);
|
||||
writeFileSync(join(root, 'houseplan-panel.js'), panelCode);
|
||||
writeFileSync(join(root, 'houseplan-assets', 'main-abc.js'), chunkCode);
|
||||
writeFileSync(join(root, 'houseplan-assets.json'), JSON.stringify({
|
||||
schema: 1,
|
||||
fingerprint: 'f'.repeat(64),
|
||||
entry: 'houseplan-card.js',
|
||||
panelEntry: 'houseplan-panel.js',
|
||||
initialViewFiles: ['houseplan-assets/main-abc.js', 'houseplan-card.js'],
|
||||
initialViewGzipBytes: 12,
|
||||
initialPanelFiles: [
|
||||
'houseplan-assets/main-abc.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
],
|
||||
initialPanelGzipBytes: 14,
|
||||
initialPanelOnlyFiles: ['houseplan-panel.js'],
|
||||
initialPanelOnlyGzipBytes: 2,
|
||||
files: [
|
||||
{ path: 'houseplan-card.js', sha256: sha256Bytes(Buffer.from(entryCode)) },
|
||||
{ path: 'houseplan-assets/main-abc.js', sha256: sha256Bytes(Buffer.from(chunkCode)) },
|
||||
{
|
||||
path: 'houseplan-card.js', sha256: sha256Bytes(Buffer.from(entryCode)),
|
||||
gzipBytes: 5, isEntry: true,
|
||||
},
|
||||
{
|
||||
path: 'houseplan-panel.js', sha256: sha256Bytes(Buffer.from(panelCode)),
|
||||
gzipBytes: 2, isEntry: true,
|
||||
},
|
||||
{
|
||||
path: 'houseplan-assets/main-abc.js', sha256: sha256Bytes(Buffer.from(chunkCode)),
|
||||
gzipBytes: 7, isEntry: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
assert.equal(verifyBundleTree(root).files.length, 2, 'a clean tree verifies');
|
||||
assert.equal(verifyBundleTree(root).files.length, 3, 'a clean tree verifies');
|
||||
writeFileSync(join(root, 'houseplan-assets', 'junk-old.js'), 'stale');
|
||||
assert.throws(
|
||||
() => verifyBundleTree(root),
|
||||
/orphan bundle asset: houseplan-assets\/junk-old\.js/,
|
||||
);
|
||||
rmSync(join(root, 'houseplan-assets', 'junk-old.js'));
|
||||
writeFileSync(join(root, 'houseplan-obsolete.js'), 'stale root entry');
|
||||
assert.throws(
|
||||
() => verifyBundleTree(root),
|
||||
/orphan bundle asset: houseplan-obsolete\.js/,
|
||||
);
|
||||
});
|
||||
|
||||
// #367. Рекалибровка после того, как запас ушёл с 26 КБ до 8.3 КБ за сутки.
|
||||
@@ -448,7 +713,8 @@ const runBudgetCli = (initialViewGzipBytes) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'houseplan-budget-cli-'));
|
||||
try {
|
||||
mkdirSync(join(dir, 'dist'));
|
||||
writeFileSync(join(dir, 'dist/initial.js'), 'view graph without support copy');
|
||||
writeFileSync(join(dir, 'dist/houseplan-card.js'), 'view graph without support copy');
|
||||
writeFileSync(join(dir, 'dist/houseplan-panel.js'), 'panel shell');
|
||||
writeFileSync(join(dir, 'dist/editor.js'), SUPPORT_LAZY_MARKERS.join('\n'));
|
||||
writeFileSync(join(dir, 'dist/locale.js'), 'lazy locale dictionary');
|
||||
writeFileSync(join(dir, 'dist/isometric.js'), 'lazy isometric runtime');
|
||||
@@ -456,9 +722,21 @@ const runBudgetCli = (initialViewGzipBytes) => {
|
||||
writeFileSync(join(dir, 'dist/pdf.js'), 'lazy pdf writer');
|
||||
writeFileSync(join(dir, 'dist/houseplan-assets.json'), JSON.stringify({
|
||||
schema: 1,
|
||||
files: [],
|
||||
initialViewFiles: ['initial.js'],
|
||||
fingerprint: 'f'.repeat(64),
|
||||
entry: 'houseplan-card.js',
|
||||
panelEntry: 'houseplan-panel.js',
|
||||
files: [
|
||||
{
|
||||
path: 'houseplan-card.js', gzipBytes: initialViewGzipBytes, isEntry: true,
|
||||
},
|
||||
{ path: 'houseplan-panel.js', gzipBytes: 1, isEntry: true },
|
||||
],
|
||||
initialViewFiles: ['houseplan-card.js'],
|
||||
initialViewGzipBytes,
|
||||
initialPanelFiles: ['houseplan-card.js', 'houseplan-panel.js'],
|
||||
initialPanelGzipBytes: initialViewGzipBytes + 1,
|
||||
initialPanelOnlyFiles: ['houseplan-panel.js'],
|
||||
initialPanelOnlyGzipBytes: 1,
|
||||
lazyEditorFiles: ['editor.js'],
|
||||
lazyEditorGzipBytes: 1000,
|
||||
lazyLocaleFiles: ['locale.js'],
|
||||
|
||||
@@ -48,27 +48,58 @@ test('bundle freshness rejects a missing or stale fingerprint', async () => {
|
||||
test('bundle freshness verifies every manifest-listed demo asset', async () => {
|
||||
const root = fixtureRoot();
|
||||
try {
|
||||
writeFileSync(
|
||||
resolve(root, 'src/houseplan-panel.ts'),
|
||||
"import './card'; export const panel = true;\n",
|
||||
'utf8',
|
||||
);
|
||||
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-panel.js', 'panel'],
|
||||
['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({
|
||||
const manifest = {
|
||||
schema: 1,
|
||||
fingerprint: expected,
|
||||
files: files.map(([path, contents]) => ({
|
||||
entry: 'houseplan-card.js',
|
||||
panelEntry: 'houseplan-panel.js',
|
||||
initialViewFiles: ['houseplan-assets/editor-HASH.js', 'houseplan-card.js'],
|
||||
initialViewGzipBytes: 11,
|
||||
initialPanelFiles: [
|
||||
'houseplan-assets/editor-HASH.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
],
|
||||
initialPanelGzipBytes: 16,
|
||||
initialPanelOnlyFiles: ['houseplan-panel.js'],
|
||||
initialPanelOnlyGzipBytes: 5,
|
||||
files: files.map(([path, contents], index) => ({
|
||||
path,
|
||||
sha256: createHash('sha256').update(contents).digest('hex'),
|
||||
gzipBytes: [6, 5, 5][index],
|
||||
isEntry: path === 'houseplan-card.js' || path === 'houseplan-panel.js',
|
||||
})),
|
||||
}), 'utf8');
|
||||
};
|
||||
writeFileSync(resolve(assets, 'houseplan-assets.json'), JSON.stringify(manifest), '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/);
|
||||
|
||||
writeFileSync(resolve(assets, 'houseplan-assets/editor-HASH.js'), 'editor', 'utf8');
|
||||
rmSync(resolve(assets, 'houseplan-panel.js'));
|
||||
await assert.rejects(
|
||||
assertFreshDemoBundle(page, root),
|
||||
/manifest asset is missing: houseplan-panel\.js/,
|
||||
);
|
||||
|
||||
writeFileSync(resolve(assets, 'houseplan-panel.js'), 'panel', 'utf8');
|
||||
delete manifest.panelEntry;
|
||||
writeFileSync(resolve(assets, 'houseplan-assets.json'), JSON.stringify(manifest), 'utf8');
|
||||
await assert.rejects(assertFreshDemoBundle(page, root), /expected entries/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import test from 'node:test';
|
||||
|
||||
const sha256 = (contents) => createHash('sha256').update(contents).digest('hex');
|
||||
|
||||
const writeCurrentBundle = (root) => {
|
||||
const contents = new Map([
|
||||
['houseplan-card.js', 'card facade'],
|
||||
['houseplan-panel.js', 'panel shell'],
|
||||
['houseplan-assets/card-HASH.js', 'shared card implementation'],
|
||||
]);
|
||||
for (const [name, value] of contents) {
|
||||
const path = join(root, 'dist', name);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, value);
|
||||
}
|
||||
const files = [...contents].map(([path, value]) => ({
|
||||
path,
|
||||
sha256: sha256(value),
|
||||
rawBytes: Buffer.byteLength(value),
|
||||
gzipBytes: path === 'houseplan-card.js' ? 5 : path === 'houseplan-panel.js' ? 3 : 7,
|
||||
isEntry: path === 'houseplan-card.js' || path === 'houseplan-panel.js',
|
||||
imports: path === 'houseplan-panel.js'
|
||||
? ['houseplan-card.js']
|
||||
: path === 'houseplan-card.js' ? ['houseplan-assets/card-HASH.js'] : [],
|
||||
dynamicImports: [],
|
||||
}));
|
||||
const manifest = {
|
||||
schema: 1,
|
||||
fingerprint: 'f'.repeat(64),
|
||||
entry: 'houseplan-card.js',
|
||||
panelEntry: 'houseplan-panel.js',
|
||||
initialViewFiles: ['houseplan-assets/card-HASH.js', 'houseplan-card.js'],
|
||||
initialViewGzipBytes: 12,
|
||||
initialPanelFiles: [
|
||||
'houseplan-assets/card-HASH.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
],
|
||||
initialPanelGzipBytes: 15,
|
||||
initialPanelOnlyFiles: ['houseplan-panel.js'],
|
||||
initialPanelOnlyGzipBytes: 3,
|
||||
files,
|
||||
};
|
||||
writeFileSync(join(root, 'dist/houseplan-assets.json'), `${JSON.stringify(manifest)}\n`);
|
||||
return manifest;
|
||||
};
|
||||
|
||||
const writeLegacyTarget = (root, target) => {
|
||||
const targetRoot = join(root, target);
|
||||
mkdirSync(join(targetRoot, 'houseplan-assets'), { recursive: true });
|
||||
writeFileSync(join(targetRoot, 'houseplan-card.js'), 'old card');
|
||||
writeFileSync(join(targetRoot, 'houseplan-legacy.js'), 'old root entry');
|
||||
writeFileSync(join(targetRoot, 'houseplan-assets/old-HASH.js'), 'old chunk');
|
||||
writeFileSync(join(targetRoot, 'houseplan-assets.json'), JSON.stringify({
|
||||
schema: 1,
|
||||
fingerprint: 'legacy',
|
||||
entry: 'houseplan-card.js',
|
||||
files: [
|
||||
{ path: 'houseplan-card.js' },
|
||||
{ path: 'houseplan-legacy.js' },
|
||||
{ path: 'houseplan-assets/old-HASH.js' },
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
test('#486 bundle sync materializes both entries and removes the legacy inventory', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'houseplan-bundle-sync-'));
|
||||
try {
|
||||
mkdirSync(join(root, 'scripts'), { recursive: true });
|
||||
for (const name of ['bundle-sync.mjs', 'bundle-tree.mjs']) {
|
||||
writeFileSync(
|
||||
join(root, 'scripts', name),
|
||||
readFileSync(new URL(`../scripts/${name}`, import.meta.url)),
|
||||
);
|
||||
}
|
||||
const manifest = writeCurrentBundle(root);
|
||||
for (const target of [
|
||||
'custom_components/houseplan/frontend', 'demo/srv/assets',
|
||||
]) writeLegacyTarget(root, target);
|
||||
|
||||
const run = spawnSync(process.execPath, ['scripts/bundle-sync.mjs'], {
|
||||
cwd: root, encoding: 'utf8',
|
||||
});
|
||||
assert.equal(run.status, 0, `${run.stdout}\n${run.stderr}`);
|
||||
for (const target of [
|
||||
'custom_components/houseplan/frontend', 'demo/srv/assets',
|
||||
]) {
|
||||
assert.equal(readFileSync(join(root, target, 'houseplan-card.js'), 'utf8'), 'card facade');
|
||||
assert.equal(readFileSync(join(root, target, 'houseplan-panel.js'), 'utf8'), 'panel shell');
|
||||
assert.deepEqual(
|
||||
JSON.parse(readFileSync(join(root, target, 'houseplan-assets.json'), 'utf8')),
|
||||
manifest,
|
||||
);
|
||||
assert.equal(existsSync(join(root, target, 'houseplan-legacy.js')), false);
|
||||
assert.equal(existsSync(join(root, target, 'houseplan-assets/old-HASH.js')), false);
|
||||
}
|
||||
|
||||
writeFileSync(join(root, 'dist/houseplan-orphan.js'), 'unlisted entry');
|
||||
const orphan = spawnSync(process.execPath, ['scripts/bundle-sync.mjs'], {
|
||||
cwd: root, encoding: 'utf8',
|
||||
});
|
||||
assert.notEqual(orphan.status, 0);
|
||||
assert.match(`${orphan.stdout}${orphan.stderr}`, /orphan bundle asset: houseplan-orphan\.js/);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -136,6 +136,32 @@ test('golden matrix has stable unique ids and bounded comparison thresholds', ()
|
||||
}
|
||||
});
|
||||
|
||||
test('issue 486 golden matrix has exactly the three focused panel-host surfaces', () => {
|
||||
const scenarios = GOLDEN_SCENARIOS.filter((scenario) => scenario.panelHost);
|
||||
assert.deepEqual(scenarios.map((scenario) => scenario.id), [
|
||||
'panel-wide-view-light-en',
|
||||
'panel-wide-plan-editor-dark-en',
|
||||
'panel-narrow-read-only-empty-light-en',
|
||||
]);
|
||||
assert.deepEqual(scenarios.map((scenario) => scenario.viewport), [
|
||||
{ width: 1280, height: 800 },
|
||||
{ width: 1280, height: 800 },
|
||||
{ width: 320, height: 720 },
|
||||
]);
|
||||
assert.equal(scenarios.every((scenario) => scenario.capture === 'page'), true);
|
||||
assert.deepEqual(scenarios.map((scenario) => scenario.mode), ['view', 'plan', 'view']);
|
||||
|
||||
const readOnly = scenarios.at(-1);
|
||||
assert.equal(readOnly.emptyPlan, true);
|
||||
assert.equal(readOnly.canWrite, false);
|
||||
assert.equal(readOnly.userIsAdmin, false);
|
||||
assert.equal(readOnly.preloadEditorRuntime, false);
|
||||
const fixture = prepareGoldenFixture(readOnly);
|
||||
assert.deepEqual(fixture.config.spaces, []);
|
||||
assert.deepEqual(fixture.config.markers, []);
|
||||
assert.deepEqual(fixture.layout, {});
|
||||
});
|
||||
|
||||
test('issue 462 has reviewed desktop, narrow-touch and attempted-kiosk notices', () => {
|
||||
const scenarios = GOLDEN_SCENARIOS.filter((scenario) =>
|
||||
scenario.id.startsWith('version-mismatch-'));
|
||||
@@ -415,7 +441,7 @@ test('sun-ray golden requires browser-painted light from a state-only sun entity
|
||||
assert.ok(scenario);
|
||||
const fixture = prepareGoldenFixture(scenario);
|
||||
const space = fixture.config.spaces.find((item) => item.id === scenario.space);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 59);
|
||||
assert.equal(GOLDEN_MATRIX_VERSION, 60);
|
||||
assert.equal(space.settings.sun_rays, true);
|
||||
assert.equal(scenario.northDeg, 90,
|
||||
'the sign-sensitive golden must keep a non-zero north direction');
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const source = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8');
|
||||
|
||||
test('#486 panel shell keeps one full card and forwards HA properties without inventing card config', () => {
|
||||
const panel = source('src/houseplan-panel.ts');
|
||||
assert.match(panel, /import '\.\/houseplan-card';/);
|
||||
assert.match(panel, /if \(this\._card \|\| !this\.shadowRoot\) return;/);
|
||||
assert.match(panel, /card\.setConfig\(\{ type: 'custom:houseplan-card' \}\);/);
|
||||
assert.match(panel, /if \(this\._card\) this\._card\.hass = value;/);
|
||||
assert.match(panel, /public set narrow\(value: boolean\)/);
|
||||
assert.match(panel, /public set route\(value: unknown\)/);
|
||||
assert.match(panel, /public set panel\(value: unknown\)/);
|
||||
assert.doesNotMatch(panel, /kiosk\s*:/, 'HA shell state must not enable card kiosk mode');
|
||||
});
|
||||
|
||||
test('#486 menu and container-owned panel layout have explicit accessibility and overflow contracts', () => {
|
||||
const panel = source('src/houseplan-panel.ts');
|
||||
assert.match(panel, /new CustomEvent\('hass-toggle-menu', \{[\s\S]*bubbles: true,[\s\S]*composed: true/);
|
||||
assert.match(panel, /width: 44px;[\s\S]*height: 44px;/);
|
||||
assert.match(panel, /setAttribute\('aria-label', label\)/);
|
||||
assert.match(panel, /grid-template-rows: auto minmax\(0, 1fr\)/);
|
||||
assert.match(panel, /\.content \{[\s\S]*min-width: 0;[\s\S]*min-height: 0;[\s\S]*overflow: hidden;/);
|
||||
assert.doesNotMatch(panel, /100dvh|100vh|safe-area-inset/,
|
||||
'the shell must use the HA-owned content slot rather than viewport or duplicated safe-area math');
|
||||
});
|
||||
|
||||
test('#486 only the full card advertises a full-width Sections default', () => {
|
||||
const card = source('src/houseplan-card.ts');
|
||||
const spaceCard = source('src/space-card.ts');
|
||||
assert.match(card, /getGridOptions\(\): \{ columns: 'full' \} \{\s*return \{ columns: 'full' \};\s*\}/);
|
||||
assert.match(card, /getCardSize\(\): number/);
|
||||
assert.doesNotMatch(spaceCard, /getGridOptions/);
|
||||
});
|
||||
|
||||
test('#486 read-only empty state is localized in every shipped frontend dictionary', () => {
|
||||
for (const locale of ['en', 'ru', 'de', 'fr']) {
|
||||
const dictionary = JSON.parse(source(`src/i18n/${locale}.json`));
|
||||
assert.equal(typeof dictionary['empty.read_only'], 'string');
|
||||
assert.ok(dictionary['empty.read_only'].trim().length > 0);
|
||||
}
|
||||
const card = source('src/houseplan-card.ts');
|
||||
assert.match(card, /this\._serverStorage && this\._canManageConfiguration/);
|
||||
assert.match(card, /this\._serverStorage \? 'empty\.read_only' : 'empty\.install'/);
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import {
|
||||
mkdirSync, mkdtempSync, rmSync, unlinkSync, writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import test from 'node:test';
|
||||
|
||||
import { verifyHouseplanZip } from '../scripts/verify-houseplan-zip.mjs';
|
||||
|
||||
const sha256 = (contents) => createHash('sha256').update(contents).digest('hex');
|
||||
|
||||
const git = (root, args) => {
|
||||
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
};
|
||||
|
||||
const write = (root, name, contents) => {
|
||||
const path = join(root, name);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, contents);
|
||||
};
|
||||
|
||||
test('#486 HACS zip contains both entries and rejects stale managed roots', () => {
|
||||
const root = mkdtempSync(join(tmpdir(), 'houseplan-zip-panel-'));
|
||||
const component = join(root, 'component');
|
||||
const frontend = join(component, 'frontend');
|
||||
const zip = join(root, 'houseplan.zip');
|
||||
try {
|
||||
const payload = new Map([
|
||||
['houseplan-card.js', 'card entry'],
|
||||
['houseplan-panel.js', 'panel entry'],
|
||||
['houseplan-assets/card-HASH.js', 'shared card'],
|
||||
]);
|
||||
for (const [name, contents] of payload) write(frontend, name, contents);
|
||||
const files = [...payload].map(([path, contents]) => ({
|
||||
path,
|
||||
sha256: sha256(contents),
|
||||
rawBytes: Buffer.byteLength(contents),
|
||||
gzipBytes: path === 'houseplan-card.js' ? 5 : path === 'houseplan-panel.js' ? 3 : 7,
|
||||
isEntry: path === 'houseplan-card.js' || path === 'houseplan-panel.js',
|
||||
imports: path === 'houseplan-panel.js'
|
||||
? ['houseplan-card.js']
|
||||
: path === 'houseplan-card.js' ? ['houseplan-assets/card-HASH.js'] : [],
|
||||
dynamicImports: [],
|
||||
}));
|
||||
write(frontend, 'houseplan-assets.json', `${JSON.stringify({
|
||||
schema: 1,
|
||||
fingerprint: 'f'.repeat(64),
|
||||
entry: 'houseplan-card.js',
|
||||
panelEntry: 'houseplan-panel.js',
|
||||
initialViewFiles: ['houseplan-assets/card-HASH.js', 'houseplan-card.js'],
|
||||
initialViewGzipBytes: 12,
|
||||
initialPanelFiles: [
|
||||
'houseplan-assets/card-HASH.js', 'houseplan-card.js', 'houseplan-panel.js',
|
||||
],
|
||||
initialPanelGzipBytes: 15,
|
||||
initialPanelOnlyFiles: ['houseplan-panel.js'],
|
||||
initialPanelOnlyGzipBytes: 3,
|
||||
files,
|
||||
})}\n`);
|
||||
write(component, 'manifest.json', '{"version":"1.2.3"}\n');
|
||||
|
||||
git(root, ['init', '-q']);
|
||||
git(root, ['config', 'user.name', 'House Plan test']);
|
||||
git(root, ['config', 'user.email', 'test@example.invalid']);
|
||||
git(root, ['config', 'core.autocrlf', 'false']);
|
||||
git(root, ['add', 'component']);
|
||||
git(root, ['commit', '-qm', 'fixture']);
|
||||
git(root, ['archive', '--format=zip', `--output=${zip}`, 'HEAD:component']);
|
||||
assert.deepEqual(verifyHouseplanZip(zip, frontend, '1.2.3'), {
|
||||
version: '1.2.3', files: 3,
|
||||
});
|
||||
|
||||
unlinkSync(join(frontend, 'houseplan-panel.js'));
|
||||
git(root, ['add', '-A', 'component']);
|
||||
git(root, ['commit', '-qm', 'missing panel']);
|
||||
unlinkSync(zip);
|
||||
git(root, ['archive', '--format=zip', `--output=${zip}`, 'HEAD:component']);
|
||||
assert.throws(
|
||||
() => verifyHouseplanZip(zip, frontend, '1.2.3'),
|
||||
/missing frontend\/houseplan-panel\.js/,
|
||||
);
|
||||
|
||||
write(frontend, 'houseplan-panel.js', 'panel entry');
|
||||
write(frontend, 'houseplan-obsolete.js', 'stale root entry');
|
||||
git(root, ['add', '-A', 'component']);
|
||||
git(root, ['commit', '-qm', 'orphan']);
|
||||
unlinkSync(zip);
|
||||
git(root, ['archive', '--format=zip', `--output=${zip}`, 'HEAD:component']);
|
||||
assert.throws(
|
||||
() => verifyHouseplanZip(zip, frontend, '1.2.3'),
|
||||
/orphan bundle asset: frontend\/houseplan-obsolete\.js/,
|
||||
);
|
||||
|
||||
unlinkSync(join(frontend, 'houseplan-obsolete.js'));
|
||||
write(frontend, 'houseplan-panel.js', 'tampered panel entry');
|
||||
git(root, ['add', '-A', 'component']);
|
||||
git(root, ['commit', '-qm', 'tamper']);
|
||||
unlinkSync(zip);
|
||||
git(root, ['archive', '--format=zip', `--output=${zip}`, 'HEAD:component']);
|
||||
assert.throws(
|
||||
() => verifyHouseplanZip(zip, frontend, '1.2.3'),
|
||||
/frontend hash mismatch: houseplan-panel\.js/,
|
||||
);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
@@ -56,6 +56,35 @@ test('every mutant explains itself', () => {
|
||||
assert.ok(MUTANTS.length >= 6, 'стартовый набор — шесть мутантов по дырам из #85');
|
||||
});
|
||||
|
||||
test('#486 panel registration and cleanup protections have mutation witnesses', () => {
|
||||
const ids = new Set(MUTANTS.map(({ id }) => id));
|
||||
for (const id of [
|
||||
'panel-registers-wrong-route',
|
||||
'panel-registers-card-static-url',
|
||||
'panel-module-url-loses-version',
|
||||
'panel-becomes-admin-only',
|
||||
'panel-bypasses-panel-custom-api',
|
||||
'panel-static-legacy-boolean-claims-both-urls',
|
||||
'panel-cleanup-drops-generation-guard',
|
||||
'panel-cleanup-drops-identity-guard',
|
||||
'panel-cleanup-uses-new-remove-keyword',
|
||||
'panel-accepts-unverifiable-ownership',
|
||||
]) {
|
||||
assert.ok(ids.has(id), `${id}: отсутствует защитный свидетель #486`);
|
||||
}
|
||||
});
|
||||
|
||||
test('#486 bundle topology and both stale entries have mutation witnesses', () => {
|
||||
const ids = new Set(MUTANTS.map(({ id }) => id));
|
||||
for (const id of [
|
||||
'entry-fallback-rewrite-skipped',
|
||||
'panel-entry-fallback-rewrite-skipped',
|
||||
'panel-entry-bypasses-card-graph',
|
||||
]) {
|
||||
assert.ok(ids.has(id), `${id}: отсутствует защитный свидетель bundle-контракта #486`);
|
||||
}
|
||||
});
|
||||
|
||||
test('applyPatches rewrites the anchor and refuses a stale one', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'hp-mg-'));
|
||||
try {
|
||||
|
||||
@@ -61,6 +61,9 @@ test('full performance is isolated to stable, scheduled and manual entry points'
|
||||
const release = readWorkflow('release.yml');
|
||||
assert.ok(release.includes('if: ${{ !github.event.release.prerelease }}'));
|
||||
assert.ok(release.includes('--workflow=performance.yml --label="Полные бенчмарки производительности"'));
|
||||
assert.ok(release.includes('test -s dist/houseplan-panel.js'));
|
||||
assert.ok(release.includes('files: dist/houseplan-card.js'),
|
||||
'the standalone release asset remains card-only; the panel ships through HACS zip');
|
||||
});
|
||||
|
||||
test('#160 Stage 3 dense fixture extends rather than mutates the historical witness', () => {
|
||||
|
||||
@@ -226,6 +226,7 @@ test('manual publish workflow is draft-first, exact-SHA gated and self-contained
|
||||
'node scripts/release-gate.mjs',
|
||||
'--draft --prerelease',
|
||||
"'houseplan-card.js', 'houseplan.zip'",
|
||||
'test -s dist/houseplan-panel.js',
|
||||
'--draft=false --prerelease',
|
||||
'Verify HACS prerelease discovery order',
|
||||
'group: publish-prerelease-${{ inputs.tag }}',
|
||||
|
||||
@@ -29,9 +29,20 @@ test('the tree carries exactly one *manifest.json: HACS rejects a repository wit
|
||||
test('копия бандла для стенда не коммитится (#255)', () => {
|
||||
const tracked = execFileSync('git', ['ls-files', '-z', 'demo/srv/assets'], { cwd: ROOT })
|
||||
.toString('utf8').split('\0').filter(Boolean);
|
||||
assert.ok(!tracked.includes('demo/srv/assets/houseplan-card.js'),
|
||||
'demo/srv/assets/houseplan-card.js снова в индексе: соберите его `npm run bundle:sync`, '
|
||||
+ 'а из коммита уберите');
|
||||
const ignored = execFileSync('git', [
|
||||
'check-ignore',
|
||||
'demo/srv/assets/houseplan-card.js',
|
||||
'demo/srv/assets/houseplan-panel.js',
|
||||
], { cwd: ROOT }).toString('utf8').split(/\r?\n/).filter(Boolean);
|
||||
assert.deepEqual(ignored, [
|
||||
'demo/srv/assets/houseplan-card.js',
|
||||
'demo/srv/assets/houseplan-panel.js',
|
||||
], 'оба стабильных demo entry должны оставаться локальными build-артефактами');
|
||||
for (const entry of ['houseplan-card.js', 'houseplan-panel.js']) {
|
||||
assert.ok(!tracked.includes(`demo/srv/assets/${entry}`),
|
||||
`demo/srv/assets/${entry} снова в индексе: соберите его \`npm run bundle:sync\`, `
|
||||
+ 'а из коммита уберите');
|
||||
}
|
||||
// Остальное содержимое стенда (иконки, страницы) коммитится и должно остаться.
|
||||
assert.ok(tracked.length > 0, 'каталог стенда пуст: проверьте, что убрали только бандл');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user