This commit is contained in:
Mateusz Gruszczyński
2026-09-07 10:01:52 +02:00
parent 11da46c4d6
commit 7772e1e339
18 changed files with 347 additions and 170 deletions
+18 -7
View File
@@ -27,7 +27,7 @@ class FakeWebSocket {
}
const app = {
devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [],
devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], accessTokens: [],
settings: { house_mode: 'cool', debug: { overlay_enabled: false }, home_assistant: {} },
system: {}, outdoorTemperature: null, controlPlan: null, controlPlanRevision: null,
controlPlanPushReady: false, controlPlanTimer: null, currentView: 'dashboard',
@@ -54,7 +54,6 @@ const context = {
withBase: value => value,
$: () => null,
isFormDirty: () => false,
loadSettingsSections: async () => {},
applySettingsSection: (name, data) => { app.settings[name] = data; },
loadDebugBacklog: hit('loadDebugBacklog'),
loadBootstrap: async () => { calls.loadBootstrap = (calls.loadBootstrap || 0) + 1; },
@@ -78,6 +77,7 @@ for (const name of renderNames) context[name] = hit(name);
vm.createContext(context);
vm.runInContext(fs.readFileSync(path.join(root, 'web/js/dashboard.js'), 'utf8'), context, { filename: 'dashboard.js' });
vm.runInContext(fs.readFileSync(path.join(root, 'web/js/bootstrap.js'), 'utf8'), context, { filename: 'bootstrap.js' });
vm.runInContext(fs.readFileSync(path.join(root, 'web/js/realtime.js'), 'utf8'), context, { filename: 'realtime.js' });
// Source files define DOM-heavy renderers. Replace those implementations for this state-machine test.
for (const name of renderNames) context[name] = hit(name);
@@ -85,7 +85,6 @@ context.updateDevice = device => {
const index = app.devices.findIndex(item => item.id === device.id);
if (index >= 0) app.devices[index] = device; else app.devices.push(device);
};
context.loadSettingsSections = async () => {};
context.applySettingsSection = (name, data) => { app.settings[name] = data; };
context.loadDebugBacklog = hit('loadDebugBacklog');
context.loadBootstrap = async () => { calls.loadBootstrap = (calls.loadBootstrap || 0) + 1; };
@@ -112,10 +111,8 @@ async function testBootstrapReloadQueue() {
assert.equal(requestPath, '/api/bootstrap');
bootstrapCalls += 1;
if (bootstrapCalls === 1) return firstResponse;
return { devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], access_tokens: [], house: { mode: 'cool' }, system: {}, control_plan: { marker: 'second' }, control_plan_revision: 2 };
return { devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], access_tokens: [], settings: {}, house: { mode: 'cool' }, system: {}, control_plan: { marker: 'second' }, control_plan_revision: 2 };
},
fetchSettingsSections: async () => ({ application: {}, gree: {}, history: {}, influxdb: {}, notifications: {}, night: {}, home_assistant: {}, debug: {} }),
aggregateSettingsSections: () => ({ home_assistant: {}, debug: { overlay_enabled: false } }),
renderAll: () => {}, scheduleControlPlanLoad: () => {}, loadDebugBacklog: () => {}, toast: () => {}, tr: key => key,
$: () => ({ open: false }), connectWebSocket: () => {},
setTimeout: (fn, delay) => { queuedTimers.push({ fn, delay }); return queuedTimers.length; },
@@ -126,8 +123,9 @@ async function testBootstrapReloadQueue() {
assert.equal(c.app.loading, true);
await c.loadBootstrap();
assert.equal(c.app.bootstrapReloadPending, true, 'a bootstrap request during loading must be queued');
firstResolve({ devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], access_tokens: [], house: { mode: 'cool' }, system: {}, control_plan: { marker: 'first' }, control_plan_revision: 1 });
firstResolve({ devices: [], zones: [], groups: [], schedules: [], automations: [], flows: [], access_tokens: [], settings: {}, house: { mode: 'cool' }, system: {}, control_plan: { marker: 'first' }, control_plan_revision: 1 });
await first;
assert.equal(c.app.outdoorTemperature, null, 'null/missing outdoor temperature must stay unknown, not become 0 C');
assert.equal(c.app.bootstrapReloadPending, false);
assert.equal(queuedTimers.length, 1);
assert.equal(queuedTimers[0].delay, 0);
@@ -138,6 +136,7 @@ async function testBootstrapReloadQueue() {
async function main() {
await testBootstrapReloadQueue();
const apiCallsBeforeBootstrap = apiCalls.length;
await send('bootstrap', {
devices: [{ id: 'd1', power: false }],
zones: [{ id: 'z1', demand: false }],
@@ -145,9 +144,21 @@ async function main() {
schedules: [{ id: 's1', name: 'S1' }],
automations: [{ id: 'a1', name: 'A1' }],
flows: [{ id: 'f1', name: 'F1' }],
access_tokens: [{ id: 't1', name: 'HA', token_prefix: 'abc', created_at: '2026-09-07T08:00:00Z' }],
settings: {
application: { simulator_enabled: true },
gree: { controller_id: 'test-controller' },
history: { retention_days: 30 }, influxdb: {}, notifications: {}, night: {},
home_assistant: { sensor_aliases: { 'sensor.bootstrap': 'Bootstrap' }, flow_inputs: [], outdoor_assist_enabled: true },
debug: { overlay_enabled: false },
},
house: { mode: 'heat' }, system: { ok: true }, outdoor_temperature: 10,
control_plan: { marker: 'p1' }, control_plan_revision: 1,
});
assert.equal(apiCalls.length, apiCallsBeforeBootstrap, 'WebSocket bootstrap must not fetch split settings endpoints');
assert.equal(app.accessTokens[0].id, 't1', 'WebSocket bootstrap must refresh access-token metadata');
assert.equal(app.settings.controller_id, 'test-controller');
assert.equal(app.sensorAliases['sensor.bootstrap'], 'Bootstrap');
assert.equal(app.controlPlan.marker, 'p1');
assert.equal(app.controlPlanRevision, 1);
assert.equal(app.controlPlanPushReady, true);