mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Пятый вариант «Отображение»: маркер показывает выбранное значение и при этом
никогда не меняет цвет — ни по состоянию, ни по тревоге, ни по недоступности,
ни под цвет RGB-света, ни по активности.
Четыре прежних режима задавали одним выбором две независимые вещи: что
нарисовано внутри маркера и красится ли он состоянием. Поэтому вместо
сравнения с одним токеном появились два производных предиката рядом с
`normalizeDeviceDisplay` — `displayWantsValue` и `displayIsNeutral`, — и их
спрашивают политика, слой пульсации, внешний бейдж, редактор и три ветки
живого слоя пылесоса.
Две ловушки, из-за которых режим не сводится к одной строке в словаре:
- быстрый путь «статичному маркеру источники не нужны» (`sourceDetails: false`)
— это основной путь рендера плана, карточки пространства и PDF. Он оставлен
только режиму без значения: иначе число пропало бы именно на плане и
осталось в предпросмотре редактора;
- три ветки живого пылесоса сравнивают режим строкой и не читают политику,
поэтому зелёная политика их не гарантирует. Свидетель держит две
конфигурации карты: сопоставленная доказывает puck и след, несопоставленная
— бейдж маршрута (при совпадающей калибровке маршрут `ready`, и бейджа не
было бы ни в одном режиме). Бейдж проверяется при наполненном буфере
позиций, иначе его отсутствие объяснялось бы первой веткой.
Потолок initial View перецентрирован 291_700 → 292_400 без изменения общего
бюджета: измеренный факт 291 346 Б оставлял под прежним центром 354 Б —
внутри шумовой полосы метрики.
Эталон: в `device-icon-state-table-{light,dark}` включённая RGB-лампа
переведена в новый режим. Кадры обязаны разойтись; приёмка — отдельным
коммитом класса D с полного линуксового артефакта Validate.
Issue: #588
User-Visible: yes
115 lines
4.8 KiB
JavaScript
115 lines
4.8 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { resolveDevicePulse } from '../test-build/device-pulse.js';
|
|
|
|
const visual = (status = 'neutral', activity = 'none') => ({
|
|
availability: 'available', status, activity,
|
|
});
|
|
|
|
const resolve = (overrides = {}) => resolveDevicePulse({
|
|
display: 'icon_ripple', visual: visual(), semanticActivity: 'none',
|
|
liveStates: true, effectiveHidden: false, now: 1_000,
|
|
...overrides,
|
|
});
|
|
|
|
test('pulse priority is hidden/static, alarm, finite event, continuous state', () => {
|
|
assert.equal(resolve({ effectiveHidden: true, visual: visual('alarm') }).kind, 'none');
|
|
assert.equal(resolve({ display: 'static_icon', visual: visual('alarm') }).kind, 'none');
|
|
// #588 AC4: значение внутри маркера не возвращает ему ни тревожную пульсацию,
|
|
// ни обычную — нейтральность принадлежит цвету, а не лицу.
|
|
assert.equal(resolve({ display: 'value_static_icon', visual: visual('alarm') }).kind, 'none');
|
|
assert.equal(resolve({
|
|
display: 'value_static_icon', semanticActivity: 'presence',
|
|
}).kind, 'none');
|
|
assert.equal(resolve({ display: 'badge', visual: visual('alarm') }).kind, 'alarm');
|
|
assert.equal(resolve({
|
|
shortReason: 'event', shortExpiresAt: 2_000, semanticActivity: 'running',
|
|
}).kind, 'short');
|
|
assert.equal(resolve({ semanticActivity: 'presence' }).kind, 'continuous');
|
|
});
|
|
|
|
test('ordinary activity belongs only to icon plus activity mode', () => {
|
|
for (const display of ['badge', 'value']) {
|
|
assert.equal(resolve({ display, semanticActivity: 'running' }).kind, 'none');
|
|
}
|
|
assert.equal(resolve({ display: 'icon_ripple', semanticActivity: 'running' }).kind, 'continuous');
|
|
assert.equal(resolve({ liveStates: false, semanticActivity: 'running' }).kind, 'none');
|
|
});
|
|
|
|
test('critical alarm survives live-state and face-mode gates but not disabled binding', () => {
|
|
assert.equal(resolve({ liveStates: false, visual: visual('alarm') }).kind, 'alarm');
|
|
for (const display of ['badge', 'value']) {
|
|
assert.equal(resolve({ display, liveStates: false, visual: visual('alarm') }).kind, 'alarm');
|
|
}
|
|
assert.equal(resolve({ bindingUnavailable: true, visual: visual('alarm') }).kind, 'none');
|
|
});
|
|
|
|
test('reduced motion replaces ordinary waves with a dot and keeps alarm semantic', () => {
|
|
const ordinary = resolve({
|
|
semanticActivity: 'running', reducedMotion: true,
|
|
color: '#123456', diameterScale: 5,
|
|
});
|
|
assert.equal(ordinary.animated, false);
|
|
assert.equal(ordinary.reducedMotionIndicator, 'dot');
|
|
assert.equal(ordinary.color, '#123456');
|
|
assert.equal(ordinary.diameterScale, 1);
|
|
const short = resolve({
|
|
shortReason: 'event', shortExpiresAt: 2_000, reducedMotion: true,
|
|
});
|
|
assert.equal(short.kind, 'short');
|
|
assert.equal(short.animated, false);
|
|
assert.equal(short.reducedMotionIndicator, 'dot');
|
|
assert.equal(short.diameterScale, 1);
|
|
const alarm = resolve({
|
|
visual: visual('alarm'), reducedMotion: true,
|
|
color: '#123456', diameterScale: 5,
|
|
});
|
|
assert.equal(alarm.kind, 'alarm');
|
|
assert.equal(alarm.animated, false);
|
|
assert.equal(alarm.reducedMotionIndicator, 'none');
|
|
assert.equal(alarm.color, '#F0410C');
|
|
assert.equal(alarm.diameterScale, 1.5);
|
|
});
|
|
|
|
test('package defaults map presence, running and neutral transition to distinct colors', () => {
|
|
const presence = resolve({ semanticActivity: 'presence' });
|
|
assert.equal(presence.color, '#1DC21D');
|
|
assert.equal(presence.diameterScale, 1.5);
|
|
assert.equal(resolve({ semanticActivity: 'running' }).color, '#F0A00C');
|
|
assert.equal(resolve({
|
|
semanticActivity: 'transition', visual: visual('neutral', 'transition'),
|
|
}).color, '#0C82F0');
|
|
assert.equal(resolve({
|
|
semanticActivity: 'presence', color: '#123456', diameterScale: 4,
|
|
}).color, '#123456');
|
|
});
|
|
|
|
test('expired finite runtime never leaves a static ring', () => {
|
|
const pulse = resolve({ shortReason: 'event', shortExpiresAt: 999 });
|
|
assert.equal(pulse.kind, 'none');
|
|
});
|
|
|
|
test('inactive projection retains configured preview colour and diameter', () => {
|
|
const pulse = resolve({
|
|
color: '#123456', diameterScale: 4.25,
|
|
});
|
|
assert.equal(pulse.kind, 'none');
|
|
assert.equal(pulse.color, '#123456');
|
|
assert.equal(pulse.diameterScale, 4.25);
|
|
});
|
|
|
|
test('unknown and unavailable device state cannot retain an activity pulse', () => {
|
|
for (const kind of ['short', 'continuous']) {
|
|
const pulse = resolveDevicePulse({
|
|
display: 'icon_ripple',
|
|
visual: { availability: 'unavailable', status: 'neutral', activity: 'transition' },
|
|
semanticActivity: 'transition',
|
|
shortReason: kind === 'short' ? 'transition' : null,
|
|
shortExpiresAt: kind === 'short' ? Date.now() + 1_000 : null,
|
|
liveStates: true,
|
|
effectiveHidden: false,
|
|
});
|
|
assert.equal(pulse.kind, 'none');
|
|
}
|
|
});
|