Skip to content

Commit 02320b3

Browse files
fix(signals): enable withProps to handle Symbols (#4656)
Closes #4655
1 parent 1f5ec46 commit 02320b3

13 files changed

+179
-26
lines changed

modules/signals/spec/deep-freeze.spec.ts

+40-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { getState, patchState } from '../src/state-source';
2-
import { signalState } from '../src/signal-state';
3-
import { signalStore } from '../src/signal-store';
41
import { TestBed } from '@angular/core/testing';
5-
import { withState } from '../src/with-state';
2+
import {
3+
getState,
4+
patchState,
5+
signalState,
6+
signalStore,
7+
withState,
8+
} from '../src';
69

710
describe('deepFreeze', () => {
11+
const SECRET = Symbol('secret');
12+
813
const initialState = {
914
user: {
1015
firstName: 'John',
@@ -13,6 +18,13 @@ describe('deepFreeze', () => {
1318
foo: 'bar',
1419
numbers: [1, 2, 3],
1520
ngrx: 'signals',
21+
nestedSymbol: {
22+
[SECRET]: 'another secret',
23+
},
24+
[SECRET]: {
25+
code: 'secret',
26+
value: '123',
27+
},
1628
};
1729

1830
for (const { stateFactory, name } of [
@@ -52,6 +64,7 @@ describe('deepFreeze', () => {
5264
"Cannot assign to read only property 'firstName' of object"
5365
);
5466
});
67+
5568
describe('mutable changes outside of patchState', () => {
5669
it('throws on reassigned a property of the exposed state', () => {
5770
const state = stateFactory();
@@ -71,7 +84,7 @@ describe('deepFreeze', () => {
7184
);
7285
});
7386

74-
it('throws when mutable change happens for', () => {
87+
it('throws when mutable change happens', () => {
7588
const state = stateFactory();
7689
const s = { user: { firstName: 'M', lastName: 'S' } };
7790
patchState(state, s);
@@ -82,6 +95,28 @@ describe('deepFreeze', () => {
8295
"Cannot assign to read only property 'firstName' of object"
8396
);
8497
});
98+
99+
it('throws when mutable change of root symbol property happens', () => {
100+
const state = stateFactory();
101+
const s = getState(state);
102+
103+
expect(() => {
104+
s[SECRET].code = 'mutable change';
105+
}).toThrowError(
106+
"Cannot assign to read only property 'code' of object"
107+
);
108+
});
109+
110+
it('throws when mutable change of nested symbol property happens', () => {
111+
const state = stateFactory();
112+
const s = getState(state);
113+
114+
expect(() => {
115+
s.nestedSymbol[SECRET] = 'mutable change';
116+
}).toThrowError(
117+
"Cannot assign to read only property 'Symbol(secret)' of object"
118+
);
119+
});
85120
});
86121
});
87122
}

modules/signals/spec/signal-store.spec.ts

+85-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { inject, InjectionToken, isSignal, signal } from '@angular/core';
1+
import {
2+
computed,
3+
inject,
4+
InjectionToken,
5+
isSignal,
6+
signal,
7+
} from '@angular/core';
28
import { TestBed } from '@angular/core/testing';
39
import {
410
patchState,
@@ -145,6 +151,14 @@ describe('signalStore', () => {
145151

146152
expect(store.foo()).toBe('foo');
147153
});
154+
155+
it('allows symbols as state keys', () => {
156+
const SECRET = Symbol('SECRET');
157+
const Store = signalStore(withState({ [SECRET]: 'bar' }));
158+
const store = new Store();
159+
160+
expect(store[SECRET]()).toBe('bar');
161+
});
148162
});
149163

150164
describe('withProps', () => {
@@ -183,6 +197,26 @@ describe('signalStore', () => {
183197

184198
expect(store.foo).toBe('bar');
185199
});
200+
201+
it('allows symbols as property keys', () => {
202+
const SECRET = Symbol('SECRET');
203+
204+
const Store = signalStore(withProps(() => ({ [SECRET]: 'secret' })));
205+
const store = TestBed.configureTestingModule({
206+
providers: [Store],
207+
}).inject(Store);
208+
209+
expect(store[SECRET]).toBe('secret');
210+
});
211+
212+
it('allows numbers as property keys', () => {
213+
const Store = signalStore(withProps(() => ({ 1: 'Number One' })));
214+
const store = TestBed.configureTestingModule({
215+
providers: [Store],
216+
}).inject(Store);
217+
218+
expect(store[1]).toBe('Number One');
219+
});
186220
});
187221

188222
describe('withComputed', () => {
@@ -221,6 +255,20 @@ describe('signalStore', () => {
221255

222256
expect(store.bar()).toBe('bar');
223257
});
258+
259+
it('allows symbols as computed keys', () => {
260+
const SECRET = Symbol('SECRET');
261+
const SecretStore = signalStore(
262+
{ providedIn: 'root' },
263+
withComputed(() => ({
264+
[SECRET]: computed(() => 'secret'),
265+
}))
266+
);
267+
268+
const secretStore = TestBed.inject(SecretStore);
269+
270+
expect(secretStore[SECRET]()).toBe('secret');
271+
});
224272
});
225273

226274
describe('withMethods', () => {
@@ -263,6 +311,19 @@ describe('signalStore', () => {
263311

264312
expect(store.baz()).toBe('baz');
265313
});
314+
315+
it('allows symbols as method keys', () => {
316+
const SECRET = Symbol('SECRET');
317+
const SecretStore = signalStore(
318+
{ providedIn: 'root' },
319+
withMethods(() => ({
320+
[SECRET]: () => 'my secret',
321+
}))
322+
);
323+
const secretStore = TestBed.inject(SecretStore);
324+
325+
expect(secretStore[SECRET]()).toBe('my secret');
326+
});
266327
});
267328

268329
describe('withHooks', () => {
@@ -455,5 +516,28 @@ describe('signalStore', () => {
455516
],
456517
]);
457518
});
519+
520+
it('passes on a symbol key to the features', () => {
521+
const SECRET = Symbol('SECRET');
522+
const SecretStore = signalStore(
523+
withProps(() => ({
524+
[SECRET]: 'not your business',
525+
})),
526+
withComputed((store) => ({
527+
secret: computed(() => store[SECRET]),
528+
})),
529+
withMethods((store) => ({
530+
reveil() {
531+
return store[SECRET];
532+
},
533+
}))
534+
);
535+
536+
const secretStore = new SecretStore();
537+
538+
expect(secretStore.reveil()).toBe('not your business');
539+
expect(secretStore.secret()).toBe('not your business');
540+
expect(secretStore[SECRET]).toBe('not your business');
541+
});
458542
});
459543
});

modules/signals/spec/state-source.spec.ts

+10
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
import { STATE_SOURCE } from '../src/state-source';
1919
import { createLocalService } from './helpers';
2020

21+
const SECRET = Symbol('SECRET');
22+
2123
describe('StateSource', () => {
2224
const initialState = {
2325
user: {
@@ -27,6 +29,7 @@ describe('StateSource', () => {
2729
foo: 'bar',
2830
numbers: [1, 2, 3],
2931
ngrx: 'signals',
32+
[SECRET]: 'secret',
3033
};
3134

3235
describe('patchState', () => {
@@ -78,6 +81,13 @@ describe('StateSource', () => {
7881
});
7982
});
8083

84+
it('patches state slice with symbol key', () => {
85+
const state = stateFactory();
86+
87+
patchState(state, { [SECRET]: 'another secret' });
88+
expect(state[SECRET]()).toBe('another secret');
89+
});
90+
8191
it('patches state via sequence of partial state objects and updater functions', () => {
8292
const state = stateFactory();
8393

modules/signals/spec/with-computed.spec.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ describe('withComputed', () => {
1919
});
2020

2121
it('logs warning if previously defined signal store members have the same name', () => {
22+
const COMPUTED_SECRET = Symbol('computed_secret');
2223
const initialStore = [
2324
withState({
2425
p1: 10,
@@ -27,6 +28,7 @@ describe('withComputed', () => {
2728
withComputed(() => ({
2829
s1: signal('s1').asReadonly(),
2930
s2: signal({ s: 2 }).asReadonly(),
31+
[COMPUTED_SECRET]: signal(1).asReadonly(),
3032
})),
3133
withMethods(() => ({
3234
m1() {},
@@ -43,12 +45,13 @@ describe('withComputed', () => {
4345
m1: signal({ m: 1 }).asReadonly(),
4446
m3: signal({ m: 3 }).asReadonly(),
4547
s3: signal({ s: 3 }).asReadonly(),
48+
[COMPUTED_SECRET]: signal(10).asReadonly(),
4649
}))(initialStore);
4750

4851
expect(console.warn).toHaveBeenCalledWith(
4952
'@ngrx/signals: SignalStore members cannot be overridden.',
5053
'Trying to override:',
51-
'p1, s2, m1'
54+
'p1, s2, m1, Symbol(computed_secret)'
5255
);
5356
});
5457
});

modules/signals/spec/with-methods.spec.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,18 @@ describe('withMethods', () => {
1919
});
2020

2121
it('logs warning if previously defined signal store members have the same name', () => {
22+
const STATE_SECRET = Symbol('state_secret');
23+
const COMPUTED_SECRET = Symbol('computed_secret');
2224
const initialStore = [
2325
withState({
2426
p1: 'p1',
2527
p2: false,
28+
[STATE_SECRET]: 1,
2629
}),
2730
withComputed(() => ({
2831
s1: signal(true).asReadonly(),
2932
s2: signal({ s: 2 }).asReadonly(),
33+
[COMPUTED_SECRET]: signal(1).asReadonly(),
3034
})),
3135
withMethods(() => ({
3236
m1() {},
@@ -43,12 +47,14 @@ describe('withMethods', () => {
4347
s1: () => 100,
4448
m2,
4549
m3: () => 'm3',
50+
[STATE_SECRET]() {},
51+
[COMPUTED_SECRET]() {},
4652
}))(initialStore);
4753

4854
expect(console.warn).toHaveBeenCalledWith(
4955
'@ngrx/signals: SignalStore members cannot be overridden.',
5056
'Trying to override:',
51-
'p2, s1, m2'
57+
'p2, s1, m2, Symbol(state_secret), Symbol(computed_secret)'
5258
);
5359
});
5460
});

modules/signals/spec/with-props.spec.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,13 @@ describe('withProps', () => {
1717
});
1818

1919
it('logs warning if previously defined signal store members have the same name', () => {
20+
const STATE_SECRET = Symbol('state_secret');
21+
const METHOD_SECRET = Symbol('method_secret');
2022
const initialStore = [
2123
withState({
2224
s1: 10,
2325
s2: 's2',
26+
[STATE_SECRET]: 1,
2427
}),
2528
withProps(() => ({
2629
p1: of(100),
@@ -29,6 +32,7 @@ describe('withProps', () => {
2932
withMethods(() => ({
3033
m1() {},
3134
m2() {},
35+
[METHOD_SECRET]() {},
3236
})),
3337
].reduce((acc, feature) => feature(acc), getInitialInnerStore());
3438
vi.spyOn(console, 'warn').mockImplementation();
@@ -39,12 +43,14 @@ describe('withProps', () => {
3943
p2: signal(100),
4044
m1: { ngrx: 'rocks' },
4145
m3: of('m3'),
46+
[STATE_SECRET]: 10,
47+
[METHOD_SECRET]: { x: 'y' },
4248
}))(initialStore);
4349

4450
expect(console.warn).toHaveBeenCalledWith(
4551
'@ngrx/signals: SignalStore members cannot be overridden.',
4652
'Trying to override:',
47-
's1, p2, m1'
53+
's1, p2, m1, Symbol(state_secret), Symbol(method_secret)'
4854
);
4955
});
5056
});

modules/signals/spec/with-state.spec.ts

+7-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ describe('withState', () => {
5555
});
5656

5757
it('logs warning if previously defined signal store members have the same name', () => {
58+
const COMPUTED_SECRET = Symbol('computed_secret');
59+
const METHOD_SECRET = Symbol('method_secret');
5860
const initialStore = [
5961
withState({
6062
p1: 10,
@@ -63,10 +65,12 @@ describe('withState', () => {
6365
withComputed(() => ({
6466
s1: signal('s1').asReadonly(),
6567
s2: signal({ s: 2 }).asReadonly(),
68+
[COMPUTED_SECRET]: signal(1).asReadonly(),
6669
})),
6770
withMethods(() => ({
6871
m1() {},
6972
m2() {},
73+
[METHOD_SECRET]() {},
7074
})),
7175
].reduce((acc, feature) => feature(acc), getInitialInnerStore());
7276
vi.spyOn(console, 'warn').mockImplementation();
@@ -78,12 +82,14 @@ describe('withState', () => {
7882
m: { s: 10 },
7983
m2: { m: 2 },
8084
p3: 'p3',
85+
[COMPUTED_SECRET]: 10,
86+
[METHOD_SECRET]: 100,
8187
}))(initialStore);
8288

8389
expect(console.warn).toHaveBeenCalledWith(
8490
'@ngrx/signals: SignalStore members cannot be overridden.',
8591
'Trying to override:',
86-
'p2, s2, m2'
92+
'p2, s2, m2, Symbol(computed_secret), Symbol(method_secret)'
8793
);
8894
});
8995
});

0 commit comments

Comments
 (0)