mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
@@ -204,7 +204,10 @@ try {
|
|||||||
});
|
});
|
||||||
const cacheSnapshot = (card) => ({
|
const cacheSnapshot = (card) => ({
|
||||||
...(card.localName === 'houseplan-space-card' ? {
|
...(card.localName === 'houseplan-space-card' ? {
|
||||||
glowClip: card._glowRuntimeState.clipCache?.size ?? 0,
|
// #380: the previous stable predates the shared static-card Glow
|
||||||
|
// runtime. Its default profile still has a valid empty cache, and
|
||||||
|
// the opt-in Glow profile is explicitly bootstrap-compared below.
|
||||||
|
glowClip: card._glowRuntimeState?.clipCache?.size ?? 0,
|
||||||
} : {
|
} : {
|
||||||
cleanFloor: card._cleanFloorCache?.size ?? 0,
|
cleanFloor: card._cleanFloorCache?.size ?? 0,
|
||||||
glowClip: card._glowClipCache?.size ?? 0,
|
glowClip: card._glowClipCache?.size ?? 0,
|
||||||
|
|||||||
@@ -346,7 +346,10 @@ try {
|
|||||||
target: null,
|
target: null,
|
||||||
};
|
};
|
||||||
card._rszEdgeDown(quietEvent, room.id, 1);
|
card._rszEdgeDown(quietEvent, room.id, 1);
|
||||||
const plan = card._resize?.plan;
|
// #380: the previous stable owns the active session directly; current
|
||||||
|
// candidates own it through ResizeController. The explicit contract
|
||||||
|
// above requires one of these shapes before measurements begin.
|
||||||
|
const plan = card._resize?.plan || card._rszDrag?.plan;
|
||||||
if (!plan) throw new Error('large-house resize plan was not created');
|
if (!plan) throw new Error('large-house resize plan was not created');
|
||||||
const target = [
|
const target = [
|
||||||
plan.a[0] + plan.n[0] * card._gridPitch,
|
plan.a[0] + plan.n[0] * card._gridPitch,
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* Prepare editor internals for demo harnesses across comparison baselines.
|
||||||
|
*
|
||||||
|
* Since #337 current builds keep the editor in a lazy runtime and expose an
|
||||||
|
* explicit preload method. Older stable builds are monolithic: absence of the
|
||||||
|
* method means there is nothing to preload, not that the baseline is broken.
|
||||||
|
* A present method remains fail-closed through its result or exception.
|
||||||
|
*/
|
||||||
|
export async function ensureHarnessEditorRuntime(
|
||||||
|
card = globalThis.window?.__card,
|
||||||
|
) {
|
||||||
|
const ensure = card?._ensureEditorRuntime;
|
||||||
|
if (typeof ensure !== 'function') return true;
|
||||||
|
return Boolean(await ensure.call(card));
|
||||||
|
}
|
||||||
@@ -34,7 +34,6 @@ export const LARGE_HOUSE_CARD_CONTRACT = Object.freeze({
|
|||||||
'_loadOk',
|
'_loadOk',
|
||||||
'_model',
|
'_model',
|
||||||
'_path',
|
'_path',
|
||||||
'_resize',
|
|
||||||
'_serverCfg',
|
'_serverCfg',
|
||||||
'_settingsDialog',
|
'_settingsDialog',
|
||||||
'_tool',
|
'_tool',
|
||||||
@@ -46,6 +45,13 @@ export const LARGE_HOUSE_CARD_CONTRACT = Object.freeze({
|
|||||||
'_onLabsSnapshot', '_planSnapGeometryCache', '_roomDialog', '_setProjection',
|
'_onLabsSnapshot', '_planSnapGeometryCache', '_roomDialog', '_setProjection',
|
||||||
'_wallFaceBatch', '_wallFaceGraphCache',
|
'_wallFaceBatch', '_wallFaceGraphCache',
|
||||||
]),
|
]),
|
||||||
|
// #380: v1.68.1 owns the same resize session directly on the card; newer
|
||||||
|
// bundles moved it into ResizeController. A comparison target must expose
|
||||||
|
// one of the two explicit shapes; the current member retains its object
|
||||||
|
// type check and is also verified against current production source.
|
||||||
|
fieldAlternatives: Object.freeze([
|
||||||
|
Object.freeze({ current: '_resize', legacy: '_rszDrag' }),
|
||||||
|
]),
|
||||||
fieldTypes: Object.freeze({
|
fieldTypes: Object.freeze({
|
||||||
_booting: 'boolean',
|
_booting: 'boolean',
|
||||||
_cleanFloorCache: 'map',
|
_cleanFloorCache: 'map',
|
||||||
@@ -115,11 +121,19 @@ export function assertCardContract(card, contract) {
|
|||||||
.map((name) => `${name}()`);
|
.map((name) => `${name}()`);
|
||||||
const missingFields = contract.fields
|
const missingFields = contract.fields
|
||||||
.filter((name) => !(name in card) || card[name] === undefined);
|
.filter((name) => !(name in card) || card[name] === undefined);
|
||||||
const invalidFields = [...contract.fields, ...(contract.optionalFields || [])]
|
const missingAlternatives = (contract.fieldAlternatives || [])
|
||||||
|
.filter((choice) => !Object.values(choice)
|
||||||
|
.some((name) => name in card && card[name] !== undefined))
|
||||||
|
.map((choice) => Object.values(choice).join('|'));
|
||||||
|
const alternativeFields = (contract.fieldAlternatives || [])
|
||||||
|
.flatMap((choice) => Object.values(choice));
|
||||||
|
const invalidFields = [
|
||||||
|
...contract.fields, ...(contract.optionalFields || []), ...alternativeFields,
|
||||||
|
]
|
||||||
.filter((name) => name in card && contract.fieldTypes?.[name]
|
.filter((name) => name in card && contract.fieldTypes?.[name]
|
||||||
&& !matches(card[name], contract.fieldTypes[name]))
|
&& !matches(card[name], contract.fieldTypes[name]))
|
||||||
.map((name) => `${name}:${contract.fieldTypes[name]}`);
|
.map((name) => `${name}:${contract.fieldTypes[name]}`);
|
||||||
const missing = [...missingMethods, ...missingFields];
|
const missing = [...missingMethods, ...missingFields, ...missingAlternatives];
|
||||||
if (missing.length || invalidFields.length) {
|
if (missing.length || invalidFields.length) {
|
||||||
const details = [
|
const details = [
|
||||||
missing.length ? `missing private API: ${missing.join(', ')}` : '',
|
missing.length ? `missing private API: ${missing.join(', ')}` : '',
|
||||||
|
|||||||
+5
-1
@@ -2,6 +2,7 @@
|
|||||||
// via request interception (no HTTP server needed). Usage: const {page,browser}=await launch();
|
// via request interception (no HTTP server needed). Usage: const {page,browser}=await launch();
|
||||||
import { chromium } from 'playwright';
|
import { chromium } from 'playwright';
|
||||||
import { assertFreshDemoBundleUnlessAllowed } from './bundle-freshness.mjs';
|
import { assertFreshDemoBundleUnlessAllowed } from './bundle-freshness.mjs';
|
||||||
|
import { ensureHarnessEditorRuntime } from './editor-runtime-compat.mjs';
|
||||||
import { readFileSync, existsSync } from 'node:fs';
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
@@ -88,8 +89,11 @@ async function launchInternal(
|
|||||||
// Existing product smokes exercise editor internals directly. They preload
|
// Existing product smokes exercise editor internals directly. They preload
|
||||||
// the new #337 runtime without changing mode; the dedicated network smoke
|
// the new #337 runtime without changing mode; the dedicated network smoke
|
||||||
// uses launchColdView() and proves the real cold-View boundary separately.
|
// uses launchColdView() and proves the real cold-View boundary separately.
|
||||||
|
// Comparative performance runs also launch the previous stable through this
|
||||||
|
// file. A pre-#337 monolithic card has no preload method and is already
|
||||||
|
// ready; a current card still has to return truthy (#380).
|
||||||
if (preloadEditorRuntime) {
|
if (preloadEditorRuntime) {
|
||||||
const ready = await page.evaluate(() => window.__card._ensureEditorRuntime());
|
const ready = await page.evaluate(ensureHarnessEditorRuntime);
|
||||||
if (!ready) throw new Error('editor runtime did not preload for browser smoke');
|
if (!ready) throw new Error('editor runtime did not preload for browser smoke');
|
||||||
}
|
}
|
||||||
return { page, browser };
|
return { page, browser };
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { ensureHarnessEditorRuntime } from '../demo/editor-runtime-compat.mjs';
|
||||||
|
|
||||||
|
test('a pre-lazy-editor baseline is already ready for the harness (#380)', async () => {
|
||||||
|
assert.equal(await ensureHarnessEditorRuntime({}), true);
|
||||||
|
assert.equal(await ensureHarnessEditorRuntime(null), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a lazy editor runtime is called with its card and must succeed (#380)', async () => {
|
||||||
|
let owner = null;
|
||||||
|
const card = {
|
||||||
|
async _ensureEditorRuntime() {
|
||||||
|
owner = this;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
assert.equal(await ensureHarnessEditorRuntime(card), true);
|
||||||
|
assert.equal(owner, card);
|
||||||
|
|
||||||
|
assert.equal(await ensureHarnessEditorRuntime({
|
||||||
|
async _ensureEditorRuntime() { return false; },
|
||||||
|
}), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a broken lazy preload remains a hard harness failure (#380)', async () => {
|
||||||
|
await assert.rejects(
|
||||||
|
ensureHarnessEditorRuntime({
|
||||||
|
async _ensureEditorRuntime() { throw new Error('chunk failed'); },
|
||||||
|
}),
|
||||||
|
/chunk failed/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -21,6 +21,12 @@ const directCardMembers = (relativePath) => {
|
|||||||
|
|
||||||
const declaredMembers = (contract) => new Set([
|
const declaredMembers = (contract) => new Set([
|
||||||
...contract.methods, ...contract.fields, ...(contract.optionalFields || []),
|
...contract.methods, ...contract.fields, ...(contract.optionalFields || []),
|
||||||
|
...(contract.fieldAlternatives || []).flatMap((choice) => Object.values(choice)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentProductionMembers = (contract) => new Set([
|
||||||
|
...contract.methods, ...contract.fields, ...(contract.optionalFields || []),
|
||||||
|
...(contract.fieldAlternatives || []).map((choice) => choice.current),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
test('large-house benchmark declares every private card member it consumes', () => {
|
test('large-house benchmark declares every private card member it consumes', () => {
|
||||||
@@ -42,10 +48,18 @@ test('Glow benchmark declares every private card member it consumes', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('static-card cache diagnostics accept a pre-Glow stable baseline (#380)', () => {
|
||||||
|
const source = readFileSync(
|
||||||
|
new URL('../demo/benchmark_glow.mjs', import.meta.url), 'utf8',
|
||||||
|
);
|
||||||
|
assert.match(source, /card\._glowRuntimeState\?\.clipCache\?\.size \?\? 0/);
|
||||||
|
assert.doesNotMatch(source, /card\._glowRuntimeState\.clipCache/);
|
||||||
|
});
|
||||||
|
|
||||||
test('performance contracts reference real production members', () => {
|
test('performance contracts reference real production members', () => {
|
||||||
const source = readHouseplanProductionSource();
|
const source = readHouseplanProductionSource();
|
||||||
for (const contract of [LARGE_HOUSE_CARD_CONTRACT, GLOW_CARD_CONTRACT]) {
|
for (const contract of [LARGE_HOUSE_CARD_CONTRACT, GLOW_CARD_CONTRACT]) {
|
||||||
for (const name of declaredMembers(contract)) {
|
for (const name of currentProductionMembers(contract)) {
|
||||||
assert.match(source, new RegExp(`\\b(?:private\\s+(?:declare\\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}`);
|
`${contract.label} declares missing production member ${name}`);
|
||||||
}
|
}
|
||||||
@@ -57,6 +71,44 @@ test('performance contracts reference real production members', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('large-house contract accepts only an explicit current or stable resize owner', () => {
|
||||||
|
const methods = Object.fromEntries(
|
||||||
|
LARGE_HOUSE_CARD_CONTRACT.methods.map((name) => [name, () => undefined]),
|
||||||
|
);
|
||||||
|
const fields = Object.fromEntries(
|
||||||
|
LARGE_HOUSE_CARD_CONTRACT.fields.map((name) => [name, null]),
|
||||||
|
);
|
||||||
|
Object.assign(fields, {
|
||||||
|
_booting: false,
|
||||||
|
_cleanFloorCache: new Map(),
|
||||||
|
_devices: [],
|
||||||
|
_glowClipCache: new Map(),
|
||||||
|
_gridPitch: 1,
|
||||||
|
_loadOk: true,
|
||||||
|
_model: [],
|
||||||
|
_path: [],
|
||||||
|
_serverCfg: {},
|
||||||
|
_tool: 'view',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => assertCardContract(
|
||||||
|
{ ...methods, ...fields, _resize: {} }, LARGE_HOUSE_CARD_CONTRACT,
|
||||||
|
));
|
||||||
|
assert.doesNotThrow(() => assertCardContract(
|
||||||
|
{ ...methods, ...fields, _rszDrag: null }, LARGE_HOUSE_CARD_CONTRACT,
|
||||||
|
));
|
||||||
|
assert.throws(
|
||||||
|
() => assertCardContract(
|
||||||
|
{ ...methods, ...fields, _resize: false }, LARGE_HOUSE_CARD_CONTRACT,
|
||||||
|
),
|
||||||
|
/invalid private API types: _resize:object/,
|
||||||
|
);
|
||||||
|
assert.throws(
|
||||||
|
() => assertCardContract({ ...methods, ...fields }, LARGE_HOUSE_CARD_CONTRACT),
|
||||||
|
/missing private API: _resize\|_rszDrag/,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('contract accepts recent optional fields only when their runtime type is valid', () => {
|
test('contract accepts recent optional fields only when their runtime type is valid', () => {
|
||||||
const base = {
|
const base = {
|
||||||
_cleanFloorCache: new Map(), _glowClipCache: new Map(),
|
_cleanFloorCache: new Map(), _glowClipCache: new Map(),
|
||||||
|
|||||||
Reference in New Issue
Block a user