ESPSomfy-RTS/test/js/helpers.js
cjkas 4415beb4f7 fix: web UI client review findings (step 2a)
- One socket at a time: a socket being replaced is let go without a word,
  and connect() or a new token while a reconnect waits opens no second one.
  A new token reconnects at once, without a refused.
- Events that came before the schema go as soon as it is whole, ahead of
  the sync frames formatted after them, not at syncEnd over newer state.
- A schema's hash counts only once it has come whole, so a sync cut short
  gets it again.
- Feeds are subscribed again after a reconnect; an unknown feed is refused.
- A long op refused as busy without its id goes again.
- A sync without repeaters clears them; order changes name their list; a
  refused resync is called off.
- refused: a listener can close() to stop the retries, which the UI does
  once /loginContext says a login is needed. The client itself can't tell a
  refused token from a device that is down.
- localStorage keeps the schema by default. minify.py removes its work
  folder even when a bundle fails. ADR 0005 records the two bundles.
- Smaller: ProtoSchema.ROWS/withoutRows, ProtoModel.ITEMS, named type
  numbers, unused feeds map and FakeSocket.all gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 18:04:07 +01:00

84 lines
3.2 KiB
JavaScript

// What the web UI client tests share: the frames the e2e suite recorded from a
// device (test/fixtures/proto2), and stand-ins for the socket and the timers.
const path = require('node:path');
const FIXTURES = path.join(__dirname, '..', 'fixtures', 'proto2');
const hello = require(path.join(FIXTURES, 'hello.json'));
const sync = require(path.join(FIXTURES, 'sync.json'));
const schemaJson = require(path.join(FIXTURES, 'schema.json'));
const { ProtoSchema } = require('../../data-src/js/schema.js');
// A schema filled from the recorded sync, as a client builds it.
function recordedSchema() {
const s = new ProtoSchema();
for (const f of sync) if (f.t === 4) s.add(f.d);
s.hash = hello.d.sh;
return s;
}
// The recorded sync's device sections, expanded, frame by frame.
function deviceSections(schema) {
return sync.filter((f) => f.t === 4).map((f) => schema.expand(ProtoSchema.withoutRows(f.d)));
}
// The recorded sync as the device sends it to a client that has the schema.
function syncWithoutSchema() {
return sync.map((f) => Object.assign({}, f, { d: ProtoSchema.withoutRows(f.d || {}) }));
}
// The recorded sync's items under their short keys.
function recordedItems(key) {
return sync.filter((f) => f.t === 4 && f.d[key]).flatMap((f) => f.d[key]);
}
// A WebSocket the test drives: it records what the client sends, and the test
// opens, delivers and closes.
class FakeSocket {
constructor(url) {
this.url = url;
this.sent = [];
this.closed = false;
FakeSocket.last = this;
FakeSocket.count++;
}
send(text) { this.sent.push(JSON.parse(text)); }
close() {
if (this.closed) return;
this.closed = true;
if (this.onclose) this.onclose({});
}
open() { if (this.onopen) this.onopen({}); }
deliver(frame) { this.onmessage({ data: typeof frame === 'string' ? frame : JSON.stringify(frame) }); }
// The device closing it, or refusing it before it opened.
drop() { this.close(); }
}
// How many the client has opened.
FakeSocket.count = 0;
FakeSocket.reset = () => { FakeSocket.count = 0; FakeSocket.last = null; };
// Timers the test runs by hand.
class FakeTimers {
constructor() {
this.timers = new Map();
this.next = 1;
this.setTimeout = (fn, ms) => {
const id = this.next++;
this.timers.set(id, { fn: fn, ms: ms });
return id;
};
this.clearTimeout = (id) => { this.timers.delete(id); };
}
// Runs every timer waiting, the shortest first, and returns their delays.
runAll() {
const due = [...this.timers.entries()].sort((a, b) => a[1].ms - b[1].ms);
this.timers.clear();
for (const [, t] of due) t.fn();
return due.map(([, t]) => t.ms);
}
}
// localStorage as far as the client uses it.
class FakeStorage {
constructor() { this.items = {}; }
getItem(key) { return Object.prototype.hasOwnProperty.call(this.items, key) ? this.items[key] : null; }
setItem(key, value) { this.items[key] = String(value); }
}
module.exports = { hello, sync, schemaJson, recordedSchema, deviceSections, syncWithoutSchema, recordedItems, FakeSocket,
FakeTimers, FakeStorage };