11import { renderHook , act } from "@testing-library/react" ;
2+ import type { ReactNode } from "react" ;
23import { describe , it , expect , vi , beforeEach } from "vitest" ;
3- import { useSearchParamsPersistence } from "./use-search-params-persistence" ;
4+ import { MemoryRouter } from "react-router" ;
5+ import { useCollectionURLPersistence } from "./use-collection-url-persistence" ;
46
57beforeEach ( ( ) => {
6- // Reset URL to root before each test
7- window . history . replaceState ( null , "" , "/" ) ;
8+ vi . useRealTimers ( ) ;
89} ) ;
910
11+ function createWrapper ( initialEntry : string = "/" ) {
12+ return function Wrapper ( { children } : { children : ReactNode } ) {
13+ return < MemoryRouter initialEntries = { [ initialEntry ] } > { children } </ MemoryRouter > ;
14+ } ;
15+ }
16+
17+ function renderPersistence (
18+ options ?: Parameters < typeof useCollectionURLPersistence > [ 0 ] ,
19+ initialEntry : string = "/" ,
20+ ) {
21+ return renderHook ( ( ) => useCollectionURLPersistence ( options ) , {
22+ wrapper : createWrapper ( initialEntry ) ,
23+ } ) ;
24+ }
25+
1026/** Helper: read current snapshot */
11- function readSnapshot ( result : { current : ReturnType < typeof useSearchParamsPersistence > } ) {
27+ function readSnapshot ( result : { current : ReturnType < typeof useCollectionURLPersistence > } ) {
1228 return result . current . read ( ) ;
1329}
1430
15- describe ( "useSearchParamsPersistence " , ( ) => {
31+ describe ( "useCollectionURLPersistence " , ( ) => {
1632 // ---------------------------------------------------------------------------
1733 // read (initial)
1834 // ---------------------------------------------------------------------------
1935 describe ( "read (initial)" , ( ) => {
2036 it ( "returns undefined when no search params exist" , ( ) => {
21- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
37+ const { result } = renderPersistence ( ) ;
2238
2339 expect ( readSnapshot ( result ) ) . toBeUndefined ( ) ;
2440 } ) ;
2541
2642 it ( "returns pageSize from URL" , ( ) => {
27- window . history . replaceState ( null , "" , "/?p=50" ) ;
28- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
43+ const { result } = renderPersistence ( undefined , "/?p=50" ) ;
2944
3045 expect ( readSnapshot ( result ) ?. pageSize ) . toBe ( 50 ) ;
3146 } ) ;
3247
3348 it ( "returns sort from URL" , ( ) => {
34- window . history . replaceState ( null , "" , "/?s=name:asc" ) ;
35- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
49+ const { result } = renderPersistence ( undefined , "/?s=name:asc" ) ;
3650
3751 expect ( readSnapshot ( result ) ?. sort ) . toEqual ( [ { field : "name" , direction : "Asc" } ] ) ;
3852 } ) ;
3953
4054 it ( "returns sort desc from URL" , ( ) => {
41- window . history . replaceState ( null , "" , "/?s=createdAt:desc" ) ;
42- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
55+ const { result } = renderPersistence ( undefined , "/?s=createdAt:desc" ) ;
4356
4457 expect ( readSnapshot ( result ) ?. sort ) . toEqual ( [ { field : "createdAt" , direction : "Desc" } ] ) ;
4558 } ) ;
4659
4760 it ( "returns single-value filter from URL" , ( ) => {
48- window . history . replaceState ( null , "" , "/?f.status:eq=ACTIVE" ) ;
49- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
61+ const { result } = renderPersistence ( undefined , "/?f.status:eq=ACTIVE" ) ;
5062
5163 expect ( readSnapshot ( result ) ?. filters ) . toEqual ( [
5264 { field : "status" , operator : "eq" , value : "ACTIVE" } ,
5365 ] ) ;
5466 } ) ;
5567
5668 it ( "returns multi-value filter from URL (repeated params)" , ( ) => {
57- window . history . replaceState ( null , "" , "/?f.status:in=ACTIVE&f.status:in=PENDING" ) ;
58- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
69+ const { result } = renderPersistence ( undefined , "/?f.status:in=ACTIVE&f.status:in=PENDING" ) ;
5970
6071 expect ( readSnapshot ( result ) ?. filters ) . toEqual ( [
6172 { field : "status" , operator : "in" , value : [ "ACTIVE" , "PENDING" ] } ,
@@ -64,17 +75,15 @@ describe("useSearchParamsPersistence", () => {
6475
6576 it ( "returns JSON object filter value (e.g. between)" , ( ) => {
6677 const url = `/?f.amount:between=${ encodeURIComponent ( JSON . stringify ( { min : 10 , max : 100 } ) ) } ` ;
67- window . history . replaceState ( null , "" , url ) ;
68- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
78+ const { result } = renderPersistence ( undefined , url ) ;
6979
7080 expect ( readSnapshot ( result ) ?. filters ) . toEqual ( [
7181 { field : "amount" , operator : "between" , value : { min : 10 , max : 100 } } ,
7282 ] ) ;
7383 } ) ;
7484
7585 it ( "returns all state together" , ( ) => {
76- window . history . replaceState ( null , "" , "/?p=25&s=name:desc&f.status:eq=ACTIVE" ) ;
77- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
86+ const { result } = renderPersistence ( undefined , "/?p=25&s=name:desc&f.status:eq=ACTIVE" ) ;
7887
7988 expect ( readSnapshot ( result ) ) . toEqual ( {
8089 pageSize : 25 ,
@@ -84,22 +93,19 @@ describe("useSearchParamsPersistence", () => {
8493 } ) ;
8594
8695 it ( "respects prefix option" , ( ) => {
87- window . history . replaceState ( null , "" , "/?t1.p=30&p=10" ) ;
88- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( { prefix : "t1" } ) ) ;
96+ const { result } = renderPersistence ( { prefix : "t1" } , "/?t1.p=30&p=10" ) ;
8997
9098 expect ( readSnapshot ( result ) ?. pageSize ) . toBe ( 30 ) ;
9199 } ) ;
92100
93101 it ( "ignores invalid pageSize values" , ( ) => {
94- window . history . replaceState ( null , "" , "/?p=abc" ) ;
95- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
102+ const { result } = renderPersistence ( undefined , "/?p=abc" ) ;
96103
97104 expect ( readSnapshot ( result ) ) . toBeUndefined ( ) ;
98105 } ) ;
99106
100107 it ( "ignores filter keys without operator" , ( ) => {
101- window . history . replaceState ( null , "" , "/?f.status=ACTIVE" ) ;
102- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
108+ const { result } = renderPersistence ( undefined , "/?f.status=ACTIVE" ) ;
103109
104110 expect ( readSnapshot ( result ) ) . toBeUndefined ( ) ;
105111 } ) ;
@@ -110,7 +116,7 @@ describe("useSearchParamsPersistence", () => {
110116 // ---------------------------------------------------------------------------
111117 describe ( "write" , ( ) => {
112118 it ( "writes pageSize to URL" , ( ) => {
113- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
119+ const { result } = renderPersistence ( ) ;
114120
115121 act ( ( ) => {
116122 result . current . write ( { filters : [ ] , sort : [ ] , pageSize : 50 } ) ;
@@ -120,7 +126,7 @@ describe("useSearchParamsPersistence", () => {
120126 } ) ;
121127
122128 it ( "writes sort to URL" , ( ) => {
123- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
129+ const { result } = renderPersistence ( ) ;
124130
125131 act ( ( ) => {
126132 result . current . write ( {
@@ -134,7 +140,7 @@ describe("useSearchParamsPersistence", () => {
134140 } ) ;
135141
136142 it ( "writes single-value filter to URL" , ( ) => {
137- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
143+ const { result } = renderPersistence ( ) ;
138144
139145 act ( ( ) => {
140146 result . current . write ( {
@@ -150,7 +156,7 @@ describe("useSearchParamsPersistence", () => {
150156 } ) ;
151157
152158 it ( "writes multi-value filter as repeated params" , ( ) => {
153- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
159+ const { result } = renderPersistence ( ) ;
154160
155161 act ( ( ) => {
156162 result . current . write ( {
@@ -166,7 +172,7 @@ describe("useSearchParamsPersistence", () => {
166172 } ) ;
167173
168174 it ( "writes object filter value as JSON" , ( ) => {
169- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
175+ const { result } = renderPersistence ( ) ;
170176
171177 act ( ( ) => {
172178 result . current . write ( {
@@ -188,8 +194,7 @@ describe("useSearchParamsPersistence", () => {
188194 } ) ;
189195
190196 it ( "clears old filters when writing new state" , ( ) => {
191- window . history . replaceState ( null , "" , "/?f.status:eq=OLD" ) ;
192- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( ) ) ;
197+ const { result } = renderPersistence ( undefined , "/?f.status:eq=OLD" ) ;
193198
194199 act ( ( ) => {
195200 result . current . write ( {
@@ -205,7 +210,7 @@ describe("useSearchParamsPersistence", () => {
205210 } ) ;
206211
207212 it ( "uses prefix when writing" , ( ) => {
208- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( { prefix : "t1" } ) ) ;
213+ const { result } = renderPersistence ( { prefix : "t1" } ) ;
209214
210215 act ( ( ) => {
211216 result . current . write ( { filters : [ ] , sort : [ ] , pageSize : 40 } ) ;
@@ -222,7 +227,7 @@ describe("useSearchParamsPersistence", () => {
222227 it ( "debounces write when debounceMs is set" , ( ) => {
223228 vi . useFakeTimers ( ) ;
224229
225- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( { debounceMs : 100 } ) ) ;
230+ const { result } = renderPersistence ( { debounceMs : 100 } ) ;
226231
227232 act ( ( ) => {
228233 result . current . write ( { filters : [ ] , sort : [ ] , pageSize : 50 } ) ;
@@ -244,7 +249,7 @@ describe("useSearchParamsPersistence", () => {
244249 it ( "only applies the last write within debounce window" , ( ) => {
245250 vi . useFakeTimers ( ) ;
246251
247- const { result } = renderHook ( ( ) => useSearchParamsPersistence ( { debounceMs : 100 } ) ) ;
252+ const { result } = renderPersistence ( { debounceMs : 100 } ) ;
248253
249254 act ( ( ) => {
250255 result . current . write ( { filters : [ ] , sort : [ ] , pageSize : 10 } ) ;
0 commit comments