Skip to content

Commit f234fd9

Browse files
committed
feat(core): add an utils waitForSignal
Add a utils function `waitForSignal` that is helping to wait the value of the signal to fulfill a condition provided in parameter (or be truthly).
1 parent 2419060 commit f234fd9

File tree

2 files changed

+140
-0
lines changed

2 files changed

+140
-0
lines changed

packages/core/test/async_spec.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Injector, signal} from '../public_api';
10+
import {TestBed, waitForSignal} from '../testing/public_api';
11+
12+
describe('async', () => {
13+
it('should resolve directly if the signal value is truthy', async () => {
14+
const sig = signal(true);
15+
const result = waitForSignal(sig);
16+
await expectAsync(result).toBeResolvedTo(true);
17+
const sig2 = signal('truthy');
18+
const result2 = waitForSignal(sig2);
19+
await expectAsync(result2).toBeResolvedTo('truthy');
20+
});
21+
22+
it('should be rejected after timeout millisecond', async () => {
23+
const sig = signal(true);
24+
const testFn = (value: boolean) => {
25+
return value === false;
26+
};
27+
const result = waitForSignal(sig, testFn, {timeout: 100});
28+
await expectAsync(result).toBeRejected();
29+
});
30+
31+
it('should be resolve correctly when the signal reach the correct value', async () => {
32+
const sig = signal(true);
33+
const testFn = (value: boolean) => {
34+
return value === false;
35+
};
36+
const result = waitForSignal(sig, testFn, {timeout: 1000});
37+
setTimeout(() => {
38+
sig.set(false);
39+
}, 500);
40+
41+
await expectAsync(result).toBeResolvedTo(false);
42+
});
43+
44+
it('should be rejected when signal is destroyed', async () => {
45+
const sig = signal(false);
46+
let result: Promise<boolean>;
47+
48+
const inj = Injector.create({
49+
providers: [],
50+
parent: TestBed.inject(Injector),
51+
});
52+
result = waitForSignal(sig, undefined, {timeout: 1000, injector: inj});
53+
setTimeout(() => {
54+
sig.set(true);
55+
}, 500);
56+
inj.destroy();
57+
await expectAsync(result).toBeRejected();
58+
});
59+
});

packages/core/testing/src/async.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.dev/license
77
*/
8+
9+
import {DestroyRef, effect, inject, Injector, Signal} from '../../src/core';
10+
import {TestBedImpl} from './test_bed';
11+
812
/**
913
* Wraps a test function in an asynchronous test zone. The test will automatically
1014
* complete when all asynchronous calls within this zone are done. Can be used
@@ -43,3 +47,80 @@ export function waitForAsync(fn: Function): (done: any) => any {
4347
);
4448
};
4549
}
50+
51+
/**
52+
* Options for `WaitForSignalOptions`.
53+
*
54+
* @publicApi
55+
*/
56+
export interface WaitForSignalOptions {
57+
/**
58+
* The `Injector` to use when creating the underlying `effect` which watches the signal.
59+
*
60+
* If this isn't specified, the TestBed injector will be used
61+
*/
62+
injector?: Injector;
63+
/**
64+
* The timeout value in millisecond
65+
*/
66+
timeout?: number;
67+
}
68+
69+
/**
70+
* Utils to wait for a specific condition fulfilled by a Signal's value.
71+
* By default, it will wait for the signal to be truthy.
72+
* This is useful when you want to wait for a signal to be in a specific state during your unit tests.
73+
*
74+
* @param source The Signal to watch for the test condition.
75+
* @param testFn The function called with the source value each time the source changes. It should return true when the condition expected is fulfilled.
76+
* @param options The options to provide both
77+
* - the injector to use when creating the underlying effect which watches the signal.
78+
*
79+
* If this isn't specified, the TestBed injector will be used
80+
*
81+
* - the timeout value in millisecond. After this time the watcher will be destroyed and the Promise rejected.
82+
* Default value is 10000.
83+
*
84+
* @usageNotes
85+
*
86+
* `waitForSignal` must be called in a test context.
87+
*
88+
* @returns The Promise<T> that will be resolved with the signal value when the testFn is returning true.
89+
*
90+
* @publicApi
91+
*/
92+
export function waitForSignal<T>(
93+
source: Signal<T>,
94+
testFn: (value: T) => boolean = (value) => !!value,
95+
options?: WaitForSignalOptions,
96+
): Promise<T> {
97+
const injector = options?.injector ?? TestBedImpl.INSTANCE.inject(Injector);
98+
99+
const promise = new Promise<T>((resolve, reject) => {
100+
const watcher = effect(
101+
() => {
102+
let value: T;
103+
try {
104+
value = source();
105+
if (testFn(value)) {
106+
resolve(value);
107+
cleanUpFn();
108+
}
109+
} catch (err) {}
110+
},
111+
{injector, manualCleanup: true},
112+
);
113+
let cleanUpFn = () => {
114+
reject();
115+
clearTimeout(overallTimeoutTimer);
116+
watcher.destroy();
117+
destroyFn();
118+
};
119+
120+
const overallTimeoutTimer = setTimeout(cleanUpFn, options?.timeout ?? 10000);
121+
122+
const destroyFn = injector.get(DestroyRef).onDestroy(cleanUpFn);
123+
});
124+
125+
return promise;
126+
}

0 commit comments

Comments
 (0)