Skip to content

Commit 2394f45

Browse files
committed
Test changes with Angular 16
1 parent d9f0661 commit 2394f45

15 files changed

+410
-149
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.2.0",
3+
"version": "1.3.0",
44
"license": "MIT",
55
"description": "Specialized Angular signals to help with frequently encountered situations.",
66
"peerDependencies": {

projects/signal-generators/src/lib/generators/debounce-signal.spec.ts

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { debounceSignal } from './debounce-signal';
44
import { fakeAsync } from '@angular/core/testing';
55
import { tickAndAssertValue } from '../../testing/testing-utilities';
66
import { autoDetectChangesSignal } from '../../testing/signal-testing-utilities';
7-
import { setupGeneralSignalTests } from '../../testing/general-signal-tests.spec';
7+
import { setupComputedAndEffectTests, setupTypeGuardTests } from '../../testing/common-signal-tests.spec';
88

99

1010
describe('debounceSignal', () => {
@@ -16,37 +16,50 @@ describe('debounceSignal', () => {
1616
injector = fixture.componentRef.injector;
1717
});
1818

19-
setupGeneralSignalTests(() => debounceSignal(1, 500, { injector }));
19+
setupTypeGuardTests(() => debounceSignal(1, 500, { injector }));
2020

21-
it('initially shows the source value', fakeAsync(() => {
22-
const source = signal(1);
23-
const debounced = debounceSignal(source, 500, { injector });
24-
expect(debounced()).toBe(source());
25-
}));
21+
describe('when created with a signal', () => {
22+
setupComputedAndEffectTests(() => {
23+
const source = signal(1);
24+
return [debounceSignal(source, 500, { injector }), () => source.set(2)];
25+
}, () => fixture);
26+
27+
it('initially shows the source value', fakeAsync(() => {
28+
const source = signal(1);
29+
const debounced = debounceSignal(source, 500, { injector });
30+
expect(debounced()).toBe(source());
31+
}));
32+
33+
it('should not change value until time of last source change equals debounce time', fakeAsync(() => {
34+
const originalValue = 1;
35+
const source = autoDetectChangesSignal(fixture, signal(originalValue));
36+
const debounced = debounceSignal(source, 500, { injector });
37+
tickAndAssertValue(debounced, [[100, originalValue]]);
38+
source.set(2);
39+
tickAndAssertValue(debounced, [[499, originalValue], [1, source()]]);
40+
source.set(3);
41+
tickAndAssertValue(debounced, [[500, source()]]);
42+
}));
43+
44+
it('should adjust debounce time when time from a signal changes', fakeAsync(() => {
45+
const originalValue = 1;
46+
const debounceTime = autoDetectChangesSignal(fixture, signal(500));
47+
const source = autoDetectChangesSignal(fixture, signal(originalValue));
48+
const debounced = autoDetectChangesSignal(fixture, debounceSignal(source, debounceTime, { injector }));
49+
tickAndAssertValue(debounced, [[100, originalValue]]);
50+
source.set(2);
51+
debounceTime.set(5000);
52+
tickAndAssertValue(debounced, [[500, originalValue], [4500, source()]]);
53+
}));
54+
});
2655

27-
it('should not change value until time of last source change equals debounce time', fakeAsync(() => {
28-
const originalValue = 1;
29-
const source = autoDetectChangesSignal(fixture, signal(originalValue));
30-
const debounced = debounceSignal(source, 500, { injector });
31-
tickAndAssertValue(debounced, [[100, originalValue]]);
32-
source.set(2);
33-
tickAndAssertValue(debounced, [[499, originalValue], [1, source()]]);
34-
source.set(3);
35-
tickAndAssertValue(debounced, [[500, source()]]);
36-
}));
3756

38-
it('should adjust debounce time when time from a signal changes', fakeAsync(() => {
39-
const originalValue = 1;
40-
const debounceTime = autoDetectChangesSignal(fixture, signal(500));
41-
const source = autoDetectChangesSignal(fixture, signal(originalValue));
42-
const debounced = autoDetectChangesSignal(fixture, debounceSignal(source, debounceTime, { injector }));
43-
tickAndAssertValue(debounced, [[100, originalValue]]);
44-
source.set(2);
45-
debounceTime.set(5000);
46-
tickAndAssertValue(debounced, [[500, originalValue], [4500, source()]]);
47-
}));
57+
describe('when created from a value', () => {
58+
setupComputedAndEffectTests(() => {
59+
const sut = debounceSignal(1, 500, { injector });
60+
return [sut, () => sut.set(2)];
61+
}, () => fixture);
4862

49-
describe('when created from writable overload', () => {
5063
it('#set should be debounced', fakeAsync(() => {
5164
const debounced = autoDetectChangesSignal(fixture, debounceSignal('x', 500, { injector }));
5265
tickAndAssertValue(debounced, [[100, 'x']]);

projects/signal-generators/src/lib/generators/extend-signal.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import { computed, signal } from '@angular/core';
22
import { extendSignal } from './extend-signal';
3-
import { setupGeneralSignalTests } from '../../testing/general-signal-tests.spec';
3+
import { setupComputedAndEffectTests, setupTypeGuardTests } from '../../testing/common-signal-tests.spec';
44

55
describe('extendSignal', () => {
6-
setupGeneralSignalTests(() => extendSignal(1, { dummy: () => undefined}));
6+
setupTypeGuardTests(() => extendSignal(1, { dummy: () => undefined}));
7+
8+
describe('for computed and effects', () => {
9+
setupComputedAndEffectTests(() => {
10+
const sut = extendSignal(1, { andOne: (proxy) => proxy.update(y => y + 1) });
11+
return [sut, () => { sut.andOne(); }];
12+
}, null, 'from a value');
13+
setupComputedAndEffectTests(() => {
14+
const source = signal(1);
15+
const sut = extendSignal(source, { andOne: () => source.update(y => y + 1) });
16+
return [sut, () => { sut.andOne(); }];
17+
}, null, 'from a signal');
18+
})
719

820
it('initially returns initial value from a value', () => {
921
const source = extendSignal(1, { dummy: () => undefined});

projects/signal-generators/src/lib/generators/filter-signal.spec.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MockedComponentFixture, MockRender } from 'ng-mocks';
2-
import { setupGeneralSignalTests } from '../../testing/general-signal-tests.spec';
2+
import { setupComputedAndEffectTests, setupTypeGuardTests } from '../../testing/common-signal-tests.spec';
33
import { filterSignal } from './filter-signal';
44
import { autoDetectChangesSignal } from '../../testing/signal-testing-utilities';
55
import { signal } from '@angular/core';
@@ -11,10 +11,16 @@ describe('filterSignal', () => {
1111
fixture = MockRender();
1212
});
1313

14-
setupGeneralSignalTests(() => filterSignal<number>(1, x => x < 5));
14+
setupTypeGuardTests(() => filterSignal<number>(1, x => x < 5));
1515

1616

1717
describe('from value', () => {
18+
setupComputedAndEffectTests(() => {
19+
const sut = filterSignal<number>(1, x => x < 5);
20+
return [sut, () => sut.set(2)];
21+
}, () => fixture);
22+
23+
1824
it('filters values based on a boolean condition', () => {
1925
const sut = filterSignal<number>(1, x => x < 5);
2026
expect(sut()).toBe(1);
@@ -46,6 +52,12 @@ describe('filterSignal', () => {
4652
});
4753

4854
describe('from signal', () => {
55+
setupComputedAndEffectTests(() => {
56+
const source = signal(1);
57+
const sut = filterSignal<number>(source, x => x < 5);
58+
return [sut, () => source.set(2)];
59+
}, () => fixture);
60+
4961
it('filters values based on a boolean condition', () => {
5062
const source = autoDetectChangesSignal(fixture, signal(1));
5163
const sut = filterSignal<number>(source, x => x < 5);
Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,100 @@
11
import { signal } from '@angular/core';
2+
import { setupComputedAndEffectTests, setupTypeGuardTests } from '../../testing/common-signal-tests.spec';
23
import { liftSignal } from './lift-signal';
3-
import { setupGeneralSignalTests } from '../../testing/general-signal-tests.spec';
4+
5+
class DummyClass {
6+
constructor(public value: number) { }
7+
8+
/** for testing mutations */
9+
double(): void {
10+
this.value *= 2;
11+
}
12+
triple(): void {
13+
this.value *= 3;
14+
}
15+
/** for testing updated */
16+
getQuad(): DummyClass {
17+
return new DummyClass(this.value * 4);
18+
}
19+
}
420

521
describe('liftSignal', () => {
6-
setupGeneralSignalTests(() => liftSignal([1, 2, 3], []));
22+
setupTypeGuardTests(() => liftSignal([1, 2, 3], []));
723

824
it('initially returns the initial value', () => {
925
const src = liftSignal([1, 2, 3], []);
1026
expect(src()).toEqual([1, 2, 3]);
1127
});
1228

1329
[
14-
{ factory: () => [1, 2, 3], label: 'object' },
15-
{ factory: () => signal([1, 2, 3]), label: 'signal' }
30+
{ factory: () => new DummyClass(5), label: 'object' },
31+
{ factory: () => signal(new DummyClass(5)), label: 'object signal' }
1632
].forEach(({ factory, label }) => {
17-
it(`adds methods from a passed ${label} that mutate the value when called`, () => {
18-
const src = liftSignal(factory(), ['push', 'pop', 'shift']);
19-
src.push(4);
20-
expect(src()).toEqual([1, 2, 3, 4]);
21-
src.pop();
22-
expect(src()).toEqual([1, 2, 3]);
23-
src.shift();
24-
expect(src()).toEqual([2, 3]);
33+
describe('mutators', () => {
34+
setupComputedAndEffectTests(() => {
35+
const sut = liftSignal(factory(), null, ['double']);
36+
return [sut, () => { sut.double(); }];
37+
});
38+
39+
it(`adds methods from a passed ${label} that mutate the value when called`, () => {
40+
const src = liftSignal(factory(), null, ['double', 'triple']);
41+
src.double()
42+
expect(src()).toEqual(new DummyClass(10));
43+
src.triple()
44+
expect(src()).toEqual(new DummyClass(30));
45+
});
2546
});
26-
it(`adds methods from a passed ${label} that update the value when called`, () => {
27-
const src = liftSignal(factory(), null, ['filter']);
28-
src.filter(x => x === 2);
29-
expect(src()).toEqual([2]);
47+
describe('updaters', () => {
48+
49+
setupComputedAndEffectTests(() => {
50+
const sut = liftSignal(factory(), ['getQuad']);
51+
return [sut, () => { sut.getQuad(); }];
52+
});
53+
54+
it(`adds methods from a passed ${label} that update the value when called`, () => {
55+
const src = liftSignal(factory(), ['getQuad']);
56+
src.getQuad();
57+
expect(src()).toEqual(new DummyClass(20));
58+
});
3059
});
60+
61+
});
62+
63+
[
64+
{ factory: () => [1, 2, 3], label: 'array' },
65+
{ factory: () => signal([1, 2, 3]), label: 'array signal' }
66+
].forEach(({ factory, label }) => {
67+
describe('mutators', () => {
68+
setupComputedAndEffectTests(() => {
69+
const sut = liftSignal(factory(), null, ['push']);
70+
return [sut, () => { sut.push(5); }];
71+
});
72+
73+
it(`adds methods from a passed ${label} that mutate the value when called`, () => {
74+
const src = liftSignal(factory(), ['concat'], ['push', 'pop', 'shift']);
75+
src.push(4);
76+
expect(src()).toEqual([1, 2, 3, 4]);
77+
src.pop();
78+
expect(src()).toEqual([1, 2, 3]);
79+
src.shift();
80+
expect(src()).toEqual([2, 3]);
81+
src.concat([4, 5]);
82+
expect(src()).toEqual([2, 3, 4, 5]);
83+
});
84+
});
85+
describe('updaters', () => {
86+
setupComputedAndEffectTests(() => {
87+
const sut = liftSignal(factory(), ['concat']);
88+
return [sut, () => { sut.concat([5]); }];
89+
});
90+
91+
it(`adds methods from a passed ${label} that update the value when called`, () => {
92+
const src = liftSignal(factory(), ['filter']);
93+
src.filter(x => x === 2);
94+
expect(src()).toEqual([2]);
95+
});
96+
});
97+
3198
});
3299

33100
});
Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11

22
import { Signal, WritableSignal, signal } from '@angular/core';
3-
import { coerceSignal } from '../internal/signal-coercion';
4-
import { isSignalInput } from '../internal/signal-input-utilities';
3+
import { SIGNAL } from '@angular/core/primitives/signals';
54

65
/* eslint-disable @typescript-eslint/no-explicit-any */
76
export type MethodKey<T> = keyof { [K in keyof T as T[K] extends (...args: any[]) => unknown ? K : never] : K } & keyof T;
@@ -18,7 +17,7 @@ export type BoundMethods<T, K extends readonly (MethodKey<T> | UpdaterKey<T>)[]
1817
* Lifts methods from the signal's value to the signal itself.
1918
* @example
2019
* ```ts
21-
* const awesomeArray = liftSignal([1, 2, 3, 4], ['push', 'pop'], ['filter']);
20+
* const awesomeArray = liftSignal([1, 2, 3, 4], ['filter'], ['push', 'pop']);
2221
* awesomeArray.push(5);
2322
* console.log(awesomeArray()); //[1, 2, 3, 4, 5];
2423
* awesomeArray.pop();
@@ -27,36 +26,44 @@ export type BoundMethods<T, K extends readonly (MethodKey<T> | UpdaterKey<T>)[]
2726
* console.log(awesomeArray()); //[2, 4];
2827
* ```
2928
* @param valueSource Either a value or a Writable signal.
29+
* @param updaters A tuple that contains the names that will return a new value.
3030
* @param mutators A tuple that contains the names that will modify the signal's value directly.
31-
* @param updaters A tuple that contains the names that will return T.
31+
* To guarantee this will return a new value, structuredClone or object.assign is used to create a brand new object, so used with caution.
3232
* @typeParam T the type of the signal's value as well as the type where the functions are lifted from.
33-
* @typeParam M A tuple that contains the names of methods appropriate for mutating.
3433
* @typeParam U A tuple that contains the names of methods appropriate for updating.
34+
* @typeParam M A tuple that contains the names of methods appropriate for mutating.
3535
*/
3636
export function liftSignal<T extends NonNullable<unknown>,
37-
const M extends readonly MethodKey<T>[] | null | undefined,
38-
const U extends readonly UpdaterKey<T>[] | null | undefined = null>(
37+
const U extends readonly UpdaterKey<T>[] | null | undefined,
38+
const M extends readonly MethodKey<T>[] | null | undefined = null>(
3939
valueSource: Exclude<T, Signal<unknown>> | WritableSignal<T>,
40-
mutators: M,
41-
updaters?: U):
40+
updaters: U,
41+
mutators?: M
42+
):
4243
WritableSignal<T> & BoundMethods<T, M> & BoundMethods<T, U> {
4344

44-
const output = isSignalInput(valueSource)
45-
? coerceSignal(valueSource as WritableSignal<T>)
45+
const output = SIGNAL in valueSource
46+
? valueSource
4647
: signal(valueSource);
4748

4849
const boundMethods: Partial<BoundMethodsStrict<T, NonNullable<M>> & BoundMethodsStrict<T, NonNullable<U>>> = {};
4950

50-
mutators?.forEach((cur) => {
51-
boundMethods[cur] = (...args) => output.update(x => {
52-
(x[cur] as MethodKeyFn<typeof x, typeof cur>)(...args);
53-
return x;
54-
});
55-
});
56-
5751
updaters?.forEach((cur) => {
5852
boundMethods[cur] = (...args) => output.update(x => (x[cur] as UpdaterKeyFn<typeof x, typeof cur>)(...args));
5953
});
6054

55+
if (mutators) {
56+
const cloneFn = Array.isArray(output())
57+
? (x: T) => structuredClone(x)
58+
: (x: T) => Object.assign(Object.create(Object.getPrototypeOf(x)), x);
59+
mutators?.forEach((cur) => {
60+
boundMethods[cur] = (...args) => output.update(x => {
61+
const cloned = cloneFn(x);
62+
(cloned[cur] as MethodKeyFn<typeof x, typeof cur>)(...args);
63+
return cloned;
64+
});
65+
});
66+
}
67+
6168
return Object.assign(output, boundMethods as BoundMethods<T, M> & BoundMethods<T, U>);
6269
}

projects/signal-generators/src/lib/generators/map-signal.spec.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { MockRender, MockedComponentFixture } from 'ng-mocks';
22
import { autoDetectChangesSignal } from '../../testing/signal-testing-utilities';
33
import { mapSignal } from './map-signal';
44
import { WritableSignal, signal } from '@angular/core';
5-
import { setupGeneralSignalTests } from '../../testing/general-signal-tests.spec';
5+
import { setupComputedAndEffectTests, setupTypeGuardTests } from '../../testing/common-signal-tests.spec';
66

77
describe('mapSignal', () => {
88
let fixture: MockedComponentFixture<void, void>;
@@ -11,7 +11,7 @@ describe('mapSignal', () => {
1111
fixture = MockRender();
1212
});
1313

14-
setupGeneralSignalTests(() => mapSignal(1, (x) => x + 1));
14+
setupTypeGuardTests(() => mapSignal(1, (x) => x + 1));
1515

1616
it('throws if not enough parameters are passed', () => {
1717
expect(() => (mapSignal as unknown as (x: number) => void)(1)).toThrow();
@@ -24,6 +24,11 @@ describe('mapSignal', () => {
2424
signal1 = autoDetectChangesSignal(fixture, signal(3));
2525
signal2 = autoDetectChangesSignal(fixture, signal(5));
2626
});
27+
setupComputedAndEffectTests(() => {
28+
const source = signal(1);
29+
const sut = mapSignal(source, x => x + 1);
30+
return [sut, () => { source.set(2) }];
31+
}, () => fixture);
2732
it('the typings are correct for a single signal', () => {
2833
const source = mapSignal(signal1, (a) => a + 1);
2934
expect(source()).toBe(4);
@@ -53,6 +58,10 @@ describe('mapSignal', () => {
5358
});
5459
});
5560
describe('when passed a value', () => {
61+
setupComputedAndEffectTests(() => {
62+
const sut = mapSignal(1, x => x + 1);
63+
return [sut, () => { sut.set(2) }];
64+
}, () => fixture);
5665
it('initially returns mapped value', () => {
5766
const source = autoDetectChangesSignal(fixture, mapSignal(1, (x) => x * 3));
5867
expect(source()).toBe(3);

0 commit comments

Comments
 (0)