iobroker.statechart 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,699 +0,0 @@
1
- "use strict";
2
-
3
- const { expect } = require("chai");
4
- const { ChartRuntime, getChartHash } = require("./chart-runtime");
5
-
6
- /**
7
- * Creates a host which keeps the states in memory
8
- *
9
- * @param {Record<string, any>} [states]
10
- */
11
- function createHost(states = {}) {
12
- const host = {
13
- states: { ...states },
14
- /** @type {string[]} */
15
- active: [],
16
- /** @type {string[]} */
17
- warnings: [],
18
- readState: async (/** @type {string} */ id) => host.states[id],
19
- /** @type {{ id: string; val: any; ack: boolean | undefined }[]} all writes */
20
- writes: [],
21
- writeState: async (
22
- /** @type {string} */ id,
23
- /** @type {any} */ val,
24
- /** @type {boolean | undefined} */ ack,
25
- ) => {
26
- host.states[id] = val;
27
- host.writes.push({ id, val, ack });
28
- },
29
- /** @type {any[][]} */
30
- messages: [],
31
- sendTo: (/** @type {any[]} */ ...args) => host.messages.push(args),
32
- /** @type {{ latitude: number; longitude: number } | null} */
33
- location: null,
34
- getLocation: () => host.location,
35
- /** @type {string[]} info and debug messages */
36
- infos: [],
37
- onActiveChange: (/** @type {{ names: string }} */ active) => host.active.push(active.names),
38
- /** @type {import("./chart-runtime").PersistedChart | null} the last persisted data */
39
- persisted: null,
40
- persistCount: 0,
41
- persist: (/** @type {import("./chart-runtime").PersistedChart} */ data) => {
42
- // a copy like a round trip through a state, so later changes do not change the stored data
43
- host.persisted = JSON.parse(JSON.stringify(data));
44
- host.persistCount++;
45
- },
46
- loadPersisted: async () => host.persisted,
47
- log: {
48
- debug: (/** @type {string} */ msg) => host.infos.push(`debug: ${msg}`),
49
- info: (/** @type {string} */ msg) => host.infos.push(`info: ${msg}`),
50
- warn: (/** @type {string} */ msg) => host.warnings.push(msg),
51
- error: (/** @type {string} */ msg) => host.warnings.push(msg),
52
- },
53
- };
54
- return host;
55
- }
56
-
57
- /**
58
- * A simple light switch: off -> on on motion, back to off after 50 ms without motion
59
- *
60
- * @returns {import("./chart-model").Chart}
61
- */
62
- function createLightChart() {
63
- return {
64
- version: 1,
65
- name: "Light",
66
- enabled: true,
67
- initial: "off",
68
- symbols: [
69
- { name: "motion", scope: "input", stateId: "test.0.motion" },
70
- { name: "light", scope: "output", stateId: "test.0.light" },
71
- { name: "count", scope: "local", initial: 0 },
72
- ],
73
- states: [
74
- { id: "off", name: "Off", entry: "light = false;", x: 0, y: 0 },
75
- { id: "on", name: "On", entry: "light = true; count++;", x: 0, y: 0 },
76
- ],
77
- transitions: [
78
- { id: "t1", source: "off", target: "on", label: "motion[motion == true]" },
79
- { id: "t2", source: "on", target: "off", label: "after(50, msec)[!motion]" },
80
- ],
81
- };
82
- }
83
-
84
- const wait = (/** @type {number} */ ms) => new Promise(resolve => setTimeout(resolve, ms));
85
-
86
- describe("chart-runtime => ChartRuntime", () => {
87
- /** @type {ChartRuntime | undefined} */
88
- let runtime;
89
- afterEach(() => runtime?.stop());
90
-
91
- it("enters the initial state and executes its entry action", async () => {
92
- const host = createHost({ "test.0.light": true });
93
- runtime = new ChartRuntime("light", createLightChart(), host);
94
- await runtime.start();
95
- expect(host.active).to.deep.equal(["Off"]);
96
- expect(host.states["test.0.light"]).to.equal(false);
97
- });
98
-
99
- it("takes transitions on input changes and writes outputs", async () => {
100
- const host = createHost({ "test.0.motion": false, "test.0.light": false });
101
- runtime = new ChartRuntime("light", createLightChart(), host);
102
- await runtime.start();
103
- runtime.handleInput("test.0.motion", true);
104
- expect(host.active).to.deep.equal(["Off", "On"]);
105
- expect(host.states["test.0.light"]).to.equal(true);
106
- expect(runtime.sandbox.count).to.equal(1);
107
- });
108
-
109
- it("executes temporal transitions", async () => {
110
- const host = createHost({ "test.0.motion": false });
111
- runtime = new ChartRuntime("light", createLightChart(), host);
112
- await runtime.start();
113
- runtime.handleInput("test.0.motion", true);
114
- runtime.handleInput("test.0.motion", false);
115
- await wait(100);
116
- expect(host.active).to.deep.equal(["Off", "On", "Off"]);
117
- expect(host.states["test.0.light"]).to.equal(false);
118
- });
119
-
120
- it("executes during actions if no transition is taken", async () => {
121
- const chart = createLightChart();
122
- chart.states[0].during = "count += 10;";
123
- const host = createHost({ "test.0.motion": false });
124
- runtime = new ChartRuntime("light", chart, host);
125
- await runtime.start();
126
- runtime.handleInput("test.0.motion", false);
127
- expect(runtime.sandbox.count).to.equal(10);
128
- runtime.handleInput("test.0.motion", true);
129
- expect(runtime.sandbox.count).to.equal(1 + 10);
130
- });
131
-
132
- it("re-enters the state on self transitions", async () => {
133
- const chart = createLightChart();
134
- chart.transitions.push({ id: "t3", source: "on", target: "on", label: "motion[motion]" });
135
- const host = createHost({ "test.0.motion": false });
136
- runtime = new ChartRuntime("light", chart, host);
137
- await runtime.start();
138
- runtime.handleInput("test.0.motion", true);
139
- runtime.handleInput("test.0.motion", true);
140
- expect(runtime.sandbox.count).to.equal(2);
141
- });
142
-
143
- it("supports nested states", async () => {
144
- const chart = createLightChart();
145
- chart.states[1].initial = "dim";
146
- chart.states.push(
147
- { id: "dim", name: "Dim", parent: "on", x: 0, y: 0 },
148
- { id: "bright", name: "Bright", parent: "on", x: 0, y: 0 },
149
- );
150
- chart.transitions.push({ id: "t3", source: "dim", target: "bright", label: "after(10, msec)" });
151
- const host = createHost({ "test.0.motion": false });
152
- runtime = new ChartRuntime("light", chart, host);
153
- await runtime.start();
154
- runtime.handleInput("test.0.motion", true);
155
- expect(host.active.at(-1)).to.equal("On.Dim");
156
- await wait(30);
157
- expect(host.active.at(-1)).to.equal("On.Bright");
158
- });
159
-
160
- it("logs errors in conditions instead of crashing", async () => {
161
- const chart = createLightChart();
162
- chart.transitions[0].label = "[unknownVariable > 1]";
163
- const host = createHost();
164
- runtime = new ChartRuntime("light", chart, host);
165
- await runtime.start();
166
- runtime.handleInput("test.0.motion", true);
167
- expect(host.active).to.deep.equal(["Off"]);
168
- expect(host.warnings.join()).to.match(/unknownVariable is not defined/);
169
- });
170
-
171
- it("keeps let declarations local to each action", async () => {
172
- const chart = createLightChart();
173
- chart.states[0].entry = "let x = 1; light = x == 1;";
174
- chart.states[1].entry = "let x = 2; light = x == 2;";
175
- const host = createHost();
176
- runtime = new ChartRuntime("light", chart, host);
177
- await runtime.start();
178
- runtime.handleInput("test.0.motion", true);
179
- expect(host.warnings).to.deep.equal([]);
180
- expect(host.states["test.0.light"]).to.equal(true);
181
- });
182
-
183
- it("provides the functions of function blocks to conditions and actions", async () => {
184
- const chart = createLightChart();
185
- chart.functions = [
186
- {
187
- id: "f1",
188
- code: "const LIMIT = 2;\nfunction allowed() { return count < LIMIT; }\nfunction switchOn() { light = true; count++; }",
189
- x: 0,
190
- y: 0,
191
- },
192
- ];
193
- chart.states[1].entry = "switchOn();";
194
- chart.transitions[0].label = "motion[motion && allowed()]";
195
- chart.transitions[1].label = "motion[!motion]";
196
- const host = createHost({ "test.0.motion": false });
197
- runtime = new ChartRuntime("light", chart, host);
198
- await runtime.start();
199
- for (let i = 0; i < 3; i++) {
200
- runtime.handleInput("test.0.motion", true);
201
- runtime.handleInput("test.0.motion", false);
202
- }
203
- expect(host.warnings).to.deep.equal([]);
204
- // the third switch on is not allowed anymore
205
- expect(runtime.sandbox.count).to.equal(2);
206
- expect(host.active).to.deep.equal(["Off", "On", "Off", "On", "Off"]);
207
- });
208
- });
209
-
210
- describe("chart-runtime => temporal transitions and restoring after a restart", () => {
211
- /** @type {ChartRuntime[]} */
212
- const runtimes = [];
213
- afterEach(() => runtimes.splice(0).forEach(r => r.stop()));
214
-
215
- /**
216
- * @param {import("./chart-model").Chart} chart
217
- * @param {ReturnType<typeof createHost>} host
218
- * @param {boolean} [restore]
219
- */
220
- async function start(chart, host, restore = false) {
221
- const runtime = new ChartRuntime("light", chart, host);
222
- runtimes.push(runtime);
223
- await runtime.start({ restore });
224
- return runtime;
225
- }
226
-
227
- it("keeps an expired temporal transition valid until its condition is true", async () => {
228
- const host = createHost({ "test.0.motion": false });
229
- const runtime = await start(createLightChart(), host);
230
- runtime.handleInput("test.0.motion", true);
231
- // after 50 ms there is still motion, so the light stays on ...
232
- await wait(80);
233
- expect(host.active.at(-1)).to.equal("On");
234
- // ... and goes off as soon as the motion stops
235
- runtime.handleInput("test.0.motion", false);
236
- expect(host.active.at(-1)).to.equal("Off");
237
- });
238
-
239
- it("restores the active state and the locals without executing the entry actions again", async () => {
240
- const host = createHost({ "test.0.motion": false });
241
- const first = await start(createLightChart(), host);
242
- first.handleInput("test.0.motion", true);
243
- expect(first.sandbox.count).to.equal(1);
244
- first.stop();
245
-
246
- // the adapter was restarted, meanwhile someone switched the light off by hand
247
- host.states["test.0.light"] = false;
248
- host.active = [];
249
- const second = await start(createLightChart(), host, true);
250
- expect(host.active).to.deep.equal(["On"]);
251
- expect(second.sandbox.count).to.equal(1, "locals restored, entry not executed again");
252
- expect(host.states["test.0.light"]).to.equal(false, "outputs are not written again");
253
- });
254
-
255
- it("continues the timers with their remaining time", async () => {
256
- const host = createHost({ "test.0.motion": false });
257
- const first = await start(createLightChart(), host);
258
- first.handleInput("test.0.motion", true);
259
- first.handleInput("test.0.motion", false);
260
- first.stop();
261
-
262
- // "On" was entered 40 ms before the restart, so after(50, msec) expires 10 ms after it
263
- /** @type {any} */ (host.persisted).entered.on = Date.now() - 40;
264
- await start(createLightChart(), host, true);
265
- expect(host.active.at(-1)).to.equal("On");
266
- await wait(30);
267
- expect(host.active.at(-1)).to.equal("Off");
268
- });
269
-
270
- it("takes transitions whose time expired while the adapter was stopped", async () => {
271
- const host = createHost({ "test.0.motion": false });
272
- const first = await start(createLightChart(), host);
273
- first.handleInput("test.0.motion", true);
274
- first.handleInput("test.0.motion", false);
275
- first.stop();
276
-
277
- /** @type {any} */ (host.persisted).entered.on = Date.now() - 60_000;
278
- await start(createLightChart(), host, true);
279
- await wait(5);
280
- expect(host.active.at(-1)).to.equal("Off");
281
- });
282
-
283
- it("starts in the initial state if the chart was changed", async () => {
284
- const host = createHost({ "test.0.motion": false });
285
- const first = await start(createLightChart(), host);
286
- first.handleInput("test.0.motion", true);
287
- first.stop();
288
-
289
- const changed = createLightChart();
290
- changed.states[1].entry = "light = true; count += 2;";
291
- host.active = [];
292
- const second = await start(changed, host, true);
293
- expect(host.active).to.deep.equal(["Off"]);
294
- expect(second.sandbox.count).to.equal(0);
295
- });
296
-
297
- it("ignores changed positions in the editor", async () => {
298
- const host = createHost({ "test.0.motion": false });
299
- const first = await start(createLightChart(), host);
300
- first.handleInput("test.0.motion", true);
301
- first.stop();
302
-
303
- const moved = createLightChart();
304
- moved.states[1].x = 500;
305
- host.active = [];
306
- await start(moved, host, true);
307
- expect(host.active).to.deep.equal(["On"]);
308
- });
309
-
310
- it("ignores comments and notes for the hash, but not the logic", () => {
311
- const chart = createLightChart();
312
- const hash = getChartHash(chart);
313
- const documented = createLightChart();
314
- documented.comment = "About the chart";
315
- documented.states[0].comment = "Light is off";
316
- documented.transitions[0].comment = "Motion switches it on";
317
- documented.notes = [{ id: "n1", text: "A note", x: 0, y: 0 }];
318
- expect(getChartHash(documented)).to.equal(hash);
319
- documented.transitions[0].label = "motion";
320
- expect(getChartHash(documented)).not.to.equal(hash);
321
- });
322
-
323
- it("does not restore if it is not requested or switched off for the chart", async () => {
324
- const host = createHost({ "test.0.motion": false });
325
- const first = await start(createLightChart(), host);
326
- first.handleInput("test.0.motion", true);
327
- first.stop();
328
- const persisted = host.persisted;
329
-
330
- host.active = [];
331
- (await start(createLightChart(), host, false)).stop();
332
- expect(host.active).to.deep.equal(["Off"]);
333
-
334
- host.persisted = persisted;
335
- host.active = [];
336
- const chart = createLightChart();
337
- chart.restoreState = false;
338
- await start(chart, host, true);
339
- expect(host.active).to.deep.equal(["Off"]);
340
- });
341
-
342
- it("persists only changes", async () => {
343
- const host = createHost({ "test.0.motion": false });
344
- const runtime = await start(createLightChart(), host);
345
- const count = host.persistCount;
346
- // a wakeup without any change
347
- runtime.handleInput("test.0.motion", false);
348
- expect(host.persistCount).to.equal(count);
349
- runtime.handleInput("test.0.motion", true);
350
- expect(host.persistCount).to.equal(count + 1);
351
- });
352
- });
353
-
354
- describe("chart-runtime => helper functions", () => {
355
- /** @type {ChartRuntime | undefined} */
356
- let runtime;
357
- afterEach(() => runtime?.stop());
358
-
359
- /**
360
- * Starts a chart whose initial state executes the given entry action
361
- *
362
- * @param {string} entry
363
- * @param {(host: ReturnType<typeof createHost>) => void} [prepare]
364
- */
365
- async function run(entry, prepare) {
366
- const chart = createLightChart();
367
- chart.symbols.push({ name: "result", scope: "local", initial: null });
368
- chart.states[0].entry = entry;
369
- const host = createHost();
370
- prepare?.(host);
371
- runtime = new ChartRuntime("light", chart, host);
372
- await runtime.start();
373
- return { host, runtime };
374
- }
375
-
376
- it("logs with a level", async () => {
377
- const { host } = await run('log("hello"); log("careful", "warn"); log("details", "debug");');
378
- expect(host.infos).to.include.members(["info: [Light] hello", "debug: [Light] details"]);
379
- expect(host.warnings).to.deep.equal(["[Light] careful"]);
380
- });
381
-
382
- it("writes any state, with ack", async () => {
383
- const { host } = await run('setState("other.0.x", 5); setState("other.0.y", "a", true);');
384
- expect(host.writes).to.deep.include({ id: "other.0.x", val: 5, ack: false });
385
- expect(host.writes).to.deep.include({ id: "other.0.y", val: "a", ack: true });
386
- });
387
-
388
- it("writes delayed and cancels pending writes when the chart stops", async () => {
389
- const { host, runtime: r } = await run(
390
- 'setStateDelayed("other.0.x", 1, 20); setStateDelayed("other.0.y", 2, 60);',
391
- );
392
- await new Promise(resolve => setTimeout(resolve, 40));
393
- expect(host.states["other.0.x"]).to.equal(1);
394
- r.stop();
395
- await new Promise(resolve => setTimeout(resolve, 40));
396
- expect(host.states).not.to.have.property("other.0.y");
397
- });
398
-
399
- it("sends messages to adapters", async () => {
400
- const { host } = await run('sendTo("telegram.0", "send", { text: "Hello" });');
401
- expect(host.messages).to.deep.equal([["telegram.0", "send", { text: "Hello" }]]);
402
- });
403
-
404
- it("checks time ranges", async () => {
405
- const { runtime: r } = await run('result = isTimeBetween("00:00", "23:59:59");');
406
- expect(r.sandbox.result).to.equal(true);
407
- });
408
-
409
- it("calculates astro times at the configured location", async () => {
410
- const { runtime: r } = await run(
411
- 'const sunrise = astroTime("sunrise"); result = [sunrise.getTime(), astroTime("sunrise", 30).getTime(), isDaytime()];',
412
- host => (host.location = { latitude: 52.52, longitude: 13.4 }),
413
- );
414
- const [sunrise, later, daytime] = r.sandbox.result;
415
- expect(later - sunrise).to.equal(30 * 60_000);
416
- const sunset = r.getSunTimes(new Date()).sunset.getTime();
417
- const now = Date.now();
418
- expect(daytime).to.equal(now >= sunrise && now < sunset);
419
- });
420
-
421
- it("reports a missing location and unknown astro events as problem of the action", async () => {
422
- const { host } = await run("result = isDaytime();");
423
- expect(host.warnings.join()).to.match(/No location \(latitude\/longitude\) configured/);
424
- const second = await run(
425
- 'result = astroTime("teatime");',
426
- h => (h.location = { latitude: 52.52, longitude: 13.4 }),
427
- );
428
- expect(second.host.warnings.join()).to.match(/Unknown astro event "teatime"/);
429
- });
430
-
431
- it("knows the helper functions in the validation", () => {
432
- const { validateCode } = require("./chart-validation");
433
- expect(
434
- validateCode('sendTo("a.0", "b", {}); setStateDelayed("x", 1, 100); isDaytime();', "statements", []),
435
- ).to.deep.equal([]);
436
- });
437
- });
438
-
439
- describe("chart-runtime => triggers of inputs", () => {
440
- /** @type {ChartRuntime | undefined} */
441
- let runtime;
442
- afterEach(() => runtime?.stop());
443
-
444
- /**
445
- * A chart which counts its wakeups (during action) and stores the last motion value
446
- *
447
- * @param {Partial<import("./chart-model").ChartSymbol>} options Of the input "motion"
448
- */
449
- async function start(options) {
450
- const chart = createLightChart();
451
- Object.assign(chart.symbols[0], options);
452
- chart.symbols.push({ name: "seen", scope: "local", initial: null });
453
- chart.states[0].during = "count++; seen = motion;";
454
- // no transitions, so every wakeup executes the during action
455
- chart.transitions = [];
456
- const host = createHost({ "test.0.motion": false });
457
- runtime = new ChartRuntime("light", chart, host);
458
- await runtime.start();
459
- return runtime;
460
- }
461
-
462
- it("wakes up on every update by default, regardless of ack", async () => {
463
- const r = await start({});
464
- r.handleInput("test.0.motion", false, { ack: true });
465
- r.handleInput("test.0.motion", false, { ack: false });
466
- r.handleInput("test.0.motion", true);
467
- expect(r.sandbox.count).to.equal(3);
468
- });
469
-
470
- it("wakes up only on changes", async () => {
471
- const r = await start({ trigger: "change" });
472
- r.handleInput("test.0.motion", false);
473
- r.handleInput("test.0.motion", true);
474
- r.handleInput("test.0.motion", true);
475
- r.handleInput("test.0.motion", false);
476
- expect(r.sandbox.count).to.equal(2);
477
- });
478
-
479
- it("uses only confirmed values", async () => {
480
- const r = await start({ ack: "ack" });
481
- r.handleInput("test.0.motion", true, { ack: false });
482
- expect(r.sandbox.count).to.equal(0);
483
- expect(r.sandbox.motion).to.equal(false, "a filtered update does not change the variable");
484
- r.handleInput("test.0.motion", true, { ack: true });
485
- expect(r.sandbox.count).to.equal(1);
486
- expect(r.sandbox.seen).to.equal(true);
487
- });
488
-
489
- it("uses only commands", async () => {
490
- const r = await start({ ack: "command" });
491
- r.handleInput("test.0.motion", true, { ack: true });
492
- r.handleInput("test.0.motion", true);
493
- expect(r.sandbox.count).to.equal(0);
494
- r.handleInput("test.0.motion", true, { ack: false });
495
- expect(r.sandbox.count).to.equal(1);
496
- });
497
-
498
- it("debounces: one wakeup with the last value after the input was quiet", async () => {
499
- const r = await start({ debounceMs: 30 });
500
- r.handleInput("test.0.motion", true);
501
- await wait(10);
502
- r.handleInput("test.0.motion", false);
503
- await wait(10);
504
- r.handleInput("test.0.motion", true);
505
- expect(r.sandbox.count).to.equal(0);
506
- expect(r.sandbox.motion).to.equal(false, "the variable changes only with the wakeup");
507
- await wait(50);
508
- expect(r.sandbox.count).to.equal(1);
509
- expect(r.sandbox.seen).to.equal(true);
510
- });
511
-
512
- it("debounces and checks for changes with the last value", async () => {
513
- const r = await start({ debounceMs: 20, trigger: "change" });
514
- // bounces back to the old value: no change, no wakeup
515
- r.handleInput("test.0.motion", true);
516
- r.handleInput("test.0.motion", false);
517
- await wait(40);
518
- expect(r.sandbox.count).to.equal(0);
519
- });
520
-
521
- it("cancels pending debounce timers when it stops", async () => {
522
- const r = await start({ debounceMs: 20 });
523
- r.handleInput("test.0.motion", true);
524
- r.stop();
525
- await wait(40);
526
- expect(r.sandbox.count).to.equal(0);
527
- });
528
- });
529
-
530
- /**
531
- * A heating control with a super state: Auto (with the children Idle and Heat) and Manual.
532
- * All actions write to "trace", so the tests can check the order of execution.
533
- *
534
- * @returns {import("./chart-model").Chart}
535
- */
536
- function createHeatingChart() {
537
- /**
538
- * @param {string} id
539
- * @param {string} name
540
- * @param {string | null} parent
541
- */
542
- const state = (id, name, parent) => ({
543
- id,
544
- name,
545
- parent,
546
- entry: `trace.push("${name}/entry");`,
547
- during: `trace.push("${name}/during");`,
548
- exit: `trace.push("${name}/exit");`,
549
- x: 0,
550
- y: 0,
551
- });
552
- return {
553
- version: 1,
554
- name: "Heating",
555
- enabled: true,
556
- initial: "auto",
557
- symbols: [
558
- { name: "temp", scope: "input", stateId: "test.0.temp" },
559
- { name: "manual", scope: "input", stateId: "test.0.manual" },
560
- { name: "reset", scope: "input", stateId: "test.0.reset" },
561
- { name: "trace", scope: "local", initial: [] },
562
- ],
563
- states: [
564
- { ...state("auto", "Auto", null), initial: "idle" },
565
- state("idle", "Idle", "auto"),
566
- state("heat", "Heat", "auto"),
567
- state("manualState", "Manual", null),
568
- ],
569
- transitions: [
570
- { id: "toManual", source: "auto", target: "manualState", label: "[manual]" },
571
- { id: "toAuto", source: "manualState", target: "auto", label: "[!manual]" },
572
- { id: "startHeating", source: "idle", target: "heat", label: "[temp < 20]" },
573
- { id: "stopHeating", source: "heat", target: "idle", label: "[temp > 22]" },
574
- ],
575
- };
576
- }
577
-
578
- describe("chart-runtime => nested states (Stateflow semantics)", () => {
579
- /** @type {ChartRuntime | undefined} */
580
- let runtime;
581
- afterEach(() => runtime?.stop());
582
-
583
- /**
584
- * @param {import("./chart-model").Chart} chart
585
- * @param {Record<string, any>} states
586
- */
587
- async function start(chart, states) {
588
- const host = createHost(states);
589
- runtime = new ChartRuntime("heating", chart, host);
590
- await runtime.start();
591
- return host;
592
- }
593
-
594
- it("enters parents before children and leaves children before parents", async () => {
595
- const host = await start(createHeatingChart(), { "test.0.temp": 18, "test.0.manual": false });
596
- expect(runtime?.sandbox.trace).to.deep.equal(["Auto/entry", "Idle/entry"]);
597
- runtime?.sandbox.trace.splice(0);
598
- runtime?.handleInput("test.0.manual", true);
599
- expect(runtime?.sandbox.trace).to.deep.equal(["Idle/exit", "Auto/exit", "Manual/entry"]);
600
- expect(host.active.at(-1)).to.equal("Manual");
601
- });
602
-
603
- it("checks the transitions of the parent before those of the child", async () => {
604
- const host = await start(createHeatingChart(), { "test.0.temp": 18, "test.0.manual": true });
605
- // both transitions are valid now: Auto -> Manual and Idle -> Heat
606
- runtime?.handleInput("test.0.temp", 18);
607
- expect(host.active).to.deep.equal(["Auto.Idle", "Manual"]);
608
- });
609
-
610
- it("executes the during action of the parent before the child is processed", async () => {
611
- const host = await start(createHeatingChart(), { "test.0.temp": 21, "test.0.manual": false });
612
- runtime?.sandbox.trace.splice(0);
613
- // no transition: during of parent and child
614
- runtime?.handleInput("test.0.temp", 21);
615
- expect(runtime?.sandbox.trace).to.deep.equal(["Auto/during", "Idle/during"]);
616
- runtime?.sandbox.trace.splice(0);
617
- // transition of the child: the parent's during is executed, the child's not
618
- runtime?.handleInput("test.0.temp", 18);
619
- expect(runtime?.sandbox.trace).to.deep.equal(["Auto/during", "Idle/exit", "Heat/entry"]);
620
- expect(host.active.at(-1)).to.equal("Auto.Heat");
621
- });
622
-
623
- it("keeps the parent active on inner transitions", async () => {
624
- const chart = createHeatingChart();
625
- chart.transitions.push({ id: "restart", source: "auto", target: "idle", label: "reset[reset]" });
626
- const host = await start(chart, { "test.0.temp": 18, "test.0.manual": false, "test.0.reset": false });
627
- runtime?.handleInput("test.0.temp", 18);
628
- expect(host.active.at(-1)).to.equal("Auto.Heat");
629
- runtime?.sandbox.trace.splice(0);
630
- runtime?.handleInput("test.0.reset", true);
631
- // inner transitions are checked after the during action of the parent
632
- expect(runtime?.sandbox.trace).to.deep.equal(["Auto/during", "Heat/exit", "Idle/entry"]);
633
- expect(host.active.at(-1)).to.equal("Auto.Idle");
634
- });
635
-
636
- it("leaves and enters the parent again if requested", async () => {
637
- const chart = createHeatingChart();
638
- chart.transitions.push({
639
- id: "restart",
640
- source: "auto",
641
- target: "idle",
642
- label: "reset[reset]",
643
- reenterParent: true,
644
- });
645
- await start(chart, { "test.0.temp": 18, "test.0.manual": false, "test.0.reset": false });
646
- runtime?.handleInput("test.0.temp", 18);
647
- runtime?.sandbox.trace.splice(0);
648
- runtime?.handleInput("test.0.reset", true);
649
- // an outer transition: checked before the during action, the parent is left
650
- expect(runtime?.sandbox.trace).to.deep.equal(["Heat/exit", "Auto/exit", "Auto/entry", "Idle/entry"]);
651
- });
652
-
653
- it("leaves and enters the parent on transitions from a child to the parent", async () => {
654
- const chart = createHeatingChart();
655
- chart.transitions.push({ id: "up", source: "heat", target: "auto", label: "reset[reset]" });
656
- const host = await start(chart, { "test.0.temp": 18, "test.0.manual": false, "test.0.reset": false });
657
- runtime?.handleInput("test.0.temp", 18);
658
- runtime?.sandbox.trace.splice(0);
659
- runtime?.handleInput("test.0.reset", true);
660
- expect(runtime?.sandbox.trace).to.deep.equal([
661
- "Auto/during",
662
- "Heat/exit",
663
- "Auto/exit",
664
- "Auto/entry",
665
- "Idle/entry",
666
- ]);
667
- expect(host.active.at(-1)).to.equal("Auto.Idle");
668
- });
669
-
670
- it("restores nested states", async () => {
671
- const host = await start(createHeatingChart(), { "test.0.temp": 18, "test.0.manual": false });
672
- runtime?.handleInput("test.0.temp", 18);
673
- expect(host.active.at(-1)).to.equal("Auto.Heat");
674
- runtime?.stop();
675
-
676
- host.active = [];
677
- runtime = new ChartRuntime("heating", createHeatingChart(), host);
678
- await runtime.start({ restore: true });
679
- expect(host.active).to.deep.equal(["Auto.Heat"]);
680
- expect([...runtime.enteredAt.keys()]).to.deep.equal(["auto", "heat"]);
681
- // the trace (a local) is restored as well, without new entries
682
- expect(runtime.sandbox.trace.at(-1)).to.equal("Heat/entry");
683
- });
684
-
685
- it("uses the first child if the initial state is no child (anymore)", async () => {
686
- const chart = createHeatingChart();
687
- // e.g. "idle" was the initial state and was moved out of "auto" in the editor
688
- chart.states[0].initial = "manualState";
689
- chart.initial = "heat";
690
- const host = await start(chart, { "test.0.temp": 21, "test.0.manual": false });
691
- expect(host.active).to.deep.equal(["Auto.Idle"]);
692
- });
693
-
694
- it("does not change the initial values of locals in the chart model", async () => {
695
- const chart = createHeatingChart();
696
- await start(chart, { "test.0.temp": 18, "test.0.manual": false });
697
- expect(chart.symbols.find(s => s.name === "trace")?.initial).to.deep.equal([]);
698
- });
699
- });