Files
houseplan-card/test/decor-style-persist.test.mjs
T
Codex 10a1a26612 feat: persist the Background editor's default style with the plan (#377)
settings.decor_default_style (all fields optional, validated) seeds
_decorStyle once from the first config that arrives; every UI change of
the session default flows through one runtime method with a 1s debounce
and the ordinary serialized expected_rev write path. The built-in default
is stored as the absence of the key; a partial or garbage key falls back
per-field. decorStyleFromSettings/decorStyleToSettings are the single
snake_case<->camelCase conversion point.

User-Visible: yes
Issue: #377
2026-08-29 21:39:08 +03:00

55 lines
2.5 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
DEFAULT_DECOR_STYLE, decorStyleFromSettings, decorStyleToSettings,
} from '../test-build/editors/decor/geometry.js';
// #377 AC2: a stored key merges over the built-in default, per-field.
test('#377 AC2: settings key merges over DEFAULT_DECOR_STYLE', () => {
const full = decorStyleFromSettings({
color: '#8b0000', opacity: 0.9, width_cm: 5, fill: true,
fill_color: '#112233', fill_opacity: 0.5,
}, DEFAULT_DECOR_STYLE);
assert.deepEqual(full, {
color: '#8b0000', opacity: 0.9, widthCm: 5, fill: true,
fillColor: '#112233', fillOpacity: 0.5,
}, 'all six fields cross the snake_case boundary');
const partial = decorStyleFromSettings({ color: '#8b0000' }, DEFAULT_DECOR_STYLE);
assert.deepEqual(partial, { ...DEFAULT_DECOR_STYLE, color: '#8b0000' },
'a partial key inherits every other field from the default');
});
test('#377 AC2: garbage fields fall back per-field, never explode', () => {
const noisy = decorStyleFromSettings({
color: 42, opacity: 'loud', width_cm: -3, fill: 'yes',
fill_color: null, fill_opacity: 7, unknown_extra: true,
}, DEFAULT_DECOR_STYLE);
assert.equal(noisy.color, DEFAULT_DECOR_STYLE.color);
assert.equal(noisy.widthCm, DEFAULT_DECOR_STYLE.widthCm, 'non-positive width is rejected');
assert.equal(noisy.fill, DEFAULT_DECOR_STYLE.fill, 'non-boolean fill is rejected');
assert.equal(noisy.fillColor, DEFAULT_DECOR_STYLE.fillColor);
assert.equal(noisy.fillOpacity, 1, 'opacity clamps into 0..1');
assert.ok(!('unknown_extra' in noisy), 'unknown fields are ignored');
});
// #377 AC3: no key -> the built-in default, untouched legacy behaviour.
test('#377 AC3: absent or non-object key yields the default style', () => {
for (const raw of [undefined, null, 'x', 7]) {
assert.deepEqual(decorStyleFromSettings(raw, DEFAULT_DECOR_STYLE), DEFAULT_DECOR_STYLE);
}
});
// #377: the default is stored as the ABSENCE of the key.
test('#377: toSettings returns null for the default and a snake_case patch otherwise', () => {
assert.equal(decorStyleToSettings({ ...DEFAULT_DECOR_STYLE }), null);
const patch = decorStyleToSettings({ ...DEFAULT_DECOR_STYLE, color: '#8b0000' });
assert.deepEqual(patch, {
color: '#8b0000', opacity: 1, width_cm: 3.6, fill: false,
fill_color: '#607d8b', fill_opacity: 0.25,
});
// the pair is a lossless round-trip for any persisted style
const style = decorStyleFromSettings(patch, DEFAULT_DECOR_STYLE);
assert.deepEqual(decorStyleToSettings(style), patch);
});