mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-28 19:01:34 +00:00
- settings.volumetric_view (General settings › Display, third switch) replaces the alpha entry, the header projection toggle and the phone-menu item; one rule for card, sidebar page and kiosk; editors stay Flat; backend accepts only a boolean, support package copies it. - Raised tiles in 2.5D View: no ring, rounded body min(0.275 D, 0.3 h), edge 0.1 D with the lab filters, ×1.12 size also in layout/collision, lift 0.075 D, one floor-shadow layer under all markers (theme × floor table), frames hugging tile and edge (Alert > Focus > Selected > Hover), forced colours without edge and shadow (src/iso-tiles.ts, styles/iso-tiles). - Soft sun wash instead of projected Flat wedges (src/iso-sun.ts): same gates and windows as sun_rays, length from elevation, parallelogram along the sun, tone and streaks by floor lightness, sill line. - Walls take the user's wall colour (top opaque, sides × .77/.68/.60); theme rules for walls/openings/labels removed; furniture uses the Flat stroke rule. - Smokes smoke_iso_tiles/iso_sun/iso_theme_walls/volumetric_setting, 16 new mutants, acceptance scenes STAGE6_ACCEPTANCE_SCENARIOS (golden with their Linux CI baselines), ACCEPTANCE.md side by side. Issue: #649 User-Visible: yes
168 lines
7.1 KiB
JavaScript
168 lines
7.1 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { readFileSync } from 'node:fs';
|
|
import {
|
|
ALPHA_STORAGE_KEY, LABS_FLAGS, hashSpace, resolveLabs, validLabsFlag,
|
|
validLabsRegistry,
|
|
} from '../test-build/labs.js';
|
|
|
|
// #649: the product registry is empty (2.5D graduated to General settings); the
|
|
// mechanism is exercised with a synthetic capability.
|
|
const PROBE = Object.freeze([Object.freeze({ id: 'probe', issue: 1, summary: 'Synthetic capability' })]);
|
|
const resolve = (search = '', hash = '', storage = null, registry = PROBE) =>
|
|
resolveLabs({ search, hash }, storage, registry);
|
|
|
|
test('alpha capability registry is timeless and fails closed when malformed', () => {
|
|
assert.equal(ALPHA_STORAGE_KEY, 'houseplan_card_alpha_v1');
|
|
// #649: no experiment ships behind alpha; 2.5D is settings.volumetric_view.
|
|
assert.deepEqual(LABS_FLAGS, []);
|
|
assert.equal(PROBE.every(validLabsFlag), true);
|
|
assert.equal(PROBE.some((flag) => 'since' in flag || 'expires' in flag), false);
|
|
assert.equal(validLabsRegistry([PROBE[0], { ...PROBE[0] }]), false);
|
|
const malformed = resolve('?hp_alpha=1', '', null, [
|
|
PROBE[0], { ...PROBE[0] },
|
|
]);
|
|
assert.equal(malformed.alpha, false);
|
|
assert.deepEqual(malformed.active, []);
|
|
assert.equal(malformed.persist, undefined);
|
|
|
|
const source = readFileSync(new URL('../src/labs.ts', import.meta.url), 'utf8');
|
|
assert.doesNotMatch(source, /\bsince\b|\bexpires\b|parseVersionCore|compareVersion/);
|
|
});
|
|
|
|
test('hp_alpha applies query before hash and the last recognised value wins', () => {
|
|
assert.equal(resolve('?hp_alpha=1').alpha, true);
|
|
assert.deepEqual(resolve('?hp_alpha=1').active, ['probe']);
|
|
assert.deepEqual(resolveLabs({ search: '?hp_alpha=1', hash: '' }, null).active, [], 'product: nothing to enable');
|
|
assert.equal(resolve('?hp_alpha=0').alpha, false);
|
|
assert.equal(resolve('?hp_alpha=0&hp_alpha=1').alpha, true);
|
|
assert.equal(resolve('?hp_alpha=1&hp_alpha=0').alpha, false);
|
|
assert.equal(resolve('?hp_alpha=0', '#hp_alpha=1').alpha, true);
|
|
assert.equal(resolve('?hp_alpha=1', '#hp_alpha=0').alpha, false);
|
|
assert.equal(resolve('?hp_alpha=0', '#hp_alpha=1&hp_alpha=0&hp_alpha=1').alpha, true);
|
|
assert.equal(resolve('?hp_alpha=1').persist, '1');
|
|
assert.equal(resolve('?hp_alpha=0').persist, '0');
|
|
});
|
|
|
|
test('unknown alpha values fail closed for the current resolution without rewriting storage', () => {
|
|
const unknown = resolve('?hp_alpha=on', '', '1');
|
|
assert.equal(unknown.alpha, false);
|
|
assert.deepEqual(unknown.active, []);
|
|
assert.equal(unknown.persist, undefined);
|
|
assert.equal(unknown.knownUrlOperation, false);
|
|
|
|
const recognisedAfterUnknown = resolve('?hp_alpha=on&hp_alpha=1', '', '0');
|
|
assert.equal(recognisedAfterUnknown.alpha, true);
|
|
assert.equal(recognisedAfterUnknown.persist, '1');
|
|
});
|
|
|
|
test('hash parser keeps space routing alongside the alpha operation', () => {
|
|
assert.equal(hashSpace('#hp_alpha=1&space=first%20floor'), 'first floor');
|
|
assert.equal(hashSpace('#space=ground&hp_alpha=1'), 'ground');
|
|
const result = resolve('', '#space=ground&hp_alpha=1');
|
|
assert.equal(result.space, 'ground');
|
|
assert.equal(result.alpha, true);
|
|
assert.deepEqual(result.active, ['probe']);
|
|
});
|
|
|
|
test('only canonical alpha storage is read and legacy Labs inputs do not migrate', () => {
|
|
assert.equal(resolve('', '', '1').alpha, true);
|
|
assert.equal(resolve('', '', '0').alpha, false);
|
|
for (const malformed of [undefined, null, '', 'true', ' 1', '["iso"]', '{bad']) {
|
|
assert.equal(resolve('', '', malformed).alpha, false, String(malformed));
|
|
}
|
|
const legacyUrl = resolve('?hp-labs=iso', '#space=f1&hp-labs=iso', null);
|
|
assert.equal(legacyUrl.alpha, false);
|
|
assert.deepEqual(legacyUrl.active, []);
|
|
assert.equal(legacyUrl.persist, undefined);
|
|
assert.equal(legacyUrl.space, 'f1');
|
|
});
|
|
|
|
test('a reloaded alpha module replaces both browser location listeners', async () => {
|
|
const previousWindow = globalThis.window;
|
|
const listeners = new Map();
|
|
const values = new Map([['houseplan_card_labs_v1', '["iso"]']]);
|
|
const fakeWindow = {
|
|
location: { search: '', hash: '' },
|
|
localStorage: {
|
|
getItem: (key) => values.get(key) ?? null,
|
|
setItem: (key, value) => values.set(key, value),
|
|
},
|
|
addEventListener(type, listener) {
|
|
const registered = listeners.get(type) ?? new Set();
|
|
registered.add(listener);
|
|
listeners.set(type, registered);
|
|
},
|
|
removeEventListener(type, listener) {
|
|
listeners.get(type)?.delete(listener);
|
|
},
|
|
dispatch(type) {
|
|
for (const listener of [...(listeners.get(type) ?? [])]) listener({ type });
|
|
},
|
|
};
|
|
|
|
try {
|
|
globalThis.window = fakeWindow;
|
|
const first = await import('../test-build/labs.js?listener-instance=first');
|
|
const firstSnapshots = [];
|
|
const unsubscribeFirst = first.subscribeLabs((value) => { firstSnapshots.push(value); });
|
|
assert.equal(firstSnapshots[0].alpha, false, 'legacy storage must not migrate');
|
|
|
|
fakeWindow.location.search = '?hp_alpha=1';
|
|
fakeWindow.dispatch('popstate');
|
|
assert.equal(firstSnapshots.at(-1).alpha, true);
|
|
assert.equal(values.get(ALPHA_STORAGE_KEY), '1');
|
|
assert.equal(fakeWindow.__hpAlpha, true);
|
|
assert.deepEqual(fakeWindow.__hpLabs, [], '#649: the shipped registry is empty');
|
|
|
|
const second = await import('../test-build/labs.js?listener-instance=second');
|
|
let secondPublishes = 0;
|
|
const unsubscribeSecond = second.subscribeLabs(() => { secondPublishes += 1; });
|
|
const firstPublishes = firstSnapshots.length;
|
|
|
|
assert.equal(listeners.get('hashchange')?.size, 1);
|
|
assert.equal(listeners.get('popstate')?.size, 1);
|
|
fakeWindow.dispatch('hashchange');
|
|
fakeWindow.dispatch('popstate');
|
|
assert.equal(firstSnapshots.length, firstPublishes,
|
|
'the replaced module must not receive location events');
|
|
assert.equal(secondPublishes, 3,
|
|
'the current module receives initial, hash and popstate publishes');
|
|
|
|
unsubscribeFirst();
|
|
unsubscribeSecond();
|
|
} finally {
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|
|
|
|
test('storage failures keep the current URL decision safe and usable', async () => {
|
|
const previousWindow = globalThis.window;
|
|
const listeners = new Map();
|
|
const fakeWindow = {
|
|
location: { search: '?hp_alpha=1', hash: '#space=f1' },
|
|
localStorage: {
|
|
getItem: () => { throw new Error('read denied'); },
|
|
setItem: () => { throw new Error('write denied'); },
|
|
},
|
|
addEventListener(type, listener) { listeners.set(type, listener); },
|
|
removeEventListener(type, listener) {
|
|
if (listeners.get(type) === listener) listeners.delete(type);
|
|
},
|
|
};
|
|
try {
|
|
globalThis.window = fakeWindow;
|
|
const module = await import('../test-build/labs.js?storage-failure');
|
|
let current;
|
|
const unsubscribe = module.subscribeLabs((value) => { current = value; });
|
|
assert.equal(current.alpha, true);
|
|
assert.deepEqual(current.active, []);
|
|
assert.equal(current.space, 'f1');
|
|
unsubscribe();
|
|
} finally {
|
|
if (previousWindow === undefined) delete globalThis.window;
|
|
else globalThis.window = previousWindow;
|
|
}
|
|
});
|