@digital-alchemy/hass 25.11.16 → 25.11.17-beta.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.
- package/dist/helpers/entity-state.d.mts +20 -3
- package/dist/helpers/utility.d.mts +1 -1
- package/dist/mock_assistant/mock-assistant.module.mjs.map +1 -1
- package/dist/mock_assistant/services/websocket-api.service.d.mts +3 -1
- package/dist/mock_assistant/services/websocket-api.service.mjs +43 -4
- package/dist/mock_assistant/services/websocket-api.service.mjs.map +1 -1
- package/dist/services/feature.service.d.mts +2 -2
- package/dist/services/feature.service.mjs.map +1 -1
- package/dist/services/reference.service.d.mts +1 -1
- package/dist/services/reference.service.mjs +59 -5
- package/dist/services/reference.service.mjs.map +1 -1
- package/dist/testing/entity.spec.mjs +14 -0
- package/dist/testing/entity.spec.mjs.map +1 -1
- package/dist/testing/ref-by.spec.mjs +802 -2
- package/dist/testing/ref-by.spec.mjs.map +1 -1
- package/dist/testing/scheduler.spec.d.mts +1 -0
- package/dist/testing/scheduler.spec.mjs +412 -0
- package/dist/testing/scheduler.spec.mjs.map +1 -0
- package/dist/testing/workflow.spec.mjs +1 -1
- package/dist/testing/workflow.spec.mjs.map +1 -1
- package/package.json +16 -16
- package/src/helpers/entity-state.mts +20 -3
- package/src/helpers/utility.mts +1 -1
- package/src/mock_assistant/mock-assistant.module.mts +0 -1
- package/src/mock_assistant/services/websocket-api.service.mts +46 -4
- package/src/services/feature.service.mts +9 -6
- package/src/services/reference.service.mts +75 -7
- package/src/testing/entity.spec.mts +15 -0
- package/src/testing/ref-by.spec.mts +962 -2
- package/src/testing/scheduler.spec.mts +444 -0
- package/src/testing/workflow.spec.mts +1 -1
|
@@ -14,9 +14,15 @@ describe("References", () => {
|
|
|
14
14
|
describe("refBy.id", () => {
|
|
15
15
|
it("can grab references by id", async () => {
|
|
16
16
|
expect.assertions(2);
|
|
17
|
-
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
17
|
+
await hassTestRunner.run(({ lifecycle, hass, logger, context }) => {
|
|
18
18
|
lifecycle.onReady(() => {
|
|
19
19
|
const sensor = hass.refBy.id("sensor.magic");
|
|
20
|
+
sensor.onStateFor({
|
|
21
|
+
context,
|
|
22
|
+
exec: () => logger.info("HIT"),
|
|
23
|
+
for: "5h",
|
|
24
|
+
matches: new_state => new_state.state === "test",
|
|
25
|
+
});
|
|
20
26
|
expect(sensor).toBeDefined();
|
|
21
27
|
expect(sensor.state).toBe("unavailable");
|
|
22
28
|
});
|
|
@@ -168,7 +174,7 @@ describe("References", () => {
|
|
|
168
174
|
describe("functionality", () => {
|
|
169
175
|
describe("operators", () => {
|
|
170
176
|
it("has", async () => {
|
|
171
|
-
expect.assertions(
|
|
177
|
+
expect.assertions(16);
|
|
172
178
|
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
173
179
|
lifecycle.onReady(() => {
|
|
174
180
|
const entity = hass.refBy.id("switch.bedroom_lamp");
|
|
@@ -181,6 +187,7 @@ describe("References", () => {
|
|
|
181
187
|
"last",
|
|
182
188
|
"nextState",
|
|
183
189
|
"once",
|
|
190
|
+
"onStateFor",
|
|
184
191
|
"onUpdate",
|
|
185
192
|
"previous",
|
|
186
193
|
"removeAllListeners",
|
|
@@ -215,6 +222,7 @@ describe("References", () => {
|
|
|
215
222
|
"last",
|
|
216
223
|
"nextState",
|
|
217
224
|
"once",
|
|
225
|
+
"onStateFor",
|
|
218
226
|
"onUpdate",
|
|
219
227
|
"previous",
|
|
220
228
|
"removeAllListeners",
|
|
@@ -296,6 +304,958 @@ describe("References", () => {
|
|
|
296
304
|
});
|
|
297
305
|
});
|
|
298
306
|
});
|
|
307
|
+
|
|
308
|
+
it("returns undefined for non-string property access", async () => {
|
|
309
|
+
expect.assertions(1);
|
|
310
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
311
|
+
lifecycle.onReady(() => {
|
|
312
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
313
|
+
const symbolProperty = Symbol("test");
|
|
314
|
+
// Accessing with a Symbol should return undefined
|
|
315
|
+
// This tests the proxyGetLogic non-string check
|
|
316
|
+
expect(Reflect.get(sensor, symbolProperty)).toBeUndefined();
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("nextState returns early when timeout is undefined", async () => {
|
|
322
|
+
expect.assertions(1);
|
|
323
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
324
|
+
lifecycle.onReady(async () => {
|
|
325
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
326
|
+
// Call nextState without timeout - should wait indefinitely for state change
|
|
327
|
+
const nextStatePromise = sensor.nextState();
|
|
328
|
+
|
|
329
|
+
// Emit a state update - promise should resolve with the new state
|
|
330
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
331
|
+
state: "updated",
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
const result = await nextStatePromise;
|
|
335
|
+
// Should resolve with the updated state when timeout is undefined
|
|
336
|
+
expect(result?.state).toBe("updated");
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it("nextState kills timer when removed before timeout", async () => {
|
|
342
|
+
expect.assertions(1);
|
|
343
|
+
vi.useFakeTimers();
|
|
344
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
345
|
+
lifecycle.onReady(async () => {
|
|
346
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
347
|
+
let promiseResolved = false;
|
|
348
|
+
// Call nextState with timeout
|
|
349
|
+
const nextStatePromise = sensor.nextState("0.1s");
|
|
350
|
+
nextStatePromise.then(() => {
|
|
351
|
+
promiseResolved = true;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Remove all listeners before timeout completes - should kill the timer
|
|
355
|
+
sensor.removeAllListeners();
|
|
356
|
+
|
|
357
|
+
// Advance time past the timeout - promise should not resolve
|
|
358
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
359
|
+
|
|
360
|
+
// Give a small delay to check if promise resolved
|
|
361
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
362
|
+
|
|
363
|
+
// The promise should not resolve because done was set to undefined and timer was killed
|
|
364
|
+
expect(promiseResolved).toBe(false);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
vi.useRealTimers();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it("nextState complete function checks if done exists before calling", async () => {
|
|
371
|
+
expect.assertions(1);
|
|
372
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
373
|
+
lifecycle.onReady(async () => {
|
|
374
|
+
const sensor = hass.refBy.id("sensor.magic");
|
|
375
|
+
let promiseResolved = false;
|
|
376
|
+
// Call nextState without timeout
|
|
377
|
+
const nextStatePromise = sensor.nextState();
|
|
378
|
+
nextStatePromise.then(() => {
|
|
379
|
+
promiseResolved = true;
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
// Remove all listeners - this sets done = undefined
|
|
383
|
+
sensor.removeAllListeners();
|
|
384
|
+
|
|
385
|
+
// Emit an update - complete function should check if done exists
|
|
386
|
+
// Since done is undefined, it should not call done()
|
|
387
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
388
|
+
state: "updated",
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// Give a small delay to check if promise resolved
|
|
392
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
393
|
+
|
|
394
|
+
// The promise should not resolve because done was set to undefined
|
|
395
|
+
// and the complete function checks if (done) before calling it
|
|
396
|
+
expect(promiseResolved).toBe(false);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("returns previous entity state", async () => {
|
|
402
|
+
expect.assertions(3);
|
|
403
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
404
|
+
lifecycle.onReady(async () => {
|
|
405
|
+
const entity_id = "sensor.magic";
|
|
406
|
+
const entity = hass.refBy.id(entity_id);
|
|
407
|
+
const startState = entity.state;
|
|
408
|
+
|
|
409
|
+
// Emit an update
|
|
410
|
+
await mock_assistant.events.emitEntityUpdate(entity_id, {
|
|
411
|
+
state: "updated",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const updatedState = entity.state;
|
|
415
|
+
const previousState = entity.previous;
|
|
416
|
+
|
|
417
|
+
expect(updatedState).toBe("updated");
|
|
418
|
+
expect(startState).not.toBe("updated");
|
|
419
|
+
expect(previousState.state).toBe(startState);
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe("waitForState", () => {
|
|
426
|
+
it("resolves when state matches", async () => {
|
|
427
|
+
expect.assertions(2);
|
|
428
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
429
|
+
lifecycle.onReady(async () => {
|
|
430
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
431
|
+
const waitPromise = entity.waitForState("target", "1s");
|
|
432
|
+
|
|
433
|
+
// Emit update with matching state
|
|
434
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
435
|
+
state: "target",
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const result = await waitPromise;
|
|
439
|
+
expect(result?.state).toBe("target");
|
|
440
|
+
expect(result).toBeDefined();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it("does not resolve when state does not match", async () => {
|
|
446
|
+
expect.assertions(1);
|
|
447
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
448
|
+
lifecycle.onReady(async () => {
|
|
449
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
450
|
+
let promiseResolved = false;
|
|
451
|
+
// Call waitForState without timeout - should wait indefinitely
|
|
452
|
+
const waitPromise = entity.waitForState("target");
|
|
453
|
+
waitPromise.then(() => {
|
|
454
|
+
promiseResolved = true;
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
// Emit update with non-matching state
|
|
458
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
459
|
+
state: "other",
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Give a small delay to check if promise resolved
|
|
463
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
464
|
+
|
|
465
|
+
// Promise should not resolve because state didn't match
|
|
466
|
+
expect(promiseResolved).toBe(false);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it("resolves with undefined when timeout expires", async () => {
|
|
472
|
+
expect.assertions(1);
|
|
473
|
+
vi.useFakeTimers();
|
|
474
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
475
|
+
lifecycle.onReady(async () => {
|
|
476
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
477
|
+
const waitPromise = entity.waitForState("target", "0.1s");
|
|
478
|
+
|
|
479
|
+
// Advance time past timeout without matching state
|
|
480
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
481
|
+
|
|
482
|
+
const result = await waitPromise;
|
|
483
|
+
expect(result).toBeUndefined();
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
vi.useRealTimers();
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it("returns early when timeout is undefined", async () => {
|
|
490
|
+
expect.assertions(1);
|
|
491
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
492
|
+
lifecycle.onReady(async () => {
|
|
493
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
494
|
+
// Call waitForState without timeout - should wait indefinitely
|
|
495
|
+
const waitPromise = entity.waitForState("target");
|
|
496
|
+
|
|
497
|
+
// Emit update with matching state - promise should resolve
|
|
498
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
499
|
+
state: "target",
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const result = await waitPromise;
|
|
503
|
+
expect(result?.state).toBe("target");
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it("works with numeric state values", async () => {
|
|
509
|
+
expect.assertions(1);
|
|
510
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
511
|
+
lifecycle.onReady(async () => {
|
|
512
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
513
|
+
const waitPromise = entity.waitForState("42", "1s");
|
|
514
|
+
|
|
515
|
+
// Emit update with matching numeric state
|
|
516
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
517
|
+
state: "42",
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const result = await waitPromise;
|
|
521
|
+
expect(result?.state).toBe("42");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("kills timer when removed before timeout", async () => {
|
|
527
|
+
expect.assertions(1);
|
|
528
|
+
vi.useFakeTimers();
|
|
529
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
530
|
+
lifecycle.onReady(async () => {
|
|
531
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
532
|
+
let promiseResolved = false;
|
|
533
|
+
const waitPromise = entity.waitForState("target", "0.1s");
|
|
534
|
+
waitPromise.then(() => {
|
|
535
|
+
promiseResolved = true;
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// Remove all listeners before timeout completes - should kill the timer
|
|
539
|
+
entity.removeAllListeners();
|
|
540
|
+
|
|
541
|
+
// Advance time past the timeout - promise should not resolve
|
|
542
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
543
|
+
|
|
544
|
+
// Give a small delay to check if promise resolved
|
|
545
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
546
|
+
|
|
547
|
+
// The promise should not resolve because done was set to undefined and timer was killed
|
|
548
|
+
expect(promiseResolved).toBe(false);
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
vi.useRealTimers();
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
it("complete function checks if done exists before calling", async () => {
|
|
555
|
+
expect.assertions(1);
|
|
556
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
557
|
+
lifecycle.onReady(async () => {
|
|
558
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
559
|
+
let promiseResolved = false;
|
|
560
|
+
const waitPromise = entity.waitForState("target", "1s");
|
|
561
|
+
waitPromise.then(() => {
|
|
562
|
+
promiseResolved = true;
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Remove all listeners - this sets done = undefined
|
|
566
|
+
entity.removeAllListeners();
|
|
567
|
+
|
|
568
|
+
// Emit an update with matching state - complete function should check if done exists
|
|
569
|
+
// Since done is undefined, it should not call done()
|
|
570
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
571
|
+
state: "target",
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// Give a small delay to check if promise resolved
|
|
575
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
576
|
+
|
|
577
|
+
// The promise should not resolve because done was set to undefined
|
|
578
|
+
// and the complete function checks if (done) before calling it
|
|
579
|
+
expect(promiseResolved).toBe(false);
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
it("only resolves when state matches, ignores non-matching updates", async () => {
|
|
585
|
+
expect.assertions(2);
|
|
586
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
587
|
+
lifecycle.onReady(async () => {
|
|
588
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
589
|
+
const waitPromise = entity.waitForState("target", "1s");
|
|
590
|
+
|
|
591
|
+
// Emit update with non-matching state - should not resolve
|
|
592
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
593
|
+
state: "other1",
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Emit another non-matching update - should not resolve
|
|
597
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
598
|
+
state: "other2",
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
// Emit update with matching state - should resolve
|
|
602
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
603
|
+
state: "target",
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
const result = await waitPromise;
|
|
607
|
+
expect(result?.state).toBe("target");
|
|
608
|
+
expect(result).toBeDefined();
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
describe("listener management", () => {
|
|
615
|
+
it("addListener registers a remove callback", async () => {
|
|
616
|
+
expect.assertions(1);
|
|
617
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
618
|
+
lifecycle.onReady(() => {
|
|
619
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
620
|
+
const removeCallback = Object.assign(vi.fn(), { remove: vi.fn() });
|
|
621
|
+
|
|
622
|
+
entity.addListener(removeCallback);
|
|
623
|
+
|
|
624
|
+
// Call removeAllListeners - should call the registered callback
|
|
625
|
+
entity.removeAllListeners();
|
|
626
|
+
|
|
627
|
+
expect(removeCallback).toHaveBeenCalledTimes(1);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it("removeAllListeners calls all registered callbacks", async () => {
|
|
633
|
+
expect.assertions(3);
|
|
634
|
+
await hassTestRunner.run(({ lifecycle, hass }) => {
|
|
635
|
+
lifecycle.onReady(() => {
|
|
636
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
637
|
+
const removeCallback1 = Object.assign(vi.fn(), { remove: vi.fn() });
|
|
638
|
+
const removeCallback2 = Object.assign(vi.fn(), { remove: vi.fn() });
|
|
639
|
+
const removeCallback3 = Object.assign(vi.fn(), { remove: vi.fn() });
|
|
640
|
+
|
|
641
|
+
entity.addListener(removeCallback1);
|
|
642
|
+
entity.addListener(removeCallback2);
|
|
643
|
+
entity.addListener(removeCallback3);
|
|
644
|
+
|
|
645
|
+
entity.removeAllListeners();
|
|
646
|
+
|
|
647
|
+
expect(removeCallback1).toHaveBeenCalledTimes(1);
|
|
648
|
+
expect(removeCallback2).toHaveBeenCalledTimes(1);
|
|
649
|
+
expect(removeCallback3).toHaveBeenCalledTimes(1);
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it("removeAllListeners cleans up onUpdate listeners", async () => {
|
|
655
|
+
expect.assertions(1);
|
|
656
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
657
|
+
lifecycle.onReady(async () => {
|
|
658
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
659
|
+
const callback = vi.fn();
|
|
660
|
+
|
|
661
|
+
entity.onUpdate(callback);
|
|
662
|
+
|
|
663
|
+
// Remove all listeners
|
|
664
|
+
entity.removeAllListeners();
|
|
665
|
+
|
|
666
|
+
// Emit an update - callback should not be called
|
|
667
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
668
|
+
state: "updated",
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
expect(callback).not.toHaveBeenCalled();
|
|
672
|
+
});
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it("removeAllListeners cleans up once listeners", async () => {
|
|
677
|
+
expect.assertions(1);
|
|
678
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
679
|
+
lifecycle.onReady(async () => {
|
|
680
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
681
|
+
const callback = vi.fn();
|
|
682
|
+
|
|
683
|
+
entity.once(callback);
|
|
684
|
+
|
|
685
|
+
// Remove all listeners
|
|
686
|
+
entity.removeAllListeners();
|
|
687
|
+
|
|
688
|
+
// Emit an update - callback should not be called
|
|
689
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
690
|
+
state: "updated",
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
expect(callback).not.toHaveBeenCalled();
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
it("removeAllListeners cleans up onStateFor listeners", async () => {
|
|
699
|
+
expect.assertions(1);
|
|
700
|
+
vi.useFakeTimers();
|
|
701
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
702
|
+
lifecycle.onReady(async () => {
|
|
703
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
704
|
+
const callback = vi.fn();
|
|
705
|
+
|
|
706
|
+
entity.onStateFor({
|
|
707
|
+
context,
|
|
708
|
+
exec: callback,
|
|
709
|
+
for: "0.1s",
|
|
710
|
+
state: "on",
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Remove all listeners
|
|
714
|
+
entity.removeAllListeners();
|
|
715
|
+
|
|
716
|
+
// Change state to match
|
|
717
|
+
const updatePromise = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
718
|
+
state: "on",
|
|
719
|
+
});
|
|
720
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
721
|
+
await updatePromise;
|
|
722
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
723
|
+
|
|
724
|
+
expect(callback).not.toHaveBeenCalled();
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
vi.useRealTimers();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
it("removeAllListeners cleans up both addListener and built-in listeners", async () => {
|
|
731
|
+
expect.assertions(2);
|
|
732
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
733
|
+
lifecycle.onReady(async () => {
|
|
734
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
735
|
+
const addListenerCallback = Object.assign(vi.fn(), { remove: vi.fn() });
|
|
736
|
+
const onUpdateCallback = vi.fn();
|
|
737
|
+
|
|
738
|
+
entity.addListener(addListenerCallback);
|
|
739
|
+
entity.onUpdate(onUpdateCallback);
|
|
740
|
+
|
|
741
|
+
// Remove all listeners
|
|
742
|
+
entity.removeAllListeners();
|
|
743
|
+
|
|
744
|
+
// Emit an update - callbacks should not be called
|
|
745
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
746
|
+
state: "updated",
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
expect(addListenerCallback).toHaveBeenCalledTimes(1);
|
|
750
|
+
expect(onUpdateCallback).not.toHaveBeenCalled();
|
|
751
|
+
});
|
|
752
|
+
});
|
|
753
|
+
});
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
describe("once", () => {
|
|
757
|
+
it("calls callback once on next entity update", async () => {
|
|
758
|
+
expect.assertions(2);
|
|
759
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
760
|
+
lifecycle.onReady(async () => {
|
|
761
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
762
|
+
const callback = vi.fn();
|
|
763
|
+
|
|
764
|
+
entity.once(callback);
|
|
765
|
+
|
|
766
|
+
// Emit first update - callback should be called
|
|
767
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
768
|
+
state: "first",
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
772
|
+
|
|
773
|
+
// Emit second update - callback should not be called again
|
|
774
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
775
|
+
state: "second",
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
779
|
+
});
|
|
780
|
+
});
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("passes new_state and old_state to callback", async () => {
|
|
784
|
+
expect.assertions(3);
|
|
785
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
786
|
+
lifecycle.onReady(async () => {
|
|
787
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
788
|
+
const callback = vi.fn();
|
|
789
|
+
|
|
790
|
+
entity.once(callback);
|
|
791
|
+
|
|
792
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
793
|
+
state: "updated",
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
797
|
+
expect(callback).toHaveBeenCalledWith(
|
|
798
|
+
expect.objectContaining({ state: "updated" }),
|
|
799
|
+
expect.any(Object),
|
|
800
|
+
);
|
|
801
|
+
expect(callback.mock.calls[0]).toHaveLength(2);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it("removes listener after callback is called", async () => {
|
|
807
|
+
expect.assertions(2);
|
|
808
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
809
|
+
lifecycle.onReady(async () => {
|
|
810
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
811
|
+
const callback = vi.fn();
|
|
812
|
+
|
|
813
|
+
entity.once(callback);
|
|
814
|
+
|
|
815
|
+
// First update - callback should be called
|
|
816
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
817
|
+
state: "first",
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
821
|
+
|
|
822
|
+
// Second update - callback should not be called (listener removed)
|
|
823
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
824
|
+
state: "second",
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it("remove function prevents callback from being called", async () => {
|
|
833
|
+
expect.assertions(1);
|
|
834
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
835
|
+
lifecycle.onReady(async () => {
|
|
836
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
837
|
+
const callback = vi.fn();
|
|
838
|
+
|
|
839
|
+
const remove = entity.once(callback);
|
|
840
|
+
|
|
841
|
+
// Remove the listener before update
|
|
842
|
+
remove();
|
|
843
|
+
|
|
844
|
+
// Emit update - callback should not be called
|
|
845
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
846
|
+
state: "updated",
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
expect(callback).not.toHaveBeenCalled();
|
|
850
|
+
});
|
|
851
|
+
});
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it("handles multiple once listeners independently", async () => {
|
|
855
|
+
expect.assertions(4);
|
|
856
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant }) => {
|
|
857
|
+
lifecycle.onReady(async () => {
|
|
858
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
859
|
+
const callback1 = vi.fn();
|
|
860
|
+
const callback2 = vi.fn();
|
|
861
|
+
|
|
862
|
+
entity.once(callback1);
|
|
863
|
+
entity.once(callback2);
|
|
864
|
+
|
|
865
|
+
// First update - both callbacks should be called
|
|
866
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
867
|
+
state: "first",
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
871
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
872
|
+
|
|
873
|
+
// Second update - neither callback should be called
|
|
874
|
+
await mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
875
|
+
state: "second",
|
|
876
|
+
});
|
|
877
|
+
|
|
878
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
879
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
});
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
describe("onStateFor", () => {
|
|
886
|
+
it("executes callback when state matches for duration", async () => {
|
|
887
|
+
expect.assertions(1);
|
|
888
|
+
vi.useFakeTimers();
|
|
889
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
890
|
+
lifecycle.onReady(async () => {
|
|
891
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
892
|
+
const callback = vi.fn();
|
|
893
|
+
|
|
894
|
+
entity.onStateFor({
|
|
895
|
+
context,
|
|
896
|
+
exec: callback,
|
|
897
|
+
for: "0.1s",
|
|
898
|
+
state: "on",
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
// Change state to match
|
|
902
|
+
const updatePromise = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
903
|
+
state: "on",
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
// Advance timers to let the sleep(1ms) inside emitEntityUpdate complete
|
|
907
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
908
|
+
|
|
909
|
+
// Now await the emitEntityUpdate to complete
|
|
910
|
+
await updatePromise;
|
|
911
|
+
|
|
912
|
+
// Advance timers to trigger the callback (0.1s = 100ms)
|
|
913
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
914
|
+
|
|
915
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
vi.useRealTimers();
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it("does not execute callback if state changes before duration", async () => {
|
|
922
|
+
expect.assertions(1);
|
|
923
|
+
vi.useFakeTimers();
|
|
924
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
925
|
+
lifecycle.onReady(async () => {
|
|
926
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
927
|
+
const callback = vi.fn();
|
|
928
|
+
|
|
929
|
+
entity.onStateFor({
|
|
930
|
+
context,
|
|
931
|
+
exec: callback,
|
|
932
|
+
for: "0.1s",
|
|
933
|
+
state: "on",
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
// Change state to match
|
|
937
|
+
const updatePromise1 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
938
|
+
state: "on",
|
|
939
|
+
});
|
|
940
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
941
|
+
await updatePromise1;
|
|
942
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
943
|
+
|
|
944
|
+
// Change state away before timer completes
|
|
945
|
+
const updatePromise2 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
946
|
+
state: "off",
|
|
947
|
+
});
|
|
948
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
949
|
+
await updatePromise2;
|
|
950
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
951
|
+
|
|
952
|
+
expect(callback).not.toHaveBeenCalled();
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
vi.useRealTimers();
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
it("restarts timer when state changes back to matching", async () => {
|
|
959
|
+
expect.assertions(2);
|
|
960
|
+
vi.useFakeTimers();
|
|
961
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
962
|
+
lifecycle.onReady(async () => {
|
|
963
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
964
|
+
const callback = vi.fn();
|
|
965
|
+
|
|
966
|
+
entity.onStateFor({
|
|
967
|
+
context,
|
|
968
|
+
exec: callback,
|
|
969
|
+
for: "0.1s",
|
|
970
|
+
state: "on",
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// Change state to match
|
|
974
|
+
const updatePromise1 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
975
|
+
state: "on",
|
|
976
|
+
});
|
|
977
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
978
|
+
await updatePromise1;
|
|
979
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
980
|
+
|
|
981
|
+
// Change state away
|
|
982
|
+
const updatePromise2 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
983
|
+
state: "off",
|
|
984
|
+
});
|
|
985
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
986
|
+
await updatePromise2;
|
|
987
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
988
|
+
|
|
989
|
+
// Change back to matching - should start new timer
|
|
990
|
+
const updatePromise3 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
991
|
+
state: "on",
|
|
992
|
+
});
|
|
993
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
994
|
+
await updatePromise3;
|
|
995
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
996
|
+
expect(callback).not.toHaveBeenCalled();
|
|
997
|
+
|
|
998
|
+
// Complete the timer
|
|
999
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
1000
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1001
|
+
});
|
|
1002
|
+
});
|
|
1003
|
+
vi.useRealTimers();
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
it("uses custom matches function when provided", async () => {
|
|
1007
|
+
expect.assertions(3);
|
|
1008
|
+
vi.useFakeTimers();
|
|
1009
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
1010
|
+
lifecycle.onReady(async () => {
|
|
1011
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
1012
|
+
const callback = vi.fn();
|
|
1013
|
+
const matches = vi.fn((new_state, old_state) => {
|
|
1014
|
+
return new_state.state === "target" && old_state.state !== "target";
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
entity.onStateFor({
|
|
1018
|
+
context,
|
|
1019
|
+
exec: callback,
|
|
1020
|
+
for: "0.1s",
|
|
1021
|
+
matches,
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
// Change to non-matching state - matches should return false, so no timer
|
|
1025
|
+
const updatePromise1 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1026
|
+
state: "other",
|
|
1027
|
+
});
|
|
1028
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1029
|
+
await updatePromise1;
|
|
1030
|
+
// Verify matches was called and returned false (no timer should be set)
|
|
1031
|
+
expect(matches).toHaveBeenCalled();
|
|
1032
|
+
// Advance timers - callback should not be called
|
|
1033
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1034
|
+
expect(callback).not.toHaveBeenCalled();
|
|
1035
|
+
|
|
1036
|
+
// Change to matching state (target from different state) - matches should return true
|
|
1037
|
+
const updatePromise2 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1038
|
+
state: "target",
|
|
1039
|
+
});
|
|
1040
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1041
|
+
await updatePromise2;
|
|
1042
|
+
// Advance timers to trigger the callback
|
|
1043
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1044
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
vi.useRealTimers();
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
it("prevents duplicate timers when state matches multiple times", async () => {
|
|
1051
|
+
expect.assertions(1);
|
|
1052
|
+
vi.useFakeTimers();
|
|
1053
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
1054
|
+
lifecycle.onReady(async () => {
|
|
1055
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
1056
|
+
const callback = vi.fn();
|
|
1057
|
+
|
|
1058
|
+
entity.onStateFor({
|
|
1059
|
+
context,
|
|
1060
|
+
exec: callback,
|
|
1061
|
+
for: "0.1s",
|
|
1062
|
+
state: "on",
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
// Change state to match
|
|
1066
|
+
const updatePromise1 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1067
|
+
state: "on",
|
|
1068
|
+
});
|
|
1069
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1070
|
+
await updatePromise1;
|
|
1071
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
1072
|
+
|
|
1073
|
+
// Change to same matching state again - should not start new timer
|
|
1074
|
+
const updatePromise2 = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1075
|
+
state: "on",
|
|
1076
|
+
});
|
|
1077
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1078
|
+
await updatePromise2;
|
|
1079
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
1080
|
+
|
|
1081
|
+
// Complete original timer
|
|
1082
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
1083
|
+
|
|
1084
|
+
// Should only be called once
|
|
1085
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
vi.useRealTimers();
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
it("cleans up timer and listener when remove is called", async () => {
|
|
1092
|
+
expect.assertions(1);
|
|
1093
|
+
vi.useFakeTimers();
|
|
1094
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
1095
|
+
lifecycle.onReady(async () => {
|
|
1096
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
1097
|
+
const callback = vi.fn();
|
|
1098
|
+
|
|
1099
|
+
const remove = entity.onStateFor({
|
|
1100
|
+
context,
|
|
1101
|
+
exec: callback,
|
|
1102
|
+
for: "0.1s",
|
|
1103
|
+
state: "on",
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// Change state to match
|
|
1107
|
+
const updatePromise = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1108
|
+
state: "on",
|
|
1109
|
+
});
|
|
1110
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1111
|
+
await updatePromise;
|
|
1112
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
1113
|
+
|
|
1114
|
+
// Remove the listener
|
|
1115
|
+
remove();
|
|
1116
|
+
|
|
1117
|
+
// Advance time past when timer would have fired
|
|
1118
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1119
|
+
|
|
1120
|
+
// Callback should not have been called
|
|
1121
|
+
expect(callback).not.toHaveBeenCalled();
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
vi.useRealTimers();
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
it("handles cleanup when timer is already running", async () => {
|
|
1128
|
+
expect.assertions(1);
|
|
1129
|
+
vi.useFakeTimers();
|
|
1130
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
1131
|
+
lifecycle.onReady(async () => {
|
|
1132
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
1133
|
+
const callback = vi.fn();
|
|
1134
|
+
|
|
1135
|
+
const remove = entity.onStateFor({
|
|
1136
|
+
context,
|
|
1137
|
+
exec: callback,
|
|
1138
|
+
for: "0.1s",
|
|
1139
|
+
state: "on",
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
// Change state to match - starts timer
|
|
1143
|
+
const updatePromise = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1144
|
+
state: "on",
|
|
1145
|
+
});
|
|
1146
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1147
|
+
await updatePromise;
|
|
1148
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
1149
|
+
|
|
1150
|
+
// Remove should clean up the running timer
|
|
1151
|
+
remove();
|
|
1152
|
+
|
|
1153
|
+
// Advance time past when timer would have fired
|
|
1154
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1155
|
+
|
|
1156
|
+
expect(callback).not.toHaveBeenCalled();
|
|
1157
|
+
});
|
|
1158
|
+
});
|
|
1159
|
+
vi.useRealTimers();
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
it("works with numeric state values", async () => {
|
|
1163
|
+
expect.assertions(1);
|
|
1164
|
+
vi.useFakeTimers();
|
|
1165
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
1166
|
+
lifecycle.onReady(async () => {
|
|
1167
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
1168
|
+
const callback = vi.fn();
|
|
1169
|
+
|
|
1170
|
+
entity.onStateFor({
|
|
1171
|
+
context,
|
|
1172
|
+
exec: callback,
|
|
1173
|
+
for: "0.1s",
|
|
1174
|
+
state: "42",
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
// Change state to match numeric value
|
|
1178
|
+
const updatePromise = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1179
|
+
state: "42",
|
|
1180
|
+
});
|
|
1181
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1182
|
+
await updatePromise;
|
|
1183
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1184
|
+
|
|
1185
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1186
|
+
});
|
|
1187
|
+
});
|
|
1188
|
+
vi.useRealTimers();
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
it("passes proxy to exec callback", async () => {
|
|
1192
|
+
expect.assertions(3);
|
|
1193
|
+
vi.useFakeTimers();
|
|
1194
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
1195
|
+
lifecycle.onReady(async () => {
|
|
1196
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
1197
|
+
const callback = vi.fn(proxy => {
|
|
1198
|
+
expect(proxy.entity_id).toBe("sensor.magic");
|
|
1199
|
+
expect(proxy.state).toBe("on");
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
entity.onStateFor({
|
|
1203
|
+
context,
|
|
1204
|
+
exec: callback,
|
|
1205
|
+
for: "0.1s",
|
|
1206
|
+
state: "on",
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
const updatePromise = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1210
|
+
state: "on",
|
|
1211
|
+
});
|
|
1212
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1213
|
+
await updatePromise;
|
|
1214
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1215
|
+
|
|
1216
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
1217
|
+
});
|
|
1218
|
+
});
|
|
1219
|
+
vi.useRealTimers();
|
|
1220
|
+
});
|
|
1221
|
+
|
|
1222
|
+
it("handles multiple onStateFor listeners on same entity", async () => {
|
|
1223
|
+
expect.assertions(2);
|
|
1224
|
+
vi.useFakeTimers();
|
|
1225
|
+
await hassTestRunner.run(({ lifecycle, hass, mock_assistant, context }) => {
|
|
1226
|
+
lifecycle.onReady(async () => {
|
|
1227
|
+
const entity = hass.refBy.id("sensor.magic");
|
|
1228
|
+
const callback1 = vi.fn();
|
|
1229
|
+
const callback2 = vi.fn();
|
|
1230
|
+
|
|
1231
|
+
entity.onStateFor({
|
|
1232
|
+
context,
|
|
1233
|
+
exec: callback1,
|
|
1234
|
+
for: "0.1s",
|
|
1235
|
+
state: "on",
|
|
1236
|
+
});
|
|
1237
|
+
|
|
1238
|
+
entity.onStateFor({
|
|
1239
|
+
context,
|
|
1240
|
+
exec: callback2,
|
|
1241
|
+
for: ".15s",
|
|
1242
|
+
state: "on",
|
|
1243
|
+
});
|
|
1244
|
+
|
|
1245
|
+
const updatePromise = mock_assistant.events.emitEntityUpdate("sensor.magic", {
|
|
1246
|
+
state: "on",
|
|
1247
|
+
});
|
|
1248
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1249
|
+
await updatePromise;
|
|
1250
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
1251
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
1252
|
+
|
|
1253
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
1254
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
1255
|
+
});
|
|
1256
|
+
});
|
|
1257
|
+
vi.useRealTimers();
|
|
1258
|
+
});
|
|
299
1259
|
});
|
|
300
1260
|
});
|
|
301
1261
|
});
|