reselect-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +473 -0
  3. package/dist/cjs/index.js +6 -0
  4. package/dist/cjs/reselectKit.development.cjs +683 -0
  5. package/dist/cjs/reselectKit.development.cjs.map +1 -0
  6. package/dist/cjs/reselectKit.production.min.cjs +2 -0
  7. package/dist/cjs/reselectKit.production.min.cjs.map +1 -0
  8. package/dist/reselectKit.d.ts +248 -0
  9. package/dist/reselectKit.mjs +678 -0
  10. package/dist/reselectKit.mjs.map +1 -0
  11. package/package.json +70 -0
  12. package/src/__data__/state.ts +97 -0
  13. package/src/__tests__/createAdaptedSelector.test.ts +134 -0
  14. package/src/__tests__/createBoundSelector.test.ts +351 -0
  15. package/src/__tests__/createCachedSequenceSelector.test.ts +24 -0
  16. package/src/__tests__/createCachedStructuredSelector.test.ts +29 -0
  17. package/src/__tests__/createChainSelector.test.ts +404 -0
  18. package/src/__tests__/createEmptySelector.test.ts +99 -0
  19. package/src/__tests__/createPathSelector.test.ts +81 -0
  20. package/src/__tests__/createPropSelector.test.ts +36 -0
  21. package/src/__tests__/createSegmentSelector.test.ts +31 -0
  22. package/src/__tests__/createSequenceSelector.test.ts +62 -0
  23. package/src/_helpers/__tests__/arePathsEqual.test.ts +27 -0
  24. package/src/_helpers/__tests__/getObjectPaths.test.ts +56 -0
  25. package/src/_helpers/__tests__/temporaryAssign.test.ts +59 -0
  26. package/src/_helpers/arePathsEqual.ts +15 -0
  27. package/src/_helpers/defineDynamicSelectorName.ts +10 -0
  28. package/src/_helpers/generateSelectorKey.ts +15 -0
  29. package/src/_helpers/getObjectPaths.ts +13 -0
  30. package/src/_helpers/getSelectorName.ts +7 -0
  31. package/src/_helpers/isCachedSelector.ts +9 -0
  32. package/src/_helpers/isObject.ts +3 -0
  33. package/src/_helpers/stringifyFunction.ts +3 -0
  34. package/src/_helpers/temporaryAssign.ts +80 -0
  35. package/src/_hooks/__tests__/useMemoWith.test.ts +31 -0
  36. package/src/_hooks/__tests__/useSelector.test.ts +87 -0
  37. package/src/_hooks/useMemoWith.ts +11 -0
  38. package/src/_hooks/useSelector.ts +34 -0
  39. package/src/cache/TreeCache.ts +126 -0
  40. package/src/cache/__tests__/TreeCacheObject.test.ts +188 -0
  41. package/src/cache/__tests__/intervalMapCache.test.ts +133 -0
  42. package/src/cache/intervalMapCache.ts +97 -0
  43. package/src/createAdaptedSelector.ts +75 -0
  44. package/src/createBoundSelector.ts +197 -0
  45. package/src/createCachedSequenceSelector.ts +18 -0
  46. package/src/createCachedStructuredSelector.ts +44 -0
  47. package/src/createChainSelector.ts +230 -0
  48. package/src/createEmptySelector.ts +27 -0
  49. package/src/createPathSelector.ts +208 -0
  50. package/src/createPropSelector.ts +28 -0
  51. package/src/createSegmentSelector.ts +6 -0
  52. package/src/createSequenceSelector.ts +15 -0
  53. package/src/createStructuredSelector.ts +3 -0
  54. package/src/debug/__tests__/debug.test.ts +20 -0
  55. package/src/debug/debug.ts +7 -0
  56. package/src/debug/withDebugName.ts +28 -0
  57. package/src/index.ts +26 -0
  58. package/src/keys/__tests__/arrayComposeKeySelectors.test.ts +52 -0
  59. package/src/keys/__tests__/createKeySelectorCreator.test.ts +246 -0
  60. package/src/keys/__tests__/stringComposeKeySelectors.test.ts +52 -0
  61. package/src/keys/arrayComposeKeySelectors.ts +20 -0
  62. package/src/keys/createKeySelectorComposer.ts +68 -0
  63. package/src/keys/createKeySelectorCreator.ts +106 -0
  64. package/src/keys/defaultKeySelector.ts +1 -0
  65. package/src/keys/stringComposeKeySelectors.ts +15 -0
  66. package/src/types.ts +43 -0
  67. package/src/vitest-globals.d.ts +1 -0
@@ -0,0 +1,404 @@
1
+ import { createSelector, setGlobalDevModeChecks } from '@veksa/reselect';
2
+ import { createCachedSelector } from '@veksa/re-reselect';
3
+ import { createChainSelector } from '../createChainSelector';
4
+ import { createBoundSelector } from '../createBoundSelector';
5
+ import { commonState, MessageStateSegment, PersonStateSegment, State } from '../__data__/state';
6
+ import { createPathSelector } from '../createPathSelector';
7
+ import { createPropSelector } from '../createPropSelector';
8
+ import { createKeySelectorCreator } from '../keys/createKeySelectorCreator';
9
+ import { stringComposeKeySelectors } from '../keys/stringComposeKeySelectors';
10
+ import { createSequenceSelector } from '../createSequenceSelector';
11
+ import { NamedParametricSelector, NamedSelector } from '../types';
12
+
13
+ vi.mock('../debug/debug', async (importOriginal) => ({
14
+ ...(await importOriginal<object>()),
15
+ isDebugMode: vi.fn(() => true),
16
+ }));
17
+
18
+ describe('createChainSelector', () => {
19
+ const personsSelector = (state: PersonStateSegment) => {
20
+ return state.persons;
21
+ };
22
+
23
+ const personSelector = createCachedSelector(
24
+ [createPathSelector(personsSelector).data(), createPropSelector<{ id: number }>().id()],
25
+ (data, id) => {
26
+ return data[id];
27
+ },
28
+ )({
29
+ keySelector: createPropSelector<{ id: number }>().id(),
30
+ });
31
+
32
+ const messagesSelector = (state: MessageStateSegment) => {
33
+ return state.messages;
34
+ };
35
+
36
+ const messageSelector = (state: MessageStateSegment, props: { id: number }) => {
37
+ return state.messages.data[props.id];
38
+ };
39
+
40
+ const fullNameSelector = createSelector(
41
+ [personSelector],
42
+ ({ firstName = '', secondName = '' }) => {
43
+ return `${firstName} ${secondName}`;
44
+ },
45
+ );
46
+
47
+ const fullNameCachedSelector = createCachedSelector(
48
+ [personSelector],
49
+ ({ firstName = '', secondName = '' }) => {
50
+ return `${firstName} ${secondName}`;
51
+ },
52
+ )((_state, props) => props.id);
53
+
54
+ afterEach(() => {
55
+ fullNameCachedSelector.clearCache();
56
+ fullNameCachedSelector.resetRecomputations();
57
+ });
58
+
59
+ beforeAll(() => {
60
+ setGlobalDevModeChecks({
61
+ inputStabilityCheck: 'never',
62
+ identityFunctionCheck: 'never',
63
+ });
64
+ });
65
+
66
+ test('should implement simple selector chain', () => {
67
+ const personByMessageIdSelector = createChainSelector(messageSelector)
68
+ .chain((message) => {
69
+ return createBoundSelector(personSelector, {
70
+ id: message.personId,
71
+ });
72
+ })
73
+ .chain((person) => {
74
+ return createBoundSelector(fullNameSelector, {
75
+ id: person.id,
76
+ });
77
+ })
78
+ .build();
79
+
80
+ expect(personByMessageIdSelector(commonState, { id: 100 })).toBe('Marry Poppins');
81
+ expect(personByMessageIdSelector(commonState, { id: 200 })).toBe('Harry Potter');
82
+ });
83
+
84
+ test('should implement simple optional selection', () => {
85
+ const currentMessageSelector = createChainSelector(
86
+ createPathSelector(messagesSelector).currentMessageId(),
87
+ )
88
+ .chain((messageId) => {
89
+ return createBoundSelector(messageSelector, {
90
+ id: messageId,
91
+ });
92
+ })
93
+ .build();
94
+
95
+ expect(currentMessageSelector(commonState).personId).toBe(2);
96
+ expect(createPathSelector(currentMessageSelector).personId()(commonState)).toBe(2);
97
+ });
98
+
99
+ test('should implement path chain selector for name property', () => {
100
+ const personByMessageIdSelector = createChainSelector(
101
+ createPathSelector(personsSelector).currentPersonId(),
102
+ )
103
+ .chain((personId) => {
104
+ return createBoundSelector(personSelector, {
105
+ id: personId,
106
+ });
107
+ })
108
+ .build();
109
+
110
+ expect(createPathSelector(personByMessageIdSelector).firstName()(commonState)).toBe('Marry');
111
+ expect(createPathSelector(personByMessageIdSelector).name()(commonState)).toBe('M Poppins');
112
+ });
113
+
114
+ test('should cached chain callback by result of input selector', () => {
115
+ const selectorStub = () => '';
116
+ const chainMock = vi.fn(() => selectorStub);
117
+
118
+ const someByMessageIdSelector = createChainSelector(messageSelector).chain(chainMock).build();
119
+
120
+ someByMessageIdSelector(commonState, { id: 100 });
121
+ someByMessageIdSelector(commonState, { id: 100 });
122
+ someByMessageIdSelector(commonState, { id: 100 });
123
+
124
+ expect(chainMock).toHaveBeenCalledTimes(1);
125
+
126
+ someByMessageIdSelector(commonState, { id: 200 });
127
+
128
+ expect(chainMock).toHaveBeenCalledTimes(2);
129
+ });
130
+
131
+ test('should not mutate dependencies of chaining selectors', () => {
132
+ const firstPersonSelector = createBoundSelector(personSelector, {
133
+ id: 1,
134
+ });
135
+
136
+ const firstPersonMonadicSelector = createChainSelector(() => '')
137
+ .chain(() => firstPersonSelector)
138
+ .build();
139
+
140
+ const firstDependencies = firstPersonSelector.dependencies;
141
+ firstPersonMonadicSelector(commonState);
142
+ const secondDependencies = firstPersonSelector.dependencies;
143
+
144
+ expect(firstDependencies?.length).toBeDefined();
145
+ expect(firstDependencies?.length).toBe(secondDependencies?.length);
146
+ });
147
+
148
+ test('should not invalidate cache for input parametric selector', () => {
149
+ const chainFn = vi.fn((result: string) => () => result);
150
+ const fullNameProxyCachedSelector = createChainSelector(fullNameCachedSelector)
151
+ .chain(chainFn)
152
+ .build();
153
+
154
+ fullNameProxyCachedSelector(commonState, { id: 1 });
155
+ fullNameProxyCachedSelector(commonState, { id: 2 });
156
+ fullNameProxyCachedSelector(commonState, { id: 1 });
157
+ fullNameProxyCachedSelector(commonState, { id: 2 });
158
+
159
+ expect(fullNameCachedSelector.recomputations()).toBe(2);
160
+ expect(chainFn).toHaveBeenCalledTimes(2);
161
+ });
162
+
163
+ test('should not invalidate cache for input path parametric selector', () => {
164
+ const personCachedSelector = createCachedSelector([personSelector], (person) => {
165
+ return person;
166
+ })((_state, props) => props.id);
167
+
168
+ const chainFn = vi.fn((result?: string) => () => result);
169
+
170
+ const nameProxyCachedSelector = createChainSelector(
171
+ createPathSelector(personCachedSelector).firstName(),
172
+ )
173
+ .chain(chainFn)
174
+ .build();
175
+
176
+ nameProxyCachedSelector(commonState, { id: 1 });
177
+ nameProxyCachedSelector(commonState, { id: 2 });
178
+ nameProxyCachedSelector(commonState, { id: 1 });
179
+ nameProxyCachedSelector(commonState, { id: 2 });
180
+
181
+ expect(chainFn).toHaveBeenCalledTimes(2);
182
+ });
183
+
184
+ test('should not invalidate cached selector with composingKeySelectorCreator', () => {
185
+ const cachedPersonSelector = createCachedSelector(
186
+ [
187
+ (state: State) => state.persons,
188
+ (_state: State, props: { personId: number }) => props.personId,
189
+ ],
190
+ (persons, personId) => {
191
+ return persons.data[personId];
192
+ },
193
+ )({
194
+ keySelector: (_state: State, props: { personId: number }) => props.personId,
195
+ });
196
+
197
+ const cachedMessageSelector = createCachedSelector(
198
+ [
199
+ (state: State) => state.messages.data,
200
+ (_state: State, props: { messageId: number }) => props.messageId,
201
+ ],
202
+ (messages, messageId) => {
203
+ return messages[messageId];
204
+ },
205
+ )({
206
+ keySelector: (_state: State, props: { messageId: number }) => props.messageId,
207
+ });
208
+
209
+ const personByMessageIdSelector = createChainSelector(cachedMessageSelector)
210
+ .chain((message) => {
211
+ return createBoundSelector(cachedPersonSelector, {
212
+ personId: message.personId,
213
+ });
214
+ })
215
+ .build();
216
+
217
+ const fullNameByMessageSelector = createCachedSelector(
218
+ [personByMessageIdSelector],
219
+ ({ firstName = '', secondName = '' }) => {
220
+ return `${firstName} ${secondName}`;
221
+ },
222
+ )({
223
+ keySelectorCreator: createKeySelectorCreator(stringComposeKeySelectors),
224
+ });
225
+
226
+ expect(fullNameByMessageSelector(commonState, { messageId: 100 })).toBe('Marry Poppins');
227
+ expect(fullNameByMessageSelector(commonState, { messageId: 200 })).toBe('Harry Potter');
228
+
229
+ expect(fullNameByMessageSelector(commonState, { messageId: 100 })).toBe('Marry Poppins');
230
+ expect(fullNameByMessageSelector(commonState, { messageId: 200 })).toBe('Harry Potter');
231
+
232
+ expect(fullNameByMessageSelector.recomputations()).toBe(2);
233
+ });
234
+
235
+ test('should build cached selector with few levels of chaining', () => {
236
+ const firstKeySelector = createPropSelector<{
237
+ firstProp: string;
238
+ }>().firstProp();
239
+
240
+ const secondKeySelector = createPropSelector<{
241
+ secondProp: string;
242
+ }>().secondProp();
243
+
244
+ const firstSelector = createCachedSelector(
245
+ firstKeySelector,
246
+ (firstProp) => firstProp,
247
+ )({
248
+ keySelector: firstKeySelector,
249
+ });
250
+
251
+ const secondSelector = createCachedSelector(
252
+ secondKeySelector,
253
+ (secondProp) => secondProp,
254
+ )({
255
+ keySelector: secondKeySelector,
256
+ });
257
+
258
+ const chainFn = vi.fn((result: string) => () => result);
259
+
260
+ const chainSelector = createChainSelector(firstSelector)
261
+ .chain(() => secondSelector)
262
+ .chain(chainFn)
263
+ .build();
264
+
265
+ chainSelector(expect.anything(), {
266
+ firstProp: 'firstValue',
267
+ secondProp: 'firstValue',
268
+ });
269
+
270
+ chainSelector(expect.anything(), {
271
+ firstProp: 'firstValue',
272
+ secondProp: 'secondValue',
273
+ });
274
+
275
+ chainSelector(expect.anything(), {
276
+ firstProp: 'secondValue',
277
+ secondProp: 'firstValue',
278
+ });
279
+
280
+ chainSelector(expect.anything(), {
281
+ firstProp: 'secondValue',
282
+ secondProp: 'secondValue',
283
+ });
284
+
285
+ chainSelector(expect.anything(), {
286
+ firstProp: 'firstValue',
287
+ secondProp: 'firstValue',
288
+ });
289
+
290
+ chainSelector(expect.anything(), {
291
+ firstProp: 'firstValue',
292
+ secondProp: 'secondValue',
293
+ });
294
+
295
+ chainSelector(expect.anything(), {
296
+ firstProp: 'secondValue',
297
+ secondProp: 'firstValue',
298
+ });
299
+
300
+ chainSelector(expect.anything(), {
301
+ firstProp: 'secondValue',
302
+ secondProp: 'secondValue',
303
+ });
304
+
305
+ expect(chainFn).toHaveBeenCalledTimes(4);
306
+ expect(firstSelector.recomputations()).toBe(2);
307
+ expect(secondSelector.recomputations()).toBe(2);
308
+ });
309
+
310
+ test('should implement aggregation for collection by single item selector', () => {
311
+ const personsSelector = (state: State) => state.persons;
312
+
313
+ const longestFullNameSelector = createChainSelector(personsSelector)
314
+ .chain((persons) => {
315
+ return createSequenceSelector(
316
+ Object.values(persons.data).map((person) => {
317
+ return createBoundSelector(fullNameSelector, { id: person.id });
318
+ }),
319
+ );
320
+ })
321
+ .map((fullNames) => {
322
+ return fullNames.reduce((longest, current) => {
323
+ return current.length > longest.length ? current : longest;
324
+ });
325
+ })
326
+ .build();
327
+
328
+ expect(longestFullNameSelector(commonState)).toBe('Marry Poppins');
329
+ });
330
+
331
+ test('should generate selector name', () => {
332
+ const fullNameNamedSelector = createSelector(
333
+ [personSelector],
334
+ ({ firstName = '', secondName = '' }) => {
335
+ return `${firstName} ${secondName}`;
336
+ },
337
+ );
338
+
339
+ (
340
+ fullNameNamedSelector as NamedParametricSelector<unknown, unknown, unknown, unknown>
341
+ ).selectorName = 'fullNameNamedSelector';
342
+
343
+ const personByMessageIdSelector: any = createChainSelector(messageSelector)
344
+ .chain((message) => {
345
+ return createBoundSelector(personSelector, { id: message.personId });
346
+ })
347
+ .chain((person) => {
348
+ return createBoundSelector(fullNameNamedSelector, { id: person.id });
349
+ })
350
+ .build();
351
+
352
+ expect(personByMessageIdSelector.selectorName).toMatchInlineSnapshot(
353
+ `"messageSelector (will be chained (message) => { return __vi_import_3__.createBoundSelector(personSelector, { id: message.personId }); }) (will be chained (person) => { return __vi_import_3__.createBoundSelector(fullNameNamedSelector, { id: person.id }); })"`,
354
+ );
355
+
356
+ personByMessageIdSelector(commonState, { id: 100 });
357
+
358
+ expect(personByMessageIdSelector.selectorName).toMatchInlineSnapshot(
359
+ `"messageSelector (chained by selector (id -> [*])) (chained by fullNameNamedSelector (id -> [*]))"`,
360
+ );
361
+
362
+ const [higherOrderSelector] = personByMessageIdSelector.dependencies as unknown[];
363
+
364
+ expect(
365
+ (higherOrderSelector as NamedSelector<unknown, unknown>).selectorName,
366
+ ).toMatchInlineSnapshot(
367
+ `"higher order for messageSelector (chained by selector (id -> [*])) ((person) => { return __vi_import_3__.createBoundSelector(fullNameNamedSelector, { id: person.id }); })"`,
368
+ );
369
+ });
370
+
371
+ test('should build selector with chainHierarchy property for unit test', () => {
372
+ type FirstState = { firstValue: string };
373
+
374
+ const firstSelector = (state: FirstState) => {
375
+ return state.firstValue;
376
+ };
377
+ const firstChain = () => firstSelector;
378
+
379
+ type SecondState = { secondValue: number };
380
+ type SecondProps = { prop: boolean };
381
+
382
+ const secondSelector = (state: SecondState, props: SecondProps) => {
383
+ return `${state.secondValue} ${String(props.prop)}`;
384
+ };
385
+ const secondChain = () => secondSelector;
386
+
387
+ const someByMessageIdSelector = createChainSelector(messageSelector)
388
+ .chain(firstChain)
389
+ .chain(secondChain)
390
+ .build();
391
+
392
+ expect(someByMessageIdSelector.chainHierarchy).toBeDefined();
393
+
394
+ const lastChain = someByMessageIdSelector.chainHierarchy;
395
+
396
+ expect(lastChain?.('firstValue')).toBe(secondSelector);
397
+ expect(lastChain?.('firstValue')({ secondValue: 2 }, { prop: true })).toBe('2 true');
398
+
399
+ const lastButOneChain = someByMessageIdSelector.chainHierarchy?.parentChain;
400
+
401
+ expect(lastButOneChain?.(expect.anything())).toBe(firstSelector);
402
+ expect(lastButOneChain?.(expect.anything())({ firstValue: 'firstValue' })).toBe('firstValue');
403
+ });
404
+ });
@@ -0,0 +1,99 @@
1
+ import { createCachedSelector } from '@veksa/re-reselect';
2
+ import { createChainSelector } from '../createChainSelector';
3
+ import { createBoundSelector } from '../createBoundSelector';
4
+ import { createEmptySelector } from '../createEmptySelector';
5
+ import { isCachedSelector } from '../_helpers/isCachedSelector';
6
+
7
+ describe('createEmptySelector', () => {
8
+ type Parent = {
9
+ parentId: number;
10
+ };
11
+
12
+ type Child = {
13
+ childId: number;
14
+ parentId?: number;
15
+ };
16
+
17
+ type State = {
18
+ firstGeneration: Record<number, Parent>;
19
+ secondGeneration: Record<number, Child>;
20
+ };
21
+
22
+ const exampleState: State = {
23
+ firstGeneration: {
24
+ 1: { parentId: 1 },
25
+ 2: { parentId: 2 },
26
+ },
27
+ secondGeneration: {
28
+ 101: { childId: 101, parentId: 1 },
29
+ 102: { childId: 102 },
30
+ },
31
+ };
32
+
33
+ const parentSelector = (state: State, props: { parentId: number }) =>
34
+ state.firstGeneration[props.parentId];
35
+
36
+ const childSelector = (state: State, props: { childId: number }) =>
37
+ state.secondGeneration[props.childId];
38
+
39
+ test('should return undefined for optional links', () => {
40
+ const parentByChildSelector = createChainSelector(childSelector)
41
+ .chain(({ parentId }) =>
42
+ parentId !== undefined
43
+ ? createBoundSelector(parentSelector, {
44
+ parentId,
45
+ })
46
+ : createEmptySelector(parentSelector),
47
+ )
48
+ .build();
49
+
50
+ const result1 = parentByChildSelector(exampleState, {
51
+ childId: 101,
52
+ });
53
+
54
+ expect(result1?.parentId).toBe(1);
55
+
56
+ const result2 = parentByChildSelector(exampleState, {
57
+ childId: 102,
58
+ });
59
+
60
+ expect(result2).toBeUndefined();
61
+ });
62
+
63
+ test('should create selector without parameters', () => {
64
+ const emptyParentSelector = createEmptySelector(parentSelector);
65
+
66
+ const result = emptyParentSelector(exampleState);
67
+
68
+ expect(result).toBeUndefined();
69
+ });
70
+
71
+ describe('integration with re-reselect', () => {
72
+ const parentCachedSelector = createCachedSelector(
73
+ [
74
+ (state: State) => state.firstGeneration,
75
+ (_: State, props: { parentId: number }) => props.parentId,
76
+ ],
77
+ (firstGeneration, parentId) => firstGeneration[parentId],
78
+ )({
79
+ keySelector: (_, props) => props.parentId,
80
+ });
81
+
82
+ test('should produce cached selector if input selector is cached', () => {
83
+ const emptyParentCachedSelector = createEmptySelector(parentCachedSelector);
84
+
85
+ expect(isCachedSelector(emptyParentCachedSelector)).toBeTruthy();
86
+
87
+ if (!isCachedSelector(emptyParentCachedSelector)) {
88
+ throw new Error('emptyParentCachedSelector should be cached');
89
+ }
90
+
91
+ const key = emptyParentCachedSelector.keySelector(
92
+ expect.anything(),
93
+ expect.anything(),
94
+ ) as unknown;
95
+
96
+ expect(key).toBeDefined();
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,81 @@
1
+ import { createCachedSelector } from '@veksa/re-reselect';
2
+ import { createPathSelector } from '../createPathSelector';
3
+ import { commonState, Document, State } from '../__data__/state';
4
+ import { isCachedSelector } from '../_helpers/isCachedSelector';
5
+
6
+ describe('createPathSelector', () => {
7
+ test('should implement basic access to state properties', () => {
8
+ const baseSelector = (state: Partial<State>) => state;
9
+ const pathSelector = createPathSelector(baseSelector);
10
+
11
+ type Props = {
12
+ id: number;
13
+ };
14
+
15
+ const baseParametricSelector = (state: State, props: Props): Document | undefined => {
16
+ return state.documents[props.id];
17
+ };
18
+
19
+ const pathParametricSelector = createPathSelector(baseParametricSelector);
20
+
21
+ expect(pathSelector.messages.data[100].text()(commonState)).toEqual('Hello');
22
+ expect(pathSelector.messages.data[300].text()(commonState)).toBeUndefined();
23
+ expect(pathSelector.messages.data[300].text('Default')(commonState)).toBe('Default');
24
+
25
+ expect(pathParametricSelector.messageId()(commonState, { id: 111 })).toEqual(100);
26
+ expect(pathParametricSelector.data[0]()(commonState, { id: 111 })).toEqual(1);
27
+ expect(pathParametricSelector.messageId()(commonState, { id: 333 })).toBeUndefined();
28
+ expect(pathParametricSelector.messageId(100500)(commonState, { id: 333 })).toBe(100500);
29
+ });
30
+
31
+ test('should handle states typed as interface', () => {
32
+ interface IState {
33
+ name: string;
34
+ }
35
+
36
+ const baseSelector = (state: IState) => state;
37
+
38
+ const nameSelector = createPathSelector(baseSelector).name();
39
+
40
+ expect(nameSelector({ name: 'name' })).toBe('name');
41
+ });
42
+
43
+ test('should handle default value for optional param', () => {
44
+ interface IState {
45
+ name?: string;
46
+ }
47
+
48
+ const baseSelector = (state: IState) => state;
49
+
50
+ const nameSelector = createPathSelector(baseSelector).name('default');
51
+
52
+ expect(nameSelector({})).toBe('default');
53
+ });
54
+
55
+ describe('integration with re-reselect', () => {
56
+ test('should expose keySelector if base selector is cached', () => {
57
+ const personSelector = createCachedSelector(
58
+ [
59
+ (state: State) => state.persons,
60
+ (_state: State, props: { personId: number }) => props.personId,
61
+ ],
62
+ (persons, personId) => persons.data[personId],
63
+ )({
64
+ keySelector: (_state: State, props: { personId: number }) => props.personId,
65
+ });
66
+
67
+ const firstNameSelector = createPathSelector(personSelector).firstName();
68
+
69
+ if (!isCachedSelector(firstNameSelector)) {
70
+ throw new Error('firstNameSelector should be cached');
71
+ }
72
+
73
+ const personId = 1;
74
+ const key = firstNameSelector.keySelector(expect.anything(), {
75
+ personId,
76
+ }) as unknown;
77
+
78
+ expect(key).toBe(personId);
79
+ });
80
+ });
81
+ });
@@ -0,0 +1,36 @@
1
+ import { createPropSelector } from '../createPropSelector';
2
+ import { createBoundSelector } from '../createBoundSelector';
3
+
4
+ describe('createPropSelector', () => {
5
+ test('should implement basic access to properties', () => {
6
+ type Props = {
7
+ range?: {
8
+ from?: number;
9
+ to?: number;
10
+ };
11
+ };
12
+ const props: Props = {
13
+ range: {
14
+ from: 30,
15
+ to: 70,
16
+ },
17
+ };
18
+
19
+ const propSelector = createPropSelector<Props>();
20
+
21
+ expect(propSelector.range.from()({}, props)).toEqual(30);
22
+ expect(propSelector.range.to()({}, props)).toEqual(70);
23
+
24
+ expect(propSelector.range.from(0)({}, {})).toEqual(0);
25
+ expect(propSelector.range.to(100)({}, {})).toEqual(100);
26
+ });
27
+
28
+ test('should not be detected as prop selector if wrapped in bound selector', () => {
29
+ const propSelector = createPropSelector<{ value: string }>().value();
30
+ const boundSelector = createBoundSelector(propSelector, {
31
+ value: 'value',
32
+ });
33
+
34
+ expect(boundSelector).not.toHaveProperty('isPropSelector', true);
35
+ });
36
+ });
@@ -0,0 +1,31 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { createSegmentSelector } from '../createSegmentSelector';
3
+
4
+ describe('createSegmentSelector', () => {
5
+ type State = { value?: number | null; nested: { count: number } };
6
+
7
+ test('returns the selected segment when it is present', () => {
8
+ const selector = createSegmentSelector((state: State) => state.nested, { count: 0 });
9
+
10
+ const state: State = { nested: { count: 5 } };
11
+ expect(selector(state)).toBe(state.nested);
12
+ });
13
+
14
+ test('falls back to the initial value when the segment is undefined', () => {
15
+ const selector = createSegmentSelector((state: State) => state.value, 42);
16
+
17
+ expect(selector({ value: undefined, nested: { count: 0 } })).toBe(42);
18
+ });
19
+
20
+ test('falls back to the initial value when the segment is null', () => {
21
+ const selector = createSegmentSelector((state: State) => state.value, 42);
22
+
23
+ expect(selector({ value: null, nested: { count: 0 } })).toBe(42);
24
+ });
25
+
26
+ test('does not fall back for defined but falsy values', () => {
27
+ const selector = createSegmentSelector((state: State) => state.value, 42);
28
+
29
+ expect(selector({ value: 0, nested: { count: 0 } })).toBe(0);
30
+ });
31
+ });