11import { renderHook , act } from "@testing-library/react" ;
22import { describe , it , expect , vi } from "vitest" ;
3- import type { CollectionStateSynchronizer } from "@/types/collection" ;
3+ import type { CollectionSnapshot , CollectionStateSynchronizer } from "@/types/collection" ;
44import { useCollectionVariables } from "./use-collection-variables" ;
55
66describe ( "useCollectionVariables with synchronizer" , ( ) => {
7- function createMockSynchronizer ( initial ?: ReturnType < CollectionStateSynchronizer [ "read" ] > ) {
7+ function createMockSynchronizer ( initial ?: CollectionSnapshot ) {
8+ const subscribers : Array < ( snapshot : CollectionSnapshot | undefined ) => void > = [ ] ;
89 return {
9- read : vi . fn ( ( ) => initial ) ,
10+ subscribe : vi . fn ( ( onChange ) => {
11+ subscribers . push ( onChange ) ;
12+ // BehaviorSubject: emit immediately
13+ onChange ( initial ) ;
14+ return ( ) => {
15+ const idx = subscribers . indexOf ( onChange ) ;
16+ if ( idx >= 0 ) subscribers . splice ( idx , 1 ) ;
17+ } ;
18+ } ) ,
1019 write : vi . fn ( ) ,
11- } satisfies CollectionStateSynchronizer ;
20+ // Test helper to simulate external change
21+ emit ( snapshot : CollectionSnapshot | undefined ) {
22+ for ( const cb of subscribers ) cb ( snapshot ) ;
23+ } ,
24+ } satisfies CollectionStateSynchronizer & {
25+ emit : ( s : CollectionSnapshot | undefined ) => void ;
26+ } ;
1227 }
1328
14- describe ( "read (initial hydration)" , ( ) => {
29+ describe ( "subscribe (initial hydration)" , ( ) => {
1530 it ( "uses synchronizer initial state over params defaults" , ( ) => {
1631 const synchronizer = createMockSynchronizer ( {
1732 filters : [ { field : "status" , operator : "eq" , value : "ACTIVE" } ] ,
@@ -26,15 +41,15 @@ describe("useCollectionVariables with synchronizer", () => {
2641 } ) ,
2742 ) ;
2843
29- expect ( synchronizer . read ) . toHaveBeenCalledOnce ( ) ;
44+ expect ( synchronizer . subscribe ) . toHaveBeenCalledOnce ( ) ;
3045 expect ( result . current . control . filters ) . toEqual ( [
3146 { field : "status" , operator : "eq" , value : "ACTIVE" } ,
3247 ] ) ;
3348 expect ( result . current . control . sortStates ) . toEqual ( [ { field : "name" , direction : "Asc" } ] ) ;
3449 expect ( result . current . control . pageSize ) . toBe ( 50 ) ;
3550 } ) ;
3651
37- it ( "falls back to params when synchronizer returns undefined" , ( ) => {
52+ it ( "falls back to params when synchronizer emits undefined" , ( ) => {
3853 const synchronizer = createMockSynchronizer ( undefined ) ;
3954
4055 const { result } = renderHook ( ( ) =>
@@ -70,12 +85,53 @@ describe("useCollectionVariables with synchronizer", () => {
7085 ) ;
7186
7287 expect ( result . current . control . pageSize ) . toBe ( 100 ) ;
73- // Sort falls back to params
88+ // Sort remains from params since synchronizer didn't provide it
7489 expect ( result . current . control . sortStates ) . toEqual ( [ { field : "name" , direction : "Asc" } ] ) ;
7590 } ) ;
7691 } ) ;
7792
93+ describe ( "subscribe (external changes)" , ( ) => {
94+ it ( "updates state when synchronizer emits external change" , ( ) => {
95+ const synchronizer = createMockSynchronizer ( undefined ) ;
96+
97+ const { result } = renderHook ( ( ) =>
98+ useCollectionVariables ( {
99+ params : { pageSize : 20 } ,
100+ synchronizer,
101+ } ) ,
102+ ) ;
103+
104+ act ( ( ) => {
105+ synchronizer . emit ( {
106+ filters : [ { field : "name" , operator : "contains" , value : "test" } ] ,
107+ pageSize : 50 ,
108+ } ) ;
109+ } ) ;
110+
111+ expect ( result . current . control . filters ) . toEqual ( [
112+ { field : "name" , operator : "contains" , value : "test" } ,
113+ ] ) ;
114+ expect ( result . current . control . pageSize ) . toBe ( 50 ) ;
115+ } ) ;
116+ } ) ;
117+
78118 describe ( "write (state persistence)" , ( ) => {
119+ it ( "does not call write on initial mount (skip first write)" , ( ) => {
120+ const synchronizer = createMockSynchronizer ( {
121+ filters : [ { field : "status" , operator : "eq" , value : "ACTIVE" } ] ,
122+ pageSize : 50 ,
123+ } ) ;
124+
125+ renderHook ( ( ) =>
126+ useCollectionVariables ( {
127+ params : { pageSize : 20 } ,
128+ synchronizer,
129+ } ) ,
130+ ) ;
131+
132+ expect ( synchronizer . write ) . not . toHaveBeenCalled ( ) ;
133+ } ) ;
134+
79135 it ( "calls write on filter change" , ( ) => {
80136 const synchronizer = createMockSynchronizer ( undefined ) ;
81137
@@ -141,14 +197,13 @@ describe("useCollectionVariables with synchronizer", () => {
141197 ) ;
142198 } ) ;
143199
144- it ( "does not call write when no synchronizer is provided" , ( ) => {
200+ it ( "does not crash when no synchronizer is provided" , ( ) => {
145201 const { result } = renderHook ( ( ) => useCollectionVariables ( { params : { pageSize : 20 } } ) ) ;
146202
147203 act ( ( ) => {
148204 result . current . control . addFilter ( "status" , "eq" , "ACTIVE" ) ;
149205 } ) ;
150206
151- // No error thrown — just verifying it doesn't crash
152207 expect ( result . current . control . filters ) . toHaveLength ( 1 ) ;
153208 } ) ;
154209 } ) ;
0 commit comments