@promoboxx/use-filter 2.0.0 → 2.0.1

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 (84) hide show
  1. package/dist/cjs/_virtual/_rolldown/runtime.js +29 -0
  2. package/dist/cjs/lib/buildDefaultFilterInfo.d.ts +5 -0
  3. package/dist/cjs/lib/buildDefaultFilterInfo.js +27 -0
  4. package/dist/cjs/lib/getOffsetFromPage.d.ts +3 -0
  5. package/dist/cjs/lib/getOffsetFromPage.js +8 -0
  6. package/dist/cjs/lib/getPageFromOffset.d.ts +3 -0
  7. package/dist/cjs/lib/getPageFromOffset.js +8 -0
  8. package/dist/cjs/lib/shallowEqual.d.ts +3 -0
  9. package/dist/cjs/lib/shallowEqual.js +24 -0
  10. package/dist/cjs/store/index.d.ts +14 -0
  11. package/dist/cjs/store/index.js +18 -0
  12. package/dist/cjs/store/localStorageStore.d.ts +5 -0
  13. package/dist/cjs/store/localStorageStore.js +33 -0
  14. package/dist/cjs/store/memoryStore.d.ts +5 -0
  15. package/dist/cjs/store/memoryStore.js +25 -0
  16. package/dist/cjs/store/reduxHelpers/createActions.d.ts +16 -0
  17. package/dist/cjs/store/reduxHelpers/createActions.js +16 -0
  18. package/dist/cjs/store/reduxHelpers/createReducer.d.ts +10 -0
  19. package/dist/cjs/store/reduxHelpers/createReducer.js +25 -0
  20. package/dist/cjs/store/reduxStore.d.ts +18 -0
  21. package/dist/cjs/store/reduxStore.js +63 -0
  22. package/dist/cjs/store/urlParamStore.d.ts +8 -0
  23. package/dist/cjs/store/urlParamStore.js +68 -0
  24. package/dist/cjs/useFilter.d.ts +106 -0
  25. package/dist/cjs/useFilter.js +251 -0
  26. package/dist/cjs/useSimpleFilter.d.ts +89 -0
  27. package/dist/cjs/useSimpleFilter.js +198 -0
  28. package/dist/esm/lib/buildDefaultFilterInfo.d.mts +6 -0
  29. package/dist/esm/lib/buildDefaultFilterInfo.mjs +27 -0
  30. package/dist/esm/lib/getOffsetFromPage.d.mts +4 -0
  31. package/dist/esm/lib/getOffsetFromPage.mjs +7 -0
  32. package/dist/esm/lib/getPageFromOffset.d.mts +4 -0
  33. package/dist/esm/lib/getPageFromOffset.mjs +7 -0
  34. package/dist/esm/lib/shallowEqual.d.mts +4 -0
  35. package/dist/esm/lib/shallowEqual.mjs +23 -0
  36. package/dist/esm/store/index.d.mts +14 -0
  37. package/dist/esm/store/index.mjs +15 -0
  38. package/dist/esm/store/localStorageStore.d.mts +6 -0
  39. package/dist/esm/store/localStorageStore.mjs +32 -0
  40. package/dist/esm/store/memoryStore.d.mts +6 -0
  41. package/dist/esm/store/memoryStore.mjs +24 -0
  42. package/dist/esm/store/reduxHelpers/createActions.d.mts +16 -0
  43. package/dist/esm/store/reduxHelpers/createActions.mjs +15 -0
  44. package/dist/esm/store/reduxHelpers/createReducer.d.mts +11 -0
  45. package/dist/esm/store/reduxHelpers/createReducer.mjs +24 -0
  46. package/dist/esm/store/reduxStore.d.mts +18 -0
  47. package/dist/esm/store/reduxStore.mjs +61 -0
  48. package/dist/esm/store/urlParamStore.d.mts +8 -0
  49. package/dist/esm/store/urlParamStore.mjs +63 -0
  50. package/dist/esm/useFilter.d.mts +106 -0
  51. package/dist/esm/useFilter.mjs +251 -0
  52. package/dist/esm/useSimpleFilter.d.mts +89 -0
  53. package/dist/esm/useSimpleFilter.mjs +198 -0
  54. package/package.json +1 -1
  55. package/.github/workflows/main.yml +0 -38
  56. package/.vscode/settings.json +0 -3
  57. package/CHANGELOG.md +0 -185
  58. package/Makefile +0 -25
  59. package/eslint.config.js +0 -30
  60. package/mise.toml +0 -3
  61. package/prettier.config.js +0 -3
  62. package/src/lib/buildDefaultFilterInfo.ts +0 -36
  63. package/src/lib/getOffsetFromPage.ts +0 -5
  64. package/src/lib/getPageFromOffset.ts +0 -5
  65. package/src/lib/shallowEqual.test.ts +0 -71
  66. package/src/lib/shallowEqual.ts +0 -26
  67. package/src/store/index.ts +0 -30
  68. package/src/store/localStorageStore.ts +0 -36
  69. package/src/store/memoryStore.ts +0 -27
  70. package/src/store/reduxHelpers/createActions.test.ts +0 -32
  71. package/src/store/reduxHelpers/createActions.ts +0 -56
  72. package/src/store/reduxHelpers/createReducer.test.ts +0 -65
  73. package/src/store/reduxHelpers/createReducer.ts +0 -47
  74. package/src/store/reduxStore.ts +0 -78
  75. package/src/store/urlParamStore.test.ts +0 -131
  76. package/src/store/urlParamStore.ts +0 -85
  77. package/src/useFilter.test.tsx +0 -822
  78. package/src/useFilter.ts +0 -524
  79. package/src/useSimpleFilter.test.tsx +0 -676
  80. package/src/useSimpleFilter.ts +0 -397
  81. package/src/vitest-env.d.ts +0 -1
  82. package/tsconfig.json +0 -76
  83. package/tsdown.config.ts +0 -30
  84. package/vite.config.ts +0 -9
@@ -1,822 +0,0 @@
1
- import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
2
- import { useState } from 'react'
3
-
4
- import buildDefaultFilterInfo from './lib/buildDefaultFilterInfo'
5
- import type { FilterStore } from './store'
6
- import { setFilterStore } from './store'
7
- import localStorageStore from './store/localStorageStore'
8
- import memoryStore from './store/memoryStore'
9
- import { createUrlParamStore } from './store/urlParamStore'
10
- import type { FilterInfo } from './useFilter'
11
- import useFilter from './useFilter'
12
-
13
- async function runFilterTest(testOptions: { store: FilterStore }) {
14
- describe('useFilter', () => {
15
- const defaultFilterInfo = buildDefaultFilterInfo({
16
- filter: {
17
- foo: 'bar',
18
- },
19
- })
20
-
21
- it('calls on mount', async () => {
22
- const { onChangeMock } = await setup({ defaultFilterInfo })
23
-
24
- expect(onChangeMock).toHaveBeenCalledTimes(1)
25
- expect(onChangeMock).toHaveBeenLastCalledWith(
26
- expect.objectContaining({ filter: { foo: 'bar' } }),
27
- 'initial',
28
- )
29
- expect(screen.getByTestId('updateReason').textContent).toBe('initial')
30
- })
31
-
32
- it('calls when data changes', async () => {
33
- const { onChangeMock } = await setup({ defaultFilterInfo })
34
-
35
- await actAndWait(() => {
36
- fireEvent.click(screen.getByTestId('updateFilterButton'))
37
- })
38
- expect(onChangeMock).toHaveBeenCalledTimes(2)
39
- expect(onChangeMock).toHaveBeenLastCalledWith(
40
- expect.objectContaining({ filter: { foo: '42' } }),
41
- 'filter',
42
- )
43
- expect(screen.getByTestId('updateReason').textContent).toBe('filter')
44
- })
45
-
46
- it('uses shallowEqual be default', async () => {
47
- const { onChangeMock } = await setup({ defaultFilterInfo })
48
-
49
- await actAndWait(() => {
50
- fireEvent.click(screen.getByTestId('updateFilterButton'))
51
- })
52
- await actAndWait(() => {
53
- fireEvent.click(screen.getByTestId('updateFilterButton'))
54
- })
55
- // 2 is still the correct number of times, as there is one from the initial
56
- // mount and the other from updateFilter.
57
- expect(onChangeMock).toHaveBeenCalledTimes(2)
58
- })
59
-
60
- it('can reset and keeps defaults', async () => {
61
- const { onChangeMock } = await setup({ defaultFilterInfo })
62
-
63
- await actAndWait(() => {
64
- fireEvent.click(screen.getByTestId('resetFilterButton'))
65
- })
66
- expect(onChangeMock).toHaveBeenCalledTimes(2)
67
- expect(onChangeMock).toHaveBeenLastCalledWith(
68
- expect.objectContaining({ filter: { foo: 'bar' } }),
69
- 'filter',
70
- )
71
- })
72
-
73
- it('can setSort', async () => {
74
- const { onChangeMock } = await setup({ defaultFilterInfo })
75
-
76
- await actAndWait(() => {
77
- fireEvent.click(screen.getByTestId('setSortButton'))
78
- })
79
- expect(onChangeMock).toHaveBeenCalledTimes(2)
80
- expect(onChangeMock).toHaveBeenLastCalledWith(
81
- expect.objectContaining({ filter: { foo: 'bar' }, sort: 'foo:bar' }),
82
- 'filter',
83
- )
84
- })
85
-
86
- it('renders the current filter', async () => {
87
- await setup({ defaultFilterInfo })
88
-
89
- expect(
90
- safeJsonParse(screen.getByTestId('filterJson').textContent),
91
- ).toEqual(expect.objectContaining({ filter: { foo: 'bar' } }))
92
-
93
- await actAndWait(() => {
94
- fireEvent.click(screen.getByTestId('updateFilterButton'))
95
- })
96
-
97
- expect(
98
- safeJsonParse(screen.getByTestId('filterJson').textContent),
99
- ).toEqual(expect.objectContaining({ filter: { foo: '42' } }))
100
- })
101
-
102
- it('handles onChange.filterInfo', async () => {
103
- const { onChangeMock } = await setup({ defaultFilterInfo })
104
-
105
- expect(
106
- safeJsonParse(screen.getByTestId('filterJson').textContent),
107
- ).toEqual(expect.objectContaining({ totalResults: 1 }))
108
-
109
- // Say the server response with a new totalResults.
110
- onChangeMock.mockImplementation(() => ({
111
- filterInfo: {
112
- totalResults: 100,
113
- },
114
- }))
115
- await actAndWait(() => {
116
- fireEvent.click(screen.getByTestId('forceRefreshButton'))
117
- })
118
-
119
- expect(
120
- safeJsonParse(screen.getByTestId('filterJson').textContent),
121
- ).toEqual(expect.objectContaining({ totalResults: 100 }))
122
- })
123
-
124
- it('handles onChange.filterInfo.totalResults', async () => {
125
- const { onChangeMock } = await setup({ defaultFilterInfo })
126
-
127
- // Say the server response with a new totalResults.
128
- onChangeMock.mockImplementation(() => ({
129
- filterInfo: {
130
- totalResults: 99,
131
- },
132
- }))
133
- await actAndWait(() => {
134
- fireEvent.click(screen.getByTestId('forceRefreshButton'))
135
- })
136
-
137
- expect(
138
- safeJsonParse(screen.getByTestId('filterJson').textContent),
139
- ).toEqual(expect.objectContaining({ totalResults: 99 }))
140
- })
141
-
142
- it('handles onChange.data', async () => {
143
- const { onChangeMock } = await setup({ defaultFilterInfo })
144
-
145
- onChangeMock.mockImplementation((filterInfo) => ({
146
- data: [{ name: filterInfo.filter.foo }],
147
- }))
148
-
149
- // Need to trigger an update so that our onChangeMock implementation takes
150
- // effect.
151
- await actAndWait(() => {
152
- fireEvent.click(screen.getByTestId('updateFilterButton'))
153
- })
154
-
155
- expect(
156
- safeJsonParse(screen.getByTestId('responseDataJson').textContent),
157
- ).toEqual([{ name: '42' }])
158
-
159
- await actAndWait(() => {
160
- fireEvent.click(screen.getByTestId('resetFilterButton'))
161
- })
162
-
163
- expect(
164
- safeJsonParse(screen.getByTestId('responseDataJson').textContent),
165
- ).toEqual([{ name: 'bar' }])
166
- })
167
-
168
- it('handles async onChange.filterInfo', async () => {
169
- const { onChangeMock } = await setup({ defaultFilterInfo })
170
-
171
- expect(
172
- safeJsonParse(screen.getByTestId('filterJson').textContent),
173
- ).toEqual(expect.objectContaining({ totalResults: 1 }))
174
-
175
- // Say the server response with a new totalResults.
176
- onChangeMock.mockImplementation(() =>
177
- Promise.resolve({
178
- filterInfo: {
179
- totalResults: 100,
180
- },
181
- }),
182
- )
183
- await actAndWait(() => {
184
- fireEvent.click(screen.getByTestId('forceRefreshButton'))
185
- })
186
-
187
- expect(
188
- safeJsonParse(screen.getByTestId('filterJson').textContent),
189
- ).toEqual(expect.objectContaining({ totalResults: 100 }))
190
- })
191
-
192
- it('handles async onChange.filterInfo.totalResults', async () => {
193
- const { onChangeMock } = await setup({ defaultFilterInfo })
194
-
195
- // Say the server response with a new totalResults.
196
- onChangeMock.mockImplementation(() =>
197
- Promise.resolve({
198
- filterInfo: {
199
- totalResults: 99,
200
- },
201
- }),
202
- )
203
- await actAndWait(() => {
204
- fireEvent.click(screen.getByTestId('forceRefreshButton'))
205
- })
206
-
207
- expect(
208
- safeJsonParse(screen.getByTestId('filterJson').textContent),
209
- ).toEqual(expect.objectContaining({ totalPages: 5 }))
210
- })
211
-
212
- it('handles async onChange.data', async () => {
213
- const { onChangeMock } = await setup({ defaultFilterInfo })
214
-
215
- onChangeMock.mockImplementation((filterInfo) =>
216
- Promise.resolve({
217
- data: [{ name: filterInfo.filter.foo }],
218
- }),
219
- )
220
-
221
- await actAndWait(() => {
222
- fireEvent.click(screen.getByTestId('updateFilterButton'))
223
- })
224
-
225
- expect(
226
- safeJsonParse(screen.getByTestId('responseDataJson').textContent),
227
- ).toEqual([{ name: '42' }])
228
-
229
- await actAndWait(() => {
230
- fireEvent.click(screen.getByTestId('resetFilterButton'))
231
- })
232
-
233
- expect(
234
- safeJsonParse(screen.getByTestId('responseDataJson').textContent),
235
- ).toEqual([{ name: 'bar' }])
236
- })
237
-
238
- it('persists across mounts', async () => {
239
- const { onChangeMock, ExampleComponent } = await setup({
240
- defaultFilterInfo,
241
- })
242
-
243
- const ToggleComponent = () => {
244
- const [shouldShow, setShouldShow] = useState(true)
245
-
246
- return (
247
- <div>
248
- <button
249
- data-testid="toggleShouldShow"
250
- onClick={() => setShouldShow(!shouldShow)}
251
- />
252
- {shouldShow ? <ExampleComponent /> : null}
253
- </div>
254
- )
255
- }
256
-
257
- // Simulate the filter being visible.
258
- await actAndWait(() => {
259
- // Cleanup the render done in `setup`.
260
- cleanup()
261
- render(<ToggleComponent />)
262
- })
263
- expect(screen.queryByTestId('updateFilterButton')).toBeTruthy()
264
-
265
- // Simulate removing the filter.
266
- await actAndWait(() => {
267
- fireEvent.click(screen.getByTestId('toggleShouldShow'))
268
- })
269
- expect(screen.queryByTestId('updateFilterButton')).toBeFalsy()
270
-
271
- // Simulate the filter being visible, again.
272
- await actAndWait(() => {
273
- fireEvent.click(screen.getByTestId('toggleShouldShow'))
274
- })
275
- expect(screen.queryByTestId('updateFilterButton')).toBeTruthy()
276
-
277
- // onChange should still only be called once.
278
- expect(onChangeMock).toHaveBeenCalledTimes(1)
279
- })
280
-
281
- it('uses onBeforeSaveFilter to control what is saved', async () => {
282
- const { ExampleComponent } = await setup({
283
- defaultFilterInfo,
284
- onBeforeSaveFilter: (filterInfo) => {
285
- return {
286
- ...filterInfo,
287
- filter: {
288
- ...filterInfo.filter,
289
- somethingAddedInOnBeforeSaveFilter: 'hello world',
290
- },
291
- }
292
- },
293
- })
294
-
295
- const ToggleComponent = () => {
296
- const [shouldShow, setShouldShow] = useState(true)
297
-
298
- return (
299
- <div>
300
- <button
301
- data-testid="toggleShouldShow"
302
- onClick={() => setShouldShow(!shouldShow)}
303
- />
304
- {shouldShow ? <ExampleComponent /> : null}
305
- </div>
306
- )
307
- }
308
-
309
- // Simulate the filter being visible.
310
- await actAndWait(() => {
311
- // Cleanup the render done in `setup`.
312
- cleanup()
313
- render(<ToggleComponent />)
314
- })
315
- expect(screen.queryByTestId('updateFilterButton')).toBeTruthy()
316
-
317
- // Do something to ensure filter is saved.
318
- await actAndWait(() => {
319
- fireEvent.click(screen.getByTestId('updateFilterButton'))
320
- })
321
-
322
- // Simulate removing the filter.
323
- await actAndWait(() => {
324
- fireEvent.click(screen.getByTestId('toggleShouldShow'))
325
- })
326
- expect(screen.queryByTestId('updateFilterButton')).toBeFalsy()
327
-
328
- // Simulate the filter being visible, again.
329
- await actAndWait(() => {
330
- fireEvent.click(screen.getByTestId('toggleShouldShow'))
331
- })
332
- expect(screen.queryByTestId('updateFilterButton')).toBeTruthy()
333
-
334
- // Verify that the data we added in `onBeforeSaveFilter` is there.
335
- expect(
336
- safeJsonParse(screen.getByTestId('filterJson').textContent),
337
- ).toEqual(
338
- expect.objectContaining({
339
- filter: expect.objectContaining({
340
- somethingAddedInOnBeforeSaveFilter: 'hello world',
341
- }),
342
- }),
343
- )
344
- })
345
-
346
- it('can setPageSize', async () => {
347
- const defaultFilterInfo = buildDefaultFilterInfo({
348
- filter: {
349
- foo: 'bar',
350
- },
351
- pageSize: 20,
352
- })
353
-
354
- const children: TestCaseChildren = (api) => (
355
- <>
356
- <button
357
- data-testid="setPageButton"
358
- onClick={() => api.setPage(api.filterInfo.page + 1)}
359
- />
360
- <button
361
- data-testid="setPageSizeButton"
362
- onClick={() => api.setPageSize(5)}
363
- />
364
- </>
365
- )
366
-
367
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
368
-
369
- await actAndWait(() => {
370
- fireEvent.click(screen.getByTestId('setPageButton'))
371
- })
372
- expect(onChangeMock).toBeCalledTimes(2)
373
- expect(onChangeMock).toHaveBeenLastCalledWith(
374
- expect.objectContaining({
375
- page: 2,
376
- offset: 20,
377
- }),
378
- 'pagination',
379
- )
380
-
381
- await actAndWait(() => {
382
- fireEvent.click(screen.getByTestId('setPageSizeButton'))
383
- })
384
- expect(onChangeMock).toBeCalledTimes(3)
385
- expect(onChangeMock).toHaveBeenLastCalledWith(
386
- expect.objectContaining({
387
- page: 5,
388
- offset: 20,
389
- }),
390
- 'pagination',
391
- )
392
- })
393
-
394
- it('lets you pass your own store', async () => {
395
- const mockStore: FilterStore = {
396
- getFilter: vitest.fn(),
397
- saveFilter: vitest.fn(),
398
- getData: vitest.fn(),
399
- saveData: vitest.fn(),
400
- clear: vitest.fn(),
401
- }
402
-
403
- await setup({
404
- defaultFilterInfo,
405
- store: mockStore,
406
- })
407
-
408
- expect(mockStore.getFilter).toHaveBeenCalled()
409
- expect(mockStore.saveFilter).toHaveBeenCalled()
410
- })
411
-
412
- it('handles stores not returning a full filterInfo', async () => {
413
- const mockStore: FilterStore = {
414
- getFilter: vitest
415
- .fn()
416
- .mockReturnValue({ filter: { fromStore: 'lol' } }),
417
- saveFilter: vitest.fn(),
418
- getData: vitest.fn(),
419
- saveData: vitest.fn(),
420
- clear: vitest.fn(),
421
- }
422
-
423
- await setup({
424
- defaultFilterInfo,
425
- store: mockStore,
426
- })
427
-
428
- expect(
429
- safeJsonParse(screen.getByTestId('filterJson').textContent),
430
- ).toEqual(
431
- expect.objectContaining({
432
- // This asserts that when a store returns a `Partial<FilterInfo>` it's
433
- // turned into a full `FilterInfo`.
434
- page: 1,
435
- }),
436
- )
437
- })
438
-
439
- it('merges stored filters with whats passed to the hook', async () => {
440
- const mockStore: FilterStore = {
441
- getFilter: vitest
442
- .fn()
443
- .mockReturnValue({ filter: { fromStore: 'lol' } }),
444
- saveFilter: vitest.fn(),
445
- getData: vitest.fn(),
446
- saveData: vitest.fn(),
447
- clear: vitest.fn(),
448
- }
449
-
450
- await setup({
451
- defaultFilterInfo,
452
- store: mockStore,
453
- })
454
-
455
- expect(
456
- safeJsonParse(screen.getByTestId('filterJson').textContent),
457
- ).toEqual(
458
- expect.objectContaining({
459
- filter: {
460
- // This asserts that whatever the store returns is included.
461
- fromStore: 'lol',
462
- // This asserts that whatever defaultFilterInfo passed to the hook is
463
- // included.
464
- foo: 'bar',
465
- },
466
- }),
467
- )
468
- })
469
- })
470
-
471
- describe('page based', () => {
472
- const defaultFilterInfo = buildDefaultFilterInfo({
473
- filter: {
474
- foo: 'bar',
475
- },
476
- pageSize: 20,
477
- page: 2,
478
- })
479
-
480
- const children: TestCaseChildren = (api) => (
481
- <button data-testid="setPageButton" onClick={() => api.setPage(3)} />
482
- )
483
-
484
- it('accepts initial page', async () => {
485
- await setup({ defaultFilterInfo, children })
486
-
487
- expect(
488
- safeJsonParse(screen.getByTestId('filterJson').textContent),
489
- ).toEqual(expect.objectContaining({ page: 2, offset: 20 }))
490
- })
491
-
492
- it('can set page', async () => {
493
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
494
-
495
- await actAndWait(() => {
496
- fireEvent.click(screen.getByTestId('setPageButton'))
497
- })
498
-
499
- expect(onChangeMock).toHaveBeenCalledTimes(2)
500
- expect(onChangeMock).toHaveBeenLastCalledWith(
501
- expect.objectContaining({ page: 3, offset: 40 }),
502
- 'pagination',
503
- )
504
- expect(screen.getByTestId('updateReason').textContent).toBe('pagination')
505
- })
506
-
507
- it('resets pagination when filters change', async () => {
508
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
509
-
510
- await actAndWait(() => {
511
- fireEvent.click(screen.getByTestId('setPageButton'))
512
- })
513
- expect(onChangeMock).toHaveBeenCalledTimes(2)
514
- expect(onChangeMock).toHaveBeenLastCalledWith(
515
- expect.objectContaining({ page: 3, offset: 40 }),
516
- 'pagination',
517
- )
518
-
519
- await actAndWait(() => {
520
- fireEvent.click(screen.getByTestId('updateFilterButton'))
521
- })
522
- expect(onChangeMock).toHaveBeenCalledTimes(3)
523
- expect(onChangeMock).toHaveBeenLastCalledWith(
524
- expect.objectContaining({ page: 1 }),
525
- 'filter',
526
- )
527
-
528
- expect(screen.getByTestId('updateReason').textContent).toBe('filter')
529
- })
530
-
531
- it('can handle onChange.filterInfo.page', async () => {
532
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
533
-
534
- // Intentionally respond with numbers that don't match what the button
535
- // actually does.
536
- // This is to test the actual response, the view updates immediately.
537
- onChangeMock.mockImplementation(() => ({
538
- filterInfo: {
539
- page: 20,
540
- },
541
- }))
542
-
543
- await actAndWait(() => {
544
- fireEvent.click(screen.getByTestId('setPageButton'))
545
- })
546
-
547
- expect(
548
- safeJsonParse(screen.getByTestId('filterJson').textContent),
549
- ).toEqual(expect.objectContaining({ page: 20, offset: 380 }))
550
- })
551
- })
552
-
553
- describe('offset based', () => {
554
- const defaultFilterInfo = buildDefaultFilterInfo({
555
- filter: {
556
- foo: 'bar',
557
- },
558
- pageSize: 20,
559
- offset: 80,
560
- })
561
-
562
- const children: TestCaseChildren = (api) => (
563
- <button
564
- data-testid="setOffsetButton"
565
- onClick={() => api.setOffset(100)}
566
- />
567
- )
568
-
569
- it('accepts initial offset', async () => {
570
- await setup({ defaultFilterInfo, children })
571
-
572
- expect(
573
- safeJsonParse(screen.getByTestId('filterJson').textContent),
574
- ).toEqual(expect.objectContaining({ offset: 80, page: 5 }))
575
- })
576
-
577
- it('can set offset', async () => {
578
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
579
-
580
- await actAndWait(() => {
581
- fireEvent.click(screen.getByTestId('setOffsetButton'))
582
- })
583
-
584
- expect(onChangeMock).toHaveBeenCalledTimes(2)
585
- expect(onChangeMock).toHaveBeenLastCalledWith(
586
- expect.objectContaining({ page: 6, offset: 100 }),
587
- 'pagination',
588
- )
589
- expect(screen.getByTestId('updateReason').textContent).toBe('pagination')
590
- })
591
-
592
- it('resets pagination when filters change', async () => {
593
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
594
-
595
- await actAndWait(() => {
596
- fireEvent.click(screen.getByTestId('setOffsetButton'))
597
- })
598
- expect(onChangeMock).toHaveBeenCalledTimes(2)
599
- expect(onChangeMock).toHaveBeenLastCalledWith(
600
- expect.objectContaining({ page: 6, offset: 100 }),
601
- 'pagination',
602
- )
603
-
604
- await actAndWait(() => {
605
- fireEvent.click(screen.getByTestId('updateFilterButton'))
606
- })
607
- expect(onChangeMock).toHaveBeenCalledTimes(3)
608
- expect(onChangeMock).toHaveBeenLastCalledWith(
609
- expect.objectContaining({ offset: 0 }),
610
- 'filter',
611
- )
612
-
613
- expect(screen.getByTestId('updateReason').textContent).toBe('filter')
614
- })
615
-
616
- it('can handle onChange.filterInfo.offset', async () => {
617
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
618
-
619
- // Intentionally respond with numbers that don't match what the button
620
- // actually does.
621
- // This is to test the actual response, the view updates immediately.
622
- onChangeMock.mockImplementation(() => ({
623
- filterInfo: {
624
- offset: 380,
625
- },
626
- }))
627
- await actAndWait(() => {
628
- fireEvent.click(screen.getByTestId('setOffsetButton'))
629
- })
630
-
631
- expect(
632
- safeJsonParse(screen.getByTestId('filterJson').textContent),
633
- ).toEqual(expect.objectContaining({ page: 20, offset: 380 }))
634
- })
635
- })
636
-
637
- describe('cursor based', () => {
638
- const defaultFilterInfo = buildDefaultFilterInfo({
639
- filter: {
640
- foo: 'bar',
641
- },
642
- cursor: 'currentCursor',
643
- nextCursor: 'nextCursor',
644
- })
645
-
646
- const children: TestCaseChildren = (api) => (
647
- <button
648
- data-testid="setCursorButton"
649
- onClick={() => api.setCursor(api.filterInfo.nextCursor)}
650
- />
651
- )
652
-
653
- it('accepts initial cursor', async () => {
654
- await setup({ defaultFilterInfo, children })
655
-
656
- expect(
657
- safeJsonParse(screen.getByTestId('filterJson').textContent),
658
- ).toEqual(expect.objectContaining({ cursor: 'currentCursor' }))
659
- })
660
-
661
- it('can set cursor', async () => {
662
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
663
-
664
- await actAndWait(() => {
665
- fireEvent.click(screen.getByTestId('setCursorButton'))
666
- })
667
-
668
- expect(onChangeMock).toHaveBeenCalledTimes(2)
669
- expect(onChangeMock).toHaveBeenLastCalledWith(
670
- expect.objectContaining({ cursor: 'nextCursor' }),
671
- 'pagination',
672
- )
673
- expect(screen.getByTestId('updateReason').textContent).toBe('pagination')
674
- })
675
-
676
- it('resets cursor when filters change', async () => {
677
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
678
-
679
- await actAndWait(() => {
680
- fireEvent.click(screen.getByTestId('setCursorButton'))
681
- })
682
- expect(onChangeMock).toHaveBeenCalledTimes(2)
683
- expect(onChangeMock).toHaveBeenLastCalledWith(
684
- expect.objectContaining({ cursor: 'nextCursor' }),
685
- 'pagination',
686
- )
687
-
688
- await actAndWait(() => {
689
- fireEvent.click(screen.getByTestId('updateFilterButton'))
690
- })
691
- expect(onChangeMock).toHaveBeenCalledTimes(3)
692
- expect(onChangeMock).toHaveBeenLastCalledWith(
693
- expect.objectContaining({ cursor: undefined }),
694
- 'filter',
695
- )
696
-
697
- expect(screen.getByTestId('updateReason').textContent).toBe('filter')
698
- })
699
-
700
- it('can handle onChange.filterInfo.nextCursor', async () => {
701
- const { onChangeMock } = await setup({ defaultFilterInfo, children })
702
-
703
- onChangeMock.mockImplementation(() => ({
704
- filterInfo: {
705
- nextCursor: 'nextCursorFromResponse',
706
- },
707
- }))
708
- await actAndWait(() => {
709
- fireEvent.click(screen.getByTestId('setCursorButton'))
710
- })
711
-
712
- expect(
713
- safeJsonParse(screen.getByTestId('filterJson').textContent),
714
- ).toEqual(
715
- expect.objectContaining({ nextCursor: 'nextCursorFromResponse' }),
716
- )
717
- })
718
- })
719
-
720
- function wait(n = 0) {
721
- return new Promise((resolve) => setTimeout(resolve, n))
722
- }
723
-
724
- function safeJsonParse(input: string | undefined | null = '') {
725
- if (input) {
726
- return JSON.parse(input)
727
- }
728
- }
729
-
730
- async function actAndWait(fn: () => void, n = 0) {
731
- act(fn)
732
- await act(async () => {
733
- await wait(n)
734
- })
735
- // You'd think you could just write this:
736
- // await act(async () => {
737
- // fn();
738
- // await wait(n);
739
- // });
740
- // But for some reason, doing it that way needs a longer wait duration than
741
- // just a single tick. /shrug.
742
- }
743
-
744
- type TestCaseChildren = (api: ReturnType<typeof useFilter>) => React.ReactNode
745
-
746
- async function setup<TFilterInfo extends FilterInfo<any>>(options: {
747
- defaultFilterInfo: TFilterInfo
748
- children?: TestCaseChildren
749
- onBeforeSaveFilter?: (filterInfo: TFilterInfo) => TFilterInfo
750
- store?: FilterStore
751
- }) {
752
- testOptions.store.clear()
753
- setFilterStore(testOptions.store)
754
-
755
- const onChangeMock = vitest.fn()
756
-
757
- const ExampleComponent = () => {
758
- const api = useFilter('namespace', {
759
- defaultFilterInfo: options.defaultFilterInfo,
760
- debounceDuration: 0,
761
- onChange: onChangeMock,
762
- store: options.store,
763
- // This is a very annoying warning in TypeScript:
764
- //
765
- // 'FilterInfo<any>' is assignable to the constraint of type
766
- // 'TFilterInfo', but 'TFilterInfo' could be instantiated with a
767
- // different subtype of constraint 'FilterInfo<any>'
768
- //
769
- // Which I don't think is valid, because you cannot do what's is saying is
770
- // possible, as this errors (as expected):
771
- //
772
- // setup({
773
- // defaultFilterInfo: {
774
- // lol: '24',
775
- // }
776
- // })
777
- //
778
- // So I don't know why TS thinks it can be something else, it's already
779
- // preventing you from doing that.
780
- onBeforeSaveFilter: options.onBeforeSaveFilter as unknown as any,
781
- })
782
-
783
- return (
784
- <div>
785
- {options.children?.(api)}
786
- <button
787
- data-testid="updateFilterButton"
788
- onClick={() => api.updateFilter({ foo: '42' })}
789
- />
790
- <button
791
- data-testid="resetFilterButton"
792
- onClick={() => api.resetFilter()}
793
- />
794
- <button
795
- data-testid="forceRefreshButton"
796
- onClick={() => api.forceRefresh()}
797
- />
798
- <button
799
- data-testid="setSortButton"
800
- onClick={() => api.setSort('foo:bar')}
801
- />
802
- <div data-testid="filterJson">{JSON.stringify(api.filterInfo)}</div>
803
- <div data-testid="responseDataJson">{JSON.stringify(api.data)}</div>
804
- <div data-testid="updateReason">{api.updateReason}</div>
805
- </div>
806
- )
807
- }
808
-
809
- await actAndWait(() => {
810
- render(<ExampleComponent />)
811
- })
812
-
813
- return {
814
- onChangeMock,
815
- ExampleComponent,
816
- }
817
- }
818
- }
819
-
820
- runFilterTest({ store: memoryStore })
821
- runFilterTest({ store: createUrlParamStore() })
822
- runFilterTest({ store: localStorageStore })