Skip to content

Commit 572cd52

Browse files
committed
Merged 1.4.2 changes for testing. Successful
1 parent 27746c4 commit 572cd52

28 files changed

+477
-105
lines changed

projects/signal-generators/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ddtmm/angular-signal-generators",
3-
"version": "1.4.1",
3+
"version": "1.4.2",
44
"license": "MIT",
55
"description": "Specialized Angular signals to help with frequently encountered situations.",
66
"peerDependencies": {

projects/signal-generators/src/lib/internal/signal-input-utilities.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { isSignal } from '@angular/core';
12
import { SignalInput, ToSignalInput } from '../signal-input';
23

34
export function isSignalInput<T>(obj: SignalInput<T> | T): obj is SignalInput<T>
@@ -8,7 +9,14 @@ export function isSignalInput(obj: unknown): obj is SignalInput<unknown>
89
* @param obj Any type of a value can be checked.
910
*/
1011
export function isSignalInput(obj: unknown): obj is SignalInput<unknown> {
11-
return (obj != null) && (typeof obj === 'function' && obj.length === 0 || isToSignalInput(obj));
12+
return (obj != null) && (isSignal(obj) || isSignalInputFunction(obj) || isToSignalInput(obj));
13+
}
14+
15+
export function isSignalInputFunction<T>(obj: SignalInput<T>): obj is () => T
16+
export function isSignalInputFunction(obj: unknown): obj is () => unknown
17+
/** Is true if obj is a function and it has no arguments. */
18+
export function isSignalInputFunction(obj: unknown): obj is () => unknown {
19+
return typeof obj === 'function' && obj.length === 0 && !isSignal(obj);
1220
}
1321

1422
export function isToSignalInput<T>(obj: SignalInput<T>): obj is ToSignalInput<T>

projects/signal-generators/src/lib/internal/timer-internal.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { fakeAsync, tick } from '@angular/core/testing';
2-
import { tickAndAssertValue } from '../../testing/testing-utilities';
2+
import { tickAndAssertValues } from '../../testing/testing-utilities';
33
import { TimerInternal, TimerInternalOptions, TimerStatus } from './timer-internal';
44

55
describe('timerInternal', () => {
@@ -117,7 +117,7 @@ describe('timerInternal', () => {
117117

118118
/** It is a pretty common pattern in these tests to tick, and then expect a value */
119119
function tickAndAssertTimerValue(timer: TimerInternal, pattern: [elapsedMs: number, expectedTicks: number][]): void {
120-
tickAndAssertValue(() => timer.ticks, pattern);
120+
tickAndAssertValues(() => timer.ticks, pattern);
121121
}
122122
});
123123

projects/signal-generators/src/lib/internal/value-source-utilities.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
import { CreateSignalOptions, Signal, WritableSignal, signal } from '@angular/core';
2+
import { SignalInput } from '../signal-input';
23
import { ValueSource } from '../value-source';
34
import { CoerceSignalOptions, coerceSignal } from './signal-coercion';
45
import { isSignalInput } from './signal-input-utilities';
5-
import { SignalInput } from '../signal-input';
66

7-
/** The signal that is the result of a value created by */
7+
88
export type ValueSourceSignal<V extends ValueSource<unknown>> =
99
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1010
V extends Signal<any> ? V // if value source was already a signal then it is just itself.
1111
: V extends SignalInput<infer T> ? Signal<T> // a plain signal is created from an observable or function returning a value.
1212
: WritableSignal<V>; // if a value was used as a ValueSource then the signal created is a writable signal.
1313

14-
1514
export function valueSourceToSignal<T, V extends ValueSource<T>>(
1615
valueSource: V, options?: CoerceSignalOptions<T> & CreateSignalOptions<T>): ValueSourceSignal<V> {
1716

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { Component, Injector, signal } from '@angular/core';
2+
import { fakeAsync, tick } from '@angular/core/testing';
3+
import { MockBuilder, MockRender, MockedComponentFixture } from 'ng-mocks';
4+
import { BehaviorSubject, startWith, tap, timer } from 'rxjs';
5+
import { setupComputedAndEffectTests, setupTypeGuardTests } from '../../testing/common-signal-tests.spec';
6+
import { autoDetectChangesSignal } from '../../testing/signal-testing-utilities';
7+
import { asyncSignal } from './async-signal';
8+
9+
describe('asyncSignal', () => {
10+
let fixture: MockedComponentFixture<void, void>;
11+
let injector: Injector;
12+
13+
beforeEach(() => {
14+
fixture = MockRender();
15+
injector = fixture.componentRef.injector;
16+
});
17+
18+
setupTypeGuardTests(() => asyncSignal(Promise.resolve(1), { injector }));
19+
20+
describe('for computed and effects', () => {
21+
setupComputedAndEffectTests(
22+
() => {
23+
const sut = asyncSignal(Promise.resolve(1), { injector });
24+
return [sut, () => sut.set(Promise.resolve(2))];
25+
},
26+
null,
27+
'from a value'
28+
);
29+
setupComputedAndEffectTests(
30+
() => {
31+
const source = signal(Promise.resolve(1));
32+
const sut = asyncSignal(source, { injector });
33+
return [sut, () => source.set(Promise.resolve(2))];
34+
},
35+
null,
36+
'from a signal'
37+
);
38+
});
39+
40+
describe('from a value', () => {
41+
it('returns a signal that can be set', fakeAsync(() => {
42+
const sut = autoDetectChangesSignal(fixture, asyncSignal(Promise.resolve(1), { injector }));
43+
sut.set(Promise.resolve(2));
44+
tick();
45+
expect(sut()).toBe(2);
46+
}));
47+
it('returns a signal with readonly method', fakeAsync(() => {
48+
const sut = autoDetectChangesSignal(fixture, asyncSignal(Promise.resolve(1), { injector }));
49+
sut.set(Promise.resolve(2));
50+
tick();
51+
expect(sut.asReadonly()()).toBe(2);
52+
}));
53+
it('returns a signal that can be updated', fakeAsync(() => {
54+
const sut = autoDetectChangesSignal(fixture, asyncSignal(Promise.resolve(1), { injector }));
55+
sut.update((x) => (x instanceof Promise ? Promise.resolve(2) : Promise.resolve(0)));
56+
tick();
57+
expect(sut()).toBe(2);
58+
}));
59+
});
60+
61+
describe('from a signalInput', () => {
62+
it('updates output from a signal', fakeAsync(() => {
63+
const source = autoDetectChangesSignal(fixture, signal(Promise.resolve(1)));
64+
const sut = asyncSignal(source, { injector });
65+
source.set(Promise.resolve(2));
66+
tick();
67+
expect(sut()).toBe(2);
68+
}));
69+
70+
it('updates output from a value that needs coercing', fakeAsync(() => {
71+
const source = autoDetectChangesSignal(fixture, signal(Promise.resolve(1)));
72+
const sut = asyncSignal(() => source(), { injector });
73+
source.set(Promise.resolve(2));
74+
tick();
75+
expect(sut()).toBe(2);
76+
}));
77+
});
78+
79+
describe('execution', () => {
80+
it('creates a signal that initially returns defaultValue if provided in options', fakeAsync(() => {
81+
const sut = autoDetectChangesSignal(fixture, asyncSignal(Promise.resolve(1), { defaultValue: -1, injector }));
82+
expect(sut()).toBe(-1);
83+
tick();
84+
expect(sut()).toBe(1);
85+
}));
86+
87+
it('ignores output from a prior async source value when another one is active', fakeAsync(() => {
88+
const subjectOne = new BehaviorSubject(1);
89+
const subjectTwo = new BehaviorSubject(6);
90+
const sut = autoDetectChangesSignal(fixture, asyncSignal(subjectOne, { injector }));
91+
tick();
92+
expect(sut()).toBe(1);
93+
sut.set(subjectTwo);
94+
expect(sut()).toBe(6);
95+
subjectOne.next(2);
96+
expect(sut()).toBe(6);
97+
subjectTwo.next(7);
98+
expect(sut()).toBe(7);
99+
}));
100+
101+
it('updates if a signal is used inside of the "auto-computed" overload.', fakeAsync(() => {
102+
function fakeFetch(idValue: number): Promise<number> {
103+
return Promise.resolve(idValue + 1);
104+
}
105+
const $id = autoDetectChangesSignal(fixture, signal(1), { tickAfter: true });
106+
const sut = autoDetectChangesSignal(fixture, asyncSignal(() => fakeFetch($id()), { injector }), { tickAfter: true });
107+
expect(sut()).toBe(2);
108+
$id.set(5);
109+
expect(sut()).toBe(6);
110+
}));
111+
});
112+
113+
describe('errors', () => {
114+
it('throws when subscribable async source throws', fakeAsync(() => {
115+
const obs$ = timer(1000).pipe(
116+
tap(() => {
117+
throw new Error();
118+
}),
119+
startWith(6)
120+
);
121+
const sut = autoDetectChangesSignal(fixture, asyncSignal(obs$, { injector }), { tickAfter: true });
122+
expect(sut()).toBe(6);
123+
tick(1000); // get observable to throw error.
124+
expect(() => sut()).toThrowError();
125+
}));
126+
127+
it('throws when PromiseLike async source is rejected', fakeAsync(() => {
128+
let reject: () => void = () => {};
129+
const asyncSource = new Promise<number>((_, r) => {
130+
reject = r;
131+
});
132+
const sut = autoDetectChangesSignal(fixture, asyncSignal(asyncSource, { injector }), { tickAfter: true });
133+
expect(sut()).toBe(undefined);
134+
reject();
135+
tick(); // need to process the reject.
136+
expect(() => sut()).toThrowError();
137+
}));
138+
139+
it('will not update after an error has been thrown', fakeAsync(() => {
140+
let reject: () => void = () => {};
141+
const asyncSource = new Promise<number>((_, r) => {
142+
reject = r;
143+
});
144+
const sut = autoDetectChangesSignal(fixture, asyncSignal(asyncSource, { injector }), { tickAfter: true });
145+
reject(); // this will go undetected.
146+
sut.set(Promise.resolve(5));
147+
expect(() => sut()).toThrowError();
148+
}));
149+
150+
it('will not create a new error after an error has been thrown', fakeAsync(() => {
151+
let reject: (reason: unknown) => void = () => {};
152+
const asyncSource = new Promise<number>((_, r) => (reject = r));
153+
const sut = autoDetectChangesSignal(fixture, asyncSignal(asyncSource, { injector }), { tickAfter: true });
154+
reject('error1'); // there is intentionally no tick after. Use cause to identify
155+
sut.set(Promise.reject('error2'));
156+
expect(() => sut()).toThrowMatching((x) => (x as Error).cause === 'error1');
157+
}));
158+
});
159+
});
160+
161+
/** This needed to be outside the other asyncSignal tests since the only way to test without options was to be in an injection context. */
162+
describe('asyncSignal without options', () => {
163+
it('can be created without options', fakeAsync(() => {
164+
@Component({ standalone: true })
165+
class TestComponent {
166+
sut = asyncSignal(Promise.resolve(1));
167+
}
168+
MockBuilder(TestComponent);
169+
const fixture = MockRender(TestComponent);
170+
expect(fixture.point.componentInstance.sut()).toBe(undefined);
171+
tick();
172+
expect(fixture.point.componentInstance.sut()).toBe(1);
173+
}));
174+
});

0 commit comments

Comments
 (0)