@shopify/shop-minis-react 0.0.33 → 0.0.35
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.
- package/dist/_virtual/index10.js +2 -2
- package/dist/_virtual/index2.js +4 -4
- package/dist/_virtual/index3.js +4 -4
- package/dist/_virtual/index8.js +2 -2
- package/dist/_virtual/index9.js +2 -2
- package/dist/components/atoms/alert-dialog.js.map +1 -1
- package/dist/components/atoms/icon-button.js +12 -12
- package/dist/components/atoms/icon-button.js.map +1 -1
- package/dist/components/atoms/image.js +52 -0
- package/dist/components/atoms/image.js.map +1 -0
- package/dist/components/atoms/text-input.js +22 -0
- package/dist/components/atoms/text-input.js.map +1 -0
- package/dist/components/commerce/merchant-card.js +2 -1
- package/dist/components/commerce/merchant-card.js.map +1 -1
- package/dist/components/commerce/product-card.js +11 -11
- package/dist/components/commerce/product-card.js.map +1 -1
- package/dist/components/content/image-content-wrapper.js +29 -22
- package/dist/components/content/image-content-wrapper.js.map +1 -1
- package/dist/components/ui/input.js +15 -9
- package/dist/components/ui/input.js.map +1 -1
- package/dist/hooks/content/useCreateImageContent.js +16 -22
- package/dist/hooks/content/useCreateImageContent.js.map +1 -1
- package/dist/hooks/storage/useImageUpload.js +36 -37
- package/dist/hooks/storage/useImageUpload.js.map +1 -1
- package/dist/hooks/util/useKeyboardAvoidingView.js +23 -0
- package/dist/hooks/util/useKeyboardAvoidingView.js.map +1 -0
- package/dist/index.js +218 -212
- package/dist/index.js.map +1 -1
- package/dist/mocks.js +4 -1
- package/dist/mocks.js.map +1 -1
- package/dist/shop-minis-platform/src/types/content.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@xmldom_xmldom@0.8.10/node_modules/@xmldom/xmldom/lib/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/color-string@1.9.1/node_modules/color-string/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/use-sync-external-store@1.5.0_react@19.1.0/node_modules/use-sync-external-store/shim/index.js +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/video.js@8.23.3/node_modules/video.js/dist/video.es.js +1 -1
- package/dist/utils/colors.js +1 -1
- package/dist/utils/image.js +46 -9
- package/dist/utils/image.js.map +1 -1
- package/package.json +21 -4
- package/src/components/atoms/alert-dialog.test.tsx +67 -0
- package/src/components/atoms/alert-dialog.tsx +13 -11
- package/src/components/atoms/favorite-button.test.tsx +56 -0
- package/src/components/atoms/icon-button.tsx +1 -1
- package/src/components/atoms/image.test.tsx +108 -0
- package/src/components/atoms/{thumbhash-image.tsx → image.tsx} +14 -14
- package/src/components/atoms/product-variant-price.test.tsx +128 -0
- package/src/components/atoms/text-input.test.tsx +104 -0
- package/src/components/atoms/text-input.tsx +31 -0
- package/src/components/commerce/merchant-card.test.tsx +261 -0
- package/src/components/commerce/merchant-card.tsx +4 -2
- package/src/components/commerce/product-card.test.tsx +364 -0
- package/src/components/commerce/product-card.tsx +2 -2
- package/src/components/commerce/product-link.test.tsx +483 -0
- package/src/components/commerce/quantity-selector.test.tsx +382 -0
- package/src/components/commerce/search.test.tsx +487 -0
- package/src/components/content/image-content-wrapper.test.tsx +92 -0
- package/src/components/content/image-content-wrapper.tsx +9 -2
- package/src/components/index.ts +2 -1
- package/src/components/navigation/transition-link.test.tsx +155 -0
- package/src/components/ui/input.test.tsx +21 -0
- package/src/components/ui/input.tsx +10 -1
- package/src/hooks/content/useCreateImageContent.test.ts +352 -0
- package/src/hooks/content/useCreateImageContent.ts +1 -7
- package/src/hooks/index.ts +1 -0
- package/src/hooks/navigation/useNavigateWithTransition.test.ts +371 -0
- package/src/hooks/navigation/useViewTransitions.test.ts +469 -0
- package/src/hooks/product/useProductSearch.test.ts +470 -0
- package/src/hooks/storage/useAsyncStorage.test.ts +225 -0
- package/src/hooks/storage/useImageUpload.test.ts +322 -0
- package/src/hooks/storage/useImageUpload.ts +22 -20
- package/src/hooks/util/useKeyboardAvoidingView.ts +37 -0
- package/src/internal/useHandleAction.test.ts +265 -0
- package/src/internal/useShopActionsDataFetching.test.ts +465 -0
- package/src/mocks.ts +3 -1
- package/src/providers/ImagePickerProvider.test.tsx +467 -0
- package/src/stories/ProductCard.stories.tsx +2 -2
- package/src/stories/TextInput.stories.tsx +26 -0
- package/src/test-setup.ts +34 -0
- package/src/test-utils.tsx +167 -0
- package/src/utils/image.ts +73 -0
- package/src/utils/index.ts +1 -1
- package/dist/components/atoms/thumbhash-image.js +0 -54
- package/dist/components/atoms/thumbhash-image.js.map +0 -1
- package/dist/utils/imageToDataUri.js +0 -10
- package/dist/utils/imageToDataUri.js.map +0 -1
- package/src/utils/imageToDataUri.ts +0 -8
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import {renderHook, act} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useViewTransitions} from './useViewTransitions'
|
|
5
|
+
|
|
6
|
+
// Mock react-router
|
|
7
|
+
const mockNavigationType = vi.fn()
|
|
8
|
+
const mockLocation = {pathname: '/test'}
|
|
9
|
+
|
|
10
|
+
vi.mock('react-router', () => ({
|
|
11
|
+
useNavigationType: () => mockNavigationType(),
|
|
12
|
+
useLocation: () => mockLocation,
|
|
13
|
+
}))
|
|
14
|
+
|
|
15
|
+
describe('useViewTransitions', () => {
|
|
16
|
+
let originalStartViewTransition: any
|
|
17
|
+
let addEventListenerSpy: ReturnType<typeof vi.spyOn>
|
|
18
|
+
let removeEventListenerSpy: ReturnType<typeof vi.spyOn>
|
|
19
|
+
let setAttributeSpy: ReturnType<typeof vi.spyOn>
|
|
20
|
+
let removeAttributeSpy: ReturnType<typeof vi.spyOn>
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks()
|
|
24
|
+
originalStartViewTransition = document.startViewTransition
|
|
25
|
+
|
|
26
|
+
// Reset mock navigation type
|
|
27
|
+
mockNavigationType.mockReturnValue('PUSH')
|
|
28
|
+
|
|
29
|
+
// Setup spies
|
|
30
|
+
addEventListenerSpy = vi.spyOn(window, 'addEventListener')
|
|
31
|
+
removeEventListenerSpy = vi.spyOn(window, 'removeEventListener')
|
|
32
|
+
setAttributeSpy = vi.spyOn(document.documentElement, 'setAttribute')
|
|
33
|
+
removeAttributeSpy = vi.spyOn(document.documentElement, 'removeAttribute')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
document.startViewTransition = originalStartViewTransition
|
|
38
|
+
document.documentElement.removeAttribute('data-navigation-type')
|
|
39
|
+
addEventListenerSpy.mockRestore()
|
|
40
|
+
removeEventListenerSpy.mockRestore()
|
|
41
|
+
setAttributeSpy.mockRestore()
|
|
42
|
+
removeAttributeSpy.mockRestore()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe('Event Listeners', () => {
|
|
46
|
+
it('registers event listeners on mount', () => {
|
|
47
|
+
renderHook(() => useViewTransitions())
|
|
48
|
+
|
|
49
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
50
|
+
'androidbackpressed',
|
|
51
|
+
expect.any(Function)
|
|
52
|
+
)
|
|
53
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith(
|
|
54
|
+
'popstate',
|
|
55
|
+
expect.any(Function)
|
|
56
|
+
)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('removes event listeners on unmount', () => {
|
|
60
|
+
const {unmount} = renderHook(() => useViewTransitions())
|
|
61
|
+
|
|
62
|
+
unmount()
|
|
63
|
+
|
|
64
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
65
|
+
'popstate',
|
|
66
|
+
expect.any(Function)
|
|
67
|
+
)
|
|
68
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
69
|
+
'androidbackpressed',
|
|
70
|
+
expect.any(Function)
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('Navigation Type Attribute', () => {
|
|
76
|
+
it('sets forward navigation type for PUSH', () => {
|
|
77
|
+
mockNavigationType.mockReturnValue('PUSH')
|
|
78
|
+
|
|
79
|
+
renderHook(() => useViewTransitions())
|
|
80
|
+
|
|
81
|
+
expect(setAttributeSpy).toHaveBeenCalledWith(
|
|
82
|
+
'data-navigation-type',
|
|
83
|
+
'forward'
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('sets backward navigation type for POP', () => {
|
|
88
|
+
mockNavigationType.mockReturnValue('POP')
|
|
89
|
+
|
|
90
|
+
renderHook(() => useViewTransitions())
|
|
91
|
+
|
|
92
|
+
expect(setAttributeSpy).toHaveBeenCalledWith(
|
|
93
|
+
'data-navigation-type',
|
|
94
|
+
'backward'
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('does not set attribute for REPLACE navigation', () => {
|
|
99
|
+
mockNavigationType.mockReturnValue('REPLACE')
|
|
100
|
+
|
|
101
|
+
renderHook(() => useViewTransitions())
|
|
102
|
+
|
|
103
|
+
expect(setAttributeSpy).not.toHaveBeenCalledWith(
|
|
104
|
+
'data-navigation-type',
|
|
105
|
+
expect.any(String)
|
|
106
|
+
)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('does not override existing navigation type attribute', () => {
|
|
110
|
+
// Set existing attribute
|
|
111
|
+
document.documentElement.setAttribute('data-navigation-type', 'existing')
|
|
112
|
+
setAttributeSpy.mockClear()
|
|
113
|
+
|
|
114
|
+
mockNavigationType.mockReturnValue('PUSH')
|
|
115
|
+
|
|
116
|
+
renderHook(() => useViewTransitions())
|
|
117
|
+
|
|
118
|
+
// Should not set attribute again
|
|
119
|
+
expect(setAttributeSpy).not.toHaveBeenCalledWith(
|
|
120
|
+
'data-navigation-type',
|
|
121
|
+
'forward'
|
|
122
|
+
)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('removes navigation type attribute on unmount', () => {
|
|
126
|
+
const {unmount} = renderHook(() => useViewTransitions())
|
|
127
|
+
|
|
128
|
+
unmount()
|
|
129
|
+
|
|
130
|
+
expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
describe('Android Back Press', () => {
|
|
135
|
+
beforeEach(() => {
|
|
136
|
+
// Mock startViewTransition
|
|
137
|
+
document.startViewTransition = vi.fn((callback: () => void) => {
|
|
138
|
+
callback()
|
|
139
|
+
return {
|
|
140
|
+
finished: Promise.resolve(),
|
|
141
|
+
ready: Promise.resolve(),
|
|
142
|
+
types: new Set<string>(),
|
|
143
|
+
updateCallbackDone: Promise.resolve(),
|
|
144
|
+
skipTransition: () => {},
|
|
145
|
+
}
|
|
146
|
+
}) as any
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('handles android back press with view transition', async () => {
|
|
150
|
+
renderHook(() => useViewTransitions())
|
|
151
|
+
|
|
152
|
+
// Get the registered handler
|
|
153
|
+
const androidBackHandler = addEventListenerSpy.mock.calls.find(
|
|
154
|
+
call => call[0] === 'androidbackpressed'
|
|
155
|
+
)?.[1] as EventListener
|
|
156
|
+
|
|
157
|
+
expect(androidBackHandler).toBeDefined()
|
|
158
|
+
|
|
159
|
+
// Trigger android back press
|
|
160
|
+
await act(async () => {
|
|
161
|
+
androidBackHandler(new Event('androidbackpressed'))
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(document.startViewTransition).toHaveBeenCalled()
|
|
165
|
+
expect(setAttributeSpy).toHaveBeenCalledWith(
|
|
166
|
+
'data-navigation-type',
|
|
167
|
+
'backward'
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('removes attribute after android back transition completes', async () => {
|
|
172
|
+
const transitionPromise = Promise.resolve()
|
|
173
|
+
const mockTransition = {
|
|
174
|
+
finished: transitionPromise,
|
|
175
|
+
ready: Promise.resolve(),
|
|
176
|
+
types: new Set<string>(),
|
|
177
|
+
updateCallbackDone: Promise.resolve(),
|
|
178
|
+
skipTransition: () => {},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
document.startViewTransition = vi.fn((callback: () => void) => {
|
|
182
|
+
callback()
|
|
183
|
+
return mockTransition
|
|
184
|
+
}) as any
|
|
185
|
+
|
|
186
|
+
renderHook(() => useViewTransitions())
|
|
187
|
+
|
|
188
|
+
const androidBackHandler = addEventListenerSpy.mock.calls.find(
|
|
189
|
+
call => call[0] === 'androidbackpressed'
|
|
190
|
+
)?.[1] as EventListener
|
|
191
|
+
|
|
192
|
+
await act(async () => {
|
|
193
|
+
androidBackHandler(new Event('androidbackpressed'))
|
|
194
|
+
await transitionPromise
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('handles view transition error on android back press', async () => {
|
|
201
|
+
const consoleErrorSpy = vi
|
|
202
|
+
.spyOn(console, 'error')
|
|
203
|
+
.mockImplementation(() => {})
|
|
204
|
+
|
|
205
|
+
const errorTransition = {
|
|
206
|
+
finished: Promise.reject(new Error('Transition failed')),
|
|
207
|
+
ready: Promise.resolve(),
|
|
208
|
+
types: new Set<string>(),
|
|
209
|
+
updateCallbackDone: Promise.resolve(),
|
|
210
|
+
skipTransition: () => {},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
document.startViewTransition = vi.fn((callback: () => void) => {
|
|
214
|
+
callback()
|
|
215
|
+
return errorTransition
|
|
216
|
+
}) as any
|
|
217
|
+
|
|
218
|
+
renderHook(() => useViewTransitions())
|
|
219
|
+
|
|
220
|
+
const androidBackHandler = addEventListenerSpy.mock.calls.find(
|
|
221
|
+
call => call[0] === 'androidbackpressed'
|
|
222
|
+
)?.[1] as EventListener
|
|
223
|
+
|
|
224
|
+
await act(async () => {
|
|
225
|
+
androidBackHandler(new Event('androidbackpressed'))
|
|
226
|
+
try {
|
|
227
|
+
await errorTransition.finished
|
|
228
|
+
} catch {
|
|
229
|
+
// Expected error
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
234
|
+
'View transition error:',
|
|
235
|
+
expect.any(Error)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
consoleErrorSpy.mockRestore()
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('handles android back press without view transition support', () => {
|
|
242
|
+
// Remove startViewTransition
|
|
243
|
+
;(document as any).startViewTransition = undefined
|
|
244
|
+
|
|
245
|
+
renderHook(() => useViewTransitions())
|
|
246
|
+
|
|
247
|
+
const androidBackHandler = addEventListenerSpy.mock.calls.find(
|
|
248
|
+
call => call[0] === 'androidbackpressed'
|
|
249
|
+
)?.[1] as EventListener
|
|
250
|
+
|
|
251
|
+
// Should not throw
|
|
252
|
+
expect(() => {
|
|
253
|
+
androidBackHandler(new Event('androidbackpressed'))
|
|
254
|
+
}).not.toThrow()
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('Popstate Event', () => {
|
|
259
|
+
it('sets none navigation type for iOS back gesture', () => {
|
|
260
|
+
renderHook(() => useViewTransitions())
|
|
261
|
+
|
|
262
|
+
const popstateHandler = addEventListenerSpy.mock.calls.find(
|
|
263
|
+
call => call[0] === 'popstate'
|
|
264
|
+
)?.[1] as EventListener
|
|
265
|
+
|
|
266
|
+
expect(popstateHandler).toBeDefined()
|
|
267
|
+
|
|
268
|
+
// Create popstate event with hasUAVisualTransition
|
|
269
|
+
const popstateEvent = new PopStateEvent('popstate', {state: null})
|
|
270
|
+
;(popstateEvent as any).hasUAVisualTransition = true
|
|
271
|
+
|
|
272
|
+
act(() => {
|
|
273
|
+
popstateHandler(popstateEvent)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
expect(setAttributeSpy).toHaveBeenCalledWith(
|
|
277
|
+
'data-navigation-type',
|
|
278
|
+
'none'
|
|
279
|
+
)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('does not set attribute for popstate without iOS gesture', () => {
|
|
283
|
+
renderHook(() => useViewTransitions())
|
|
284
|
+
|
|
285
|
+
const popstateHandler = addEventListenerSpy.mock.calls.find(
|
|
286
|
+
call => call[0] === 'popstate'
|
|
287
|
+
)?.[1] as EventListener
|
|
288
|
+
|
|
289
|
+
setAttributeSpy.mockClear()
|
|
290
|
+
|
|
291
|
+
// Regular popstate without hasUAVisualTransition
|
|
292
|
+
const popstateEvent = new PopStateEvent('popstate', {state: null})
|
|
293
|
+
|
|
294
|
+
act(() => {
|
|
295
|
+
popstateHandler(popstateEvent)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
expect(setAttributeSpy).not.toHaveBeenCalledWith(
|
|
299
|
+
'data-navigation-type',
|
|
300
|
+
'none'
|
|
301
|
+
)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
it('does not set none type after android back press', () => {
|
|
305
|
+
document.startViewTransition = vi.fn((callback: () => void) => {
|
|
306
|
+
callback()
|
|
307
|
+
return {
|
|
308
|
+
finished: Promise.resolve(),
|
|
309
|
+
ready: Promise.resolve(),
|
|
310
|
+
types: new Set<string>(),
|
|
311
|
+
updateCallbackDone: Promise.resolve(),
|
|
312
|
+
skipTransition: () => {},
|
|
313
|
+
}
|
|
314
|
+
}) as any
|
|
315
|
+
|
|
316
|
+
renderHook(() => useViewTransitions())
|
|
317
|
+
|
|
318
|
+
const androidBackHandler = addEventListenerSpy.mock.calls.find(
|
|
319
|
+
call => call[0] === 'androidbackpressed'
|
|
320
|
+
)?.[1] as EventListener
|
|
321
|
+
|
|
322
|
+
const popstateHandler = addEventListenerSpy.mock.calls.find(
|
|
323
|
+
call => call[0] === 'popstate'
|
|
324
|
+
)?.[1] as EventListener
|
|
325
|
+
|
|
326
|
+
// First trigger android back
|
|
327
|
+
act(() => {
|
|
328
|
+
androidBackHandler(new Event('androidbackpressed'))
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
setAttributeSpy.mockClear()
|
|
332
|
+
|
|
333
|
+
// Then trigger popstate with iOS gesture
|
|
334
|
+
const popstateEvent = new PopStateEvent('popstate', {state: null})
|
|
335
|
+
;(popstateEvent as any).hasUAVisualTransition = true
|
|
336
|
+
|
|
337
|
+
act(() => {
|
|
338
|
+
popstateHandler(popstateEvent)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
// Should not set none type because android back was pressed
|
|
342
|
+
expect(setAttributeSpy).not.toHaveBeenCalledWith(
|
|
343
|
+
'data-navigation-type',
|
|
344
|
+
'none'
|
|
345
|
+
)
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
describe('Navigation Type Changes', () => {
|
|
350
|
+
it('updates attribute when navigation type changes', () => {
|
|
351
|
+
mockNavigationType.mockReturnValue('PUSH')
|
|
352
|
+
|
|
353
|
+
const {rerender} = renderHook(() => useViewTransitions())
|
|
354
|
+
|
|
355
|
+
expect(setAttributeSpy).toHaveBeenCalledWith(
|
|
356
|
+
'data-navigation-type',
|
|
357
|
+
'forward'
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
setAttributeSpy.mockClear()
|
|
361
|
+
|
|
362
|
+
// Change navigation type
|
|
363
|
+
mockNavigationType.mockReturnValue('POP')
|
|
364
|
+
rerender()
|
|
365
|
+
|
|
366
|
+
expect(setAttributeSpy).toHaveBeenCalledWith(
|
|
367
|
+
'data-navigation-type',
|
|
368
|
+
'backward'
|
|
369
|
+
)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
it('removes and re-adds attribute on navigation type change', () => {
|
|
373
|
+
mockNavigationType.mockReturnValue('PUSH')
|
|
374
|
+
|
|
375
|
+
const {rerender} = renderHook(() => useViewTransitions())
|
|
376
|
+
|
|
377
|
+
// Clear initial calls
|
|
378
|
+
removeAttributeSpy.mockClear()
|
|
379
|
+
setAttributeSpy.mockClear()
|
|
380
|
+
|
|
381
|
+
// Change navigation type
|
|
382
|
+
mockNavigationType.mockReturnValue('POP')
|
|
383
|
+
rerender()
|
|
384
|
+
|
|
385
|
+
// Should remove old attribute
|
|
386
|
+
expect(removeAttributeSpy).toHaveBeenCalledWith('data-navigation-type')
|
|
387
|
+
// And set new one
|
|
388
|
+
expect(setAttributeSpy).toHaveBeenCalledWith(
|
|
389
|
+
'data-navigation-type',
|
|
390
|
+
'backward'
|
|
391
|
+
)
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
describe('Edge Cases', () => {
|
|
396
|
+
it('handles multiple rapid location changes', () => {
|
|
397
|
+
const {rerender} = renderHook(() => useViewTransitions())
|
|
398
|
+
|
|
399
|
+
// Simulate rapid location changes
|
|
400
|
+
for (let i = 0; i < 5; i++) {
|
|
401
|
+
mockLocation.pathname = `/path-${i}`
|
|
402
|
+
rerender()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Should still have proper listeners registered
|
|
406
|
+
const androidListeners = addEventListenerSpy.mock.calls.filter(
|
|
407
|
+
call => call[0] === 'androidbackpressed'
|
|
408
|
+
)
|
|
409
|
+
const popstateListeners = addEventListenerSpy.mock.calls.filter(
|
|
410
|
+
call => call[0] === 'popstate'
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
expect(androidListeners.length).toBeGreaterThan(0)
|
|
414
|
+
expect(popstateListeners.length).toBeGreaterThan(0)
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
it('handles missing startViewTransition gracefully', () => {
|
|
418
|
+
;(document as any).startViewTransition = undefined
|
|
419
|
+
|
|
420
|
+
// Should not throw
|
|
421
|
+
expect(() => {
|
|
422
|
+
renderHook(() => useViewTransitions())
|
|
423
|
+
}).not.toThrow()
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it('cleans up properly on unmount during transition', async () => {
|
|
427
|
+
let resolveTransition: (() => void) | undefined
|
|
428
|
+
const transitionPromise = new Promise<void>(resolve => {
|
|
429
|
+
resolveTransition = resolve
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
document.startViewTransition = vi.fn((callback: () => void) => {
|
|
433
|
+
callback()
|
|
434
|
+
return {
|
|
435
|
+
finished: transitionPromise,
|
|
436
|
+
ready: Promise.resolve(),
|
|
437
|
+
types: new Set<string>(),
|
|
438
|
+
updateCallbackDone: Promise.resolve(),
|
|
439
|
+
skipTransition: () => {},
|
|
440
|
+
}
|
|
441
|
+
}) as any
|
|
442
|
+
|
|
443
|
+
const {unmount} = renderHook(() => useViewTransitions())
|
|
444
|
+
|
|
445
|
+
const androidBackHandler = addEventListenerSpy.mock.calls.find(
|
|
446
|
+
call => call[0] === 'androidbackpressed'
|
|
447
|
+
)?.[1] as EventListener
|
|
448
|
+
|
|
449
|
+
// Start transition
|
|
450
|
+
act(() => {
|
|
451
|
+
androidBackHandler(new Event('androidbackpressed'))
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
// Unmount before transition completes
|
|
455
|
+
unmount()
|
|
456
|
+
|
|
457
|
+
// Complete transition after unmount
|
|
458
|
+
if (resolveTransition) {
|
|
459
|
+
resolveTransition()
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Should have removed listeners
|
|
463
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith(
|
|
464
|
+
'androidbackpressed',
|
|
465
|
+
expect.any(Function)
|
|
466
|
+
)
|
|
467
|
+
})
|
|
468
|
+
})
|
|
469
|
+
})
|