Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions modules/signals/spec/deep-computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,67 @@ describe('deepComputed', () => {
expect(result.count.value()).toBe(2);
});

it('creates a deep computed signal when computation result is a union of objects', () => {
const source = signal<{ s: 'asdf' } | { m: { s: string } }>({
m: { s: 't' },
});
const result = deepComputed(() => source());

expect('m' in result).toBe(true);
expect('m' in result && result.m()).toEqual({ s: 't' });
expect('m' in result && result.m.s()).toBe('t');

source.set({ s: 'asdf' });

expect('m' in result).toBe(false);
expect('s' in result).toBe(true);
expect('s' in result && result.s()).toBe('asdf');

source.set({ m: { s: 'ngrx' } });

expect('s' in result).toBe(false);
expect('m' in result).toBe(true);
expect('m' in result && result.m()).toEqual({ s: 'ngrx' });
expect('m' in result && result.m.s()).toBe('ngrx');
});

it('creates a deep computed signal when computation result is a union of nested objects', () => {
const source = signal<{ a: { b: number } } | { c: { d: string } }>({
a: { b: 1 },
});
const result = deepComputed(() => source());

expect('a' in result).toBe(true);
expect('a' in result && result.a()).toEqual({ b: 1 });
expect('a' in result && result.a.b()).toBe(1);

source.set({ c: { d: 't' } });

expect('a' in result).toBe(false);
expect('c' in result).toBe(true);
expect('c' in result && result.c()).toEqual({ d: 't' });
expect('c' in result && result.c.d()).toBe('t');
});

it('creates a deep computed signal when computation result is a union of an object, a primitive, and null', () => {
const source = signal<{ m: { s: string } } | number | null>(null);
const result = deepComputed(() => source());

expect('m' in result).toBe(false);
expect(result()).toBe(null);

source.set(1);

expect('m' in result).toBe(false);
expect(result()).toBe(1);

source.set({ m: { s: 'ngrx' } });

expect('m' in result).toBe(true);
expect('m' in result && result.m()).toEqual({ s: 'ngrx' });
expect('m' in result && result.m.s()).toBe('ngrx');
});

it('does not create a deep computed signal when computation result is an array', () => {
const source = signal(0);
const result = deepComputed(() => [{ value: source() + 1 }]);
Expand All @@ -29,4 +90,19 @@ describe('deepComputed', () => {
expect(result()).toEqual([{ value: 1 }]);
expect((result as any)[0]).toBe(undefined);
});

it('does not create a deep computed signal when computation result is a primitive, null, or undefined', () => {
const num = deepComputed(() => 1);
const nul = deepComputed(() => null);
const und = deepComputed(() => undefined);

expect(isSignal(num)).toBe(true);
expect(num()).toBe(1);

expect(isSignal(nul)).toBe(true);
expect(nul()).toBe(null);

expect(isSignal(und)).toBe(true);
expect(und()).toBe(undefined);
});
});
47 changes: 47 additions & 0 deletions modules/signals/spec/deep-signal.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,53 @@ describe('toDeepSignal', () => {
expect('m' in deepSig && deepSig.m.s()).toBe('ngrx');
});

it('creates a deep signal on the fly for a union of an object and a primitive', () => {
const sig = signal<{ m: { s: string } } | number>(1);
const deepSig = toDeepSignal(sig);

expect('m' in deepSig).toBe(false);
expect(deepSig()).toBe(1);

sig.set({ m: { s: 'ngrx' } });

expect('m' in deepSig).toBe(true);
expect('m' in deepSig && deepSig.m()).toEqual({ s: 'ngrx' });
expect('m' in deepSig && deepSig.m.s()).toBe('ngrx');
});

it('creates a deep signal on the fly for a union of an object and undefined', () => {
const sig = signal<{ m: { s: string } } | undefined>(undefined);
const deepSig = toDeepSignal(sig);

expect('m' in deepSig).toBe(false);
expect(deepSig()).toBe(undefined);

sig.set({ m: { s: 'ngrx' } });

expect('m' in deepSig).toBe(true);
expect('m' in deepSig && deepSig.m()).toEqual({ s: 'ngrx' });
expect('m' in deepSig && deepSig.m.s()).toBe('ngrx');
});

it('creates a deep signal on the fly for a union of an object, a primitive, and null', () => {
const sig = signal<{ m: { s: string } } | number | null>(null);
const deepSig = toDeepSignal(sig);

expect('m' in deepSig).toBe(false);
expect(deepSig()).toBe(null);

sig.set({ m: { s: 'ngrx' } });

expect('m' in deepSig).toBe(true);
expect('m' in deepSig && deepSig.m()).toEqual({ s: 'ngrx' });
expect('m' in deepSig && deepSig.m.s()).toBe('ngrx');

sig.set(1);

expect('m' in deepSig).toBe(false);
expect(deepSig()).toBe(1);
});

it('does not affect signals with primitives as values', () => {
const num = signal(0);
const str = signal('str');
Expand Down
26 changes: 23 additions & 3 deletions modules/signals/spec/signal-state.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { computed, effect, isSignal } from '@angular/core';
import { computed, effect, isSignal, WritableSignal } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { patchState, signalState } from '../src';
import { SignalsDictionary } from '../src/signal-store-models';
import { STATE_SOURCE } from '../src/state-source';

vi.mock('@angular/core', { spy: true });
Expand All @@ -23,7 +22,9 @@ describe('signalState', () => {

it('creates its properties as Signals', () => {
const state = signalState({ foo: 'bar' });
const stateSource: SignalsDictionary = state[STATE_SOURCE];
const stateSource: Record<string | symbol, WritableSignal<unknown>> = state[
STATE_SOURCE
];

expect(isSignal(state)).toBe(true);
for (const key of Reflect.ownKeys(stateSource)) {
Expand All @@ -38,6 +39,25 @@ describe('signalState', () => {
expect(state()).toEqual(initialState);
});

it('supports a state slice that is a union of an object, null, and a primitive', () => {
const state = signalState<{ slice: { s: string } | null | string }>({
slice: { s: 'ngrx' },
});

expect(isSignal(state.slice)).toBe(true);
expect(state.slice()).toEqual({ s: 'ngrx' });
expect('s' in state.slice).toBe(true);
expect('s' in state.slice && state.slice.s()).toBe('ngrx');

patchState(state, { slice: null });
expect(state.slice()).toBe(null);
expect('s' in state.slice).toBe(false);

patchState(state, { slice: 'signals' });
expect(state.slice()).toBe('signals');
expect('s' in state.slice).toBe(false);
});

it('creates signals for nested state slices', () => {
const state = signalState(initialState);

Expand Down
23 changes: 23 additions & 0 deletions modules/signals/spec/signal-store.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,29 @@ describe('signalStore', () => {
expect(store.x.y.z()).toBe(10);
});

it('supports a state slice that is a union of an object, undefined, and a primitive', () => {
const Store = signalStore(
{ protectedState: false },
withState<{ slice: { n: number } | undefined | number }>({
slice: { n: 1 },
})
);
const store = new Store();

expect(isSignal(store.slice)).toBe(true);
expect(store.slice()).toEqual({ n: 1 });
expect('n' in store.slice).toBe(true);
expect('n' in store.slice && store.slice.n()).toBe(1);

patchState(store, { slice: undefined });
expect(store.slice()).toBe(undefined);
expect('n' in store.slice).toBe(false);

patchState(store, { slice: 42 });
expect(store.slice()).toBe(42);
expect('n' in store.slice).toBe(false);
});

it('overrides Function properties if nested state keys have the same name', () => {
const Store = signalStore(
withState({ name: { length: { name: false } } })
Expand Down
71 changes: 68 additions & 3 deletions modules/signals/spec/types/signal-state.types.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ describe('signalState', () => {
result.toInfer('baz', 'Signal<number | undefined> | undefined');
result.toInfer(
'x',
'Signal<{ y: { z?: boolean | undefined; }; } | undefined> | undefined'
'DeepSignal<{ y: { z?: boolean | undefined; }; }> | Signal<undefined> | undefined'
);
});

Expand Down Expand Up @@ -290,14 +290,79 @@ describe('signalState', () => {

const result = expectSnippet(snippet);
result.toInfer('state', 'SignalState<State>');
result.toInfer('foo', 'Signal<number | { s: string; }>');
result.toInfer('foo', 'DeepSignal<{ s: string; }> | Signal<number>');
result.toInfer('bar', 'DeepSignal<{ baz: { n: number; } | null; }>');
result.toInfer('baz', 'Signal<{ n: number; } | null>');
result.toInfer('baz', 'DeepSignal<{ n: number; }> | Signal<null>');
result.toInfer('x', 'DeepSignal<{ y: { z: boolean | undefined; }; }>');
result.toInfer('y', 'DeepSignal<{ z: boolean | undefined; }>');
result.toInfer('z', 'Signal<boolean | undefined>');
});

it('does not split non-record union members into separate signals', () => {
const snippet = `
type FooBar = 'foo' | 'bar';
type State = {
flag: boolean;
status: FooBar;
mixed: string | number;
withRecord: { id: number } | boolean;
combo: { a: string } | FooBar | null;
multi: { a: number } | { b: string } | boolean;
withCollection: { id: number } | Set<number> | boolean;
nested: { foo: boolean | { deep: string } };
};

const state = signalState<State>({
flag: true,
status: 'foo',
mixed: 1,
withRecord: { id: 1 },
combo: null,
multi: true,
withCollection: { id: 1 },
nested: { foo: true },
});
const flag = state.flag;
const status = state.status;
const mixed = state.mixed;
const withRecord = state.withRecord;
const combo = state.combo;
const multi = state.multi;
const withCollection = state.withCollection;
const nested = state.nested;
const nestedFoo = state.nested.foo;
`;

const result = expectSnippet(snippet);
result.toInfer('flag', 'Signal<boolean>');
result.toInfer('status', 'Signal<FooBar>');
result.toInfer('mixed', 'Signal<string | number>');
result.toInfer(
'withRecord',
'Signal<boolean> | DeepSignal<{ id: number; }>'
);
result.toInfer(
'combo',
'DeepSignal<{ a: string; }> | Signal<FooBar | null>'
);
result.toInfer(
'multi',
'Signal<boolean> | DeepSignal<{ a: number; }> | DeepSignal<{ b: string; }>'
);
result.toInfer(
'withCollection',
'DeepSignal<{ id: number; }> | Signal<boolean | Set<number>>'
);
result.toInfer(
'nested',
'DeepSignal<{ foo: boolean | { deep: string; }; }>'
);
result.toInfer(
'nestedFoo',
'Signal<boolean> | DeepSignal<{ deep: string; }>'
);
});

it('succeeds when state contains Function properties', () => {
const snippet = `
const state1 = signalState({ name: 0 });
Expand Down
Loading
Loading