1- import { describe , it , expect , beforeEach , vi } from 'vitest' ;
1+ import { describe , it , expect , beforeEach , afterEach , vi } from 'vitest' ;
22import { mount } from '@vue/test-utils' ;
33import { defineComponent , nextTick } from 'vue' ;
44import { createPinia , setActivePinia } from 'pinia' ;
55import { useGeneratorControls } from '@/composables/useGeneratorControls' ;
66import { useStore } from '@/stores/store' ;
7+ import { GENERATOR_STATE_STORAGE_KEY } from '@/utils/persistentState' ;
8+
9+ const mountedWrappers = new Set ( ) ;
710
811const mountComposable = ( options ) => {
912 let api ;
@@ -16,12 +19,24 @@ const mountComposable = (options) => {
1619 } ) ;
1720
1821 const wrapper = mount ( Comp ) ;
22+ mountedWrappers . add ( wrapper ) ;
1923 return { wrapper, api } ;
2024} ;
2125
2226describe ( 'useGeneratorControls' , ( ) => {
2327 beforeEach ( ( ) => {
2428 setActivePinia ( createPinia ( ) ) ;
29+ window . localStorage . clear ( ) ;
30+ window . history . replaceState ( null , '' , '/' ) ;
31+ } ) ;
32+
33+ afterEach ( ( ) => {
34+ mountedWrappers . forEach ( ( wrapper ) => {
35+ if ( wrapper . exists ( ) ) {
36+ wrapper . unmount ( ) ;
37+ }
38+ } ) ;
39+ mountedWrappers . clear ( ) ;
2540 } ) ;
2641
2742 it ( 'provides sensible defaults and computed helpers' , ( ) => {
@@ -58,7 +73,7 @@ describe('useGeneratorControls', () => {
5873 expect ( suffixSpy ) . toHaveBeenLastCalledWith ( '' ) ;
5974 } ) ;
6075
61- it ( 'hydrates initial text on mount and resets on unmount ' , async ( ) => {
76+ it ( 'hydrates initial text on mount when nothing is persisted ' , async ( ) => {
6277 const store = useStore ( ) ;
6378 const prefixSpy = vi . spyOn ( store , 'updatePrefix' ) ;
6479 const suffixSpy = vi . spyOn ( store , 'updateSuffix' ) ;
@@ -75,11 +90,11 @@ describe('useGeneratorControls', () => {
7590
7691 wrapper . unmount ( ) ;
7792 await nextTick ( ) ;
78- expect ( prefixSpy ) . toHaveBeenLastCalledWith ( 'edit' ) ;
79- expect ( suffixSpy ) . toHaveBeenLastCalledWith ( 'me' ) ;
93+ expect ( prefixSpy ) . not . toHaveBeenCalledWith ( 'edit' ) ;
94+ expect ( suffixSpy ) . not . toHaveBeenCalledWith ( 'me' ) ;
8095 } ) ;
8196
82- it ( 'handles partial initial and reset payloads ' , async ( ) => {
97+ it ( 'handles partial initial payloads without clobbering other fields ' , async ( ) => {
8398 const store = useStore ( ) ;
8499 const prefixSpy = vi . spyOn ( store , 'updatePrefix' ) ;
85100 const suffixSpy = vi . spyOn ( store , 'updateSuffix' ) ;
@@ -100,24 +115,67 @@ describe('useGeneratorControls', () => {
100115 expect ( suffixSpy ) . not . toHaveBeenCalled ( ) ;
101116 prefixOnly . wrapper . unmount ( ) ;
102117 await nextTick ( ) ;
118+ } ) ;
103119
104- prefixSpy . mockClear ( ) ;
105- suffixSpy . mockClear ( ) ;
120+ it ( 'persists generator state to localStorage and query params' , async ( ) => {
121+ const store = useStore ( ) ;
122+ const { api } = mountComposable ( ) ;
106123
107- const resetPrefixOnly = mountComposable ( { resetText : { prefix : 'reset' } } ) ;
108- resetPrefixOnly . wrapper . unmount ( ) ;
124+ await store . updatePrefix ( 'Share' ) ;
125+ await store . updateSuffix ( 'Logo' ) ;
126+ store . font = 'Open Sans' ;
127+ api . prefixColor . value = '#123123' ;
128+ api . suffixColor . value = '#abcdef' ;
129+ api . postfixBgColor . value = '#654321' ;
130+ api . fontSize . value = 110 ;
131+ api . transparentBg . value = true ;
132+ api . reverseHighlight . value = true ;
109133 await nextTick ( ) ;
110- expect ( prefixSpy ) . toHaveBeenCalledWith ( 'reset' ) ;
111- expect ( suffixSpy ) . not . toHaveBeenCalled ( ) ;
112134
113- prefixSpy . mockClear ( ) ;
114- suffixSpy . mockClear ( ) ;
135+ const saved = JSON . parse ( window . localStorage . getItem ( GENERATOR_STATE_STORAGE_KEY ) ) ;
136+ expect ( saved . prefix ) . toBe ( 'Share' ) ;
137+ expect ( saved . suffix ) . toBe ( 'Logo' ) ;
138+ expect ( saved . font ) . toBe ( 'Open Sans' ) ;
139+ expect ( saved . prefixColor ) . toBe ( '#123123' ) ;
140+ expect ( saved . transparentBg ) . toBe ( true ) ;
141+ expect ( window . location . search ) . toContain ( 'prefix=Share' ) ;
142+ expect ( window . location . search ) . toContain ( 'reverseHighlight=1' ) ;
143+ } ) ;
115144
116- const resetSuffixOnly = mountComposable ( { resetText : { suffix : 'again' } } ) ;
117- resetSuffixOnly . wrapper . unmount ( ) ;
145+ it ( 'restores state from storage and lets query params override it' , async ( ) => {
146+ window . localStorage . setItem (
147+ GENERATOR_STATE_STORAGE_KEY ,
148+ JSON . stringify ( {
149+ prefix : 'StoredPrefix' ,
150+ suffix : 'StoredSuffix' ,
151+ font : 'Lora' ,
152+ prefixColor : '#101010' ,
153+ suffixColor : '#202020' ,
154+ postfixBgColor : '#303030' ,
155+ fontSize : 80 ,
156+ transparentBg : true ,
157+ reverseHighlight : false
158+ } )
159+ ) ;
160+ window . history . replaceState (
161+ null ,
162+ '' ,
163+ '/?prefix=QueryPrefix&suffixColor=%23aa00aa&reverseHighlight=1'
164+ ) ;
165+
166+ const { api } = mountComposable ( { initialText : { prefix : 'Default' , suffix : 'Values' } } ) ;
167+ const store = useStore ( ) ;
118168 await nextTick ( ) ;
119- expect ( suffixSpy ) . toHaveBeenCalledWith ( 'again' ) ;
120- expect ( prefixSpy ) . not . toHaveBeenCalled ( ) ;
169+
170+ expect ( store . prefix ) . toBe ( 'QueryPrefix' ) ;
171+ expect ( store . suffix ) . toBe ( 'StoredSuffix' ) ;
172+ expect ( store . font ) . toBe ( 'Lora' ) ;
173+ expect ( api . prefixColor . value ) . toBe ( '#101010' ) ;
174+ expect ( api . suffixColor . value ) . toBe ( '#aa00aa' ) ;
175+ expect ( api . postfixBgColor . value ) . toBe ( '#303030' ) ;
176+ expect ( api . fontSize . value ) . toBe ( 80 ) ;
177+ expect ( api . transparentBg . value ) . toBe ( true ) ;
178+ expect ( api . reverseHighlight . value ) . toBe ( true ) ;
121179 } ) ;
122180
123181 it ( 'builds the Twitter intent url with the expected payload' , ( ) => {
@@ -132,4 +190,111 @@ describe('useGeneratorControls', () => {
132190
133191 openSpy . mockRestore ( ) ;
134192 } ) ;
193+
194+ it ( 'hydrates correctly even when store updates resolve synchronously' , async ( ) => {
195+ const store = useStore ( ) ;
196+ const originalPrefix = store . updatePrefix ;
197+ const originalSuffix = store . updateSuffix ;
198+
199+ store . updatePrefix = vi . fn ( ( text ) => {
200+ store . prefix = text ;
201+ return text ;
202+ } ) ;
203+ store . updateSuffix = vi . fn ( ( text ) => {
204+ store . suffix = text ;
205+ return text ;
206+ } ) ;
207+
208+ mountComposable ( { initialText : { prefix : 'Sync' , suffix : 'State' } } ) ;
209+ await nextTick ( ) ;
210+ expect ( store . updatePrefix ) . toHaveBeenCalledWith ( 'Sync' ) ;
211+ expect ( store . updateSuffix ) . toHaveBeenCalledWith ( 'State' ) ;
212+ expect ( store . prefix ) . toBe ( 'Sync' ) ;
213+ expect ( store . suffix ) . toBe ( 'State' ) ;
214+
215+ store . updatePrefix = originalPrefix ;
216+ store . updateSuffix = originalSuffix ;
217+ } ) ;
218+
219+ it ( 'resets text on unmount when persistence is disabled' , async ( ) => {
220+ const store = useStore ( ) ;
221+ await store . updatePrefix ( 'Tmp' ) ;
222+ await store . updateSuffix ( 'State' ) ;
223+
224+ const prefixSpy = vi . spyOn ( store , 'updatePrefix' ) ;
225+ const suffixSpy = vi . spyOn ( store , 'updateSuffix' ) ;
226+
227+ const { wrapper } = mountComposable ( {
228+ resetText : { prefix : 'edit' , suffix : 'me' } ,
229+ persistenceEnabled : false
230+ } ) ;
231+
232+ await nextTick ( ) ;
233+ prefixSpy . mockClear ( ) ;
234+ suffixSpy . mockClear ( ) ;
235+
236+ wrapper . unmount ( ) ;
237+ await nextTick ( ) ;
238+
239+ expect ( prefixSpy ) . toHaveBeenCalledWith ( 'edit' ) ;
240+ expect ( suffixSpy ) . toHaveBeenCalledWith ( 'me' ) ;
241+ } ) ;
242+
243+ it ( 'does not reset prefix when resetText omits it' , async ( ) => {
244+ const store = useStore ( ) ;
245+ const prefixSpy = vi . spyOn ( store , 'updatePrefix' ) ;
246+ const suffixSpy = vi . spyOn ( store , 'updateSuffix' ) ;
247+
248+ const { wrapper } = mountComposable ( {
249+ resetText : { suffix : 'stay' } ,
250+ persistenceEnabled : false
251+ } ) ;
252+
253+ await nextTick ( ) ;
254+ prefixSpy . mockClear ( ) ;
255+ suffixSpy . mockClear ( ) ;
256+
257+ wrapper . unmount ( ) ;
258+ await nextTick ( ) ;
259+
260+ expect ( prefixSpy ) . not . toHaveBeenCalled ( ) ;
261+ expect ( suffixSpy ) . toHaveBeenCalledWith ( 'stay' ) ;
262+ } ) ;
263+
264+ it ( 'does not reset suffix when resetText omits it' , async ( ) => {
265+ const store = useStore ( ) ;
266+ const prefixSpy = vi . spyOn ( store , 'updatePrefix' ) ;
267+ const suffixSpy = vi . spyOn ( store , 'updateSuffix' ) ;
268+
269+ const { wrapper } = mountComposable ( {
270+ resetText : { prefix : 'back' } ,
271+ persistenceEnabled : false
272+ } ) ;
273+
274+ await nextTick ( ) ;
275+ prefixSpy . mockClear ( ) ;
276+ suffixSpy . mockClear ( ) ;
277+
278+ wrapper . unmount ( ) ;
279+ await nextTick ( ) ;
280+
281+ expect ( prefixSpy ) . toHaveBeenCalledWith ( 'back' ) ;
282+ expect ( suffixSpy ) . not . toHaveBeenCalled ( ) ;
283+ } ) ;
284+
285+ it ( 'continues hydrating when a task rejects' , async ( ) => {
286+ const store = useStore ( ) ;
287+ const originalPrefix = store . updatePrefix ;
288+ const rejection = new Error ( 'hydrate failure' ) ;
289+ store . updatePrefix = vi . fn ( ( ) => Promise . reject ( rejection ) ) ;
290+ const suffixSpy = vi . spyOn ( store , 'updateSuffix' ) ;
291+
292+ mountComposable ( { initialText : { prefix : 'Only' , suffix : 'Fans' } } ) ;
293+ await new Promise ( ( resolve ) => setTimeout ( resolve , 0 ) ) ;
294+
295+ expect ( store . updatePrefix ) . toHaveBeenCalledWith ( 'Only' ) ;
296+ expect ( suffixSpy ) . toHaveBeenCalledWith ( 'Fans' ) ;
297+
298+ store . updatePrefix = originalPrefix ;
299+ } ) ;
135300} ) ;
0 commit comments