@shopify/shop-minis-react 0.1.7 → 0.2.0
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/components/atoms/image.js +35 -33
- package/dist/components/atoms/image.js.map +1 -1
- package/dist/hooks/navigation/useDeeplink.js +9 -9
- package/dist/hooks/navigation/useDeeplink.js.map +1 -1
- package/dist/providers/ImagePickerProvider.js +54 -55
- package/dist/providers/ImagePickerProvider.js.map +1 -1
- package/eslint/README.md +201 -0
- package/eslint/config.cjs +32 -0
- package/eslint/index.cjs +17 -0
- package/eslint/rules/no-internal-imports.cjs +43 -0
- package/eslint/rules/prefer-sdk-components.cjs +153 -0
- package/eslint/rules/validate-manifest.cjs +607 -0
- package/generated-hook-maps/hook-actions-map.json +130 -0
- package/generated-hook-maps/hook-scopes-map.json +48 -0
- package/package.json +10 -4
- package/src/components/atoms/image.test.tsx +41 -1
- package/src/components/atoms/image.tsx +8 -3
- package/src/hooks/navigation/useDeeplink.test.ts +429 -0
- package/src/hooks/navigation/useDeeplink.ts +6 -3
- package/src/providers/ImagePickerProvider.test.tsx +82 -155
- package/src/providers/ImagePickerProvider.tsx +21 -33
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import {renderHook} from '@testing-library/react'
|
|
2
|
+
import {describe, expect, it, vi, beforeEach, afterEach} from 'vitest'
|
|
3
|
+
|
|
4
|
+
import {useDeeplink} from './useDeeplink'
|
|
5
|
+
|
|
6
|
+
vi.mock('../../utils/parseUrl', () => ({
|
|
7
|
+
parseUrl: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
const {parseUrl} = await import('../../utils/parseUrl')
|
|
11
|
+
|
|
12
|
+
describe('useDeeplink', () => {
|
|
13
|
+
const originalMinisParams = (window as any).minisParams
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
vi.clearAllMocks()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
;(window as any).minisParams = originalMinisParams
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('Without initialUrl', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
;(window as any).minisParams = {
|
|
26
|
+
initialUrl: undefined,
|
|
27
|
+
handle: 'test-mini',
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('returns undefined values when initialUrl is not provided', () => {
|
|
32
|
+
const {result} = renderHook(() => useDeeplink())
|
|
33
|
+
|
|
34
|
+
expect(result.current).toEqual({
|
|
35
|
+
path: undefined,
|
|
36
|
+
queryParams: undefined,
|
|
37
|
+
hash: undefined,
|
|
38
|
+
})
|
|
39
|
+
expect(parseUrl).not.toHaveBeenCalled()
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('With initialUrl', () => {
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
;(window as any).minisParams = {
|
|
46
|
+
initialUrl:
|
|
47
|
+
'https://shop.app/mini/test-mini/products?category=clothing',
|
|
48
|
+
handle: 'test-mini',
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('parses URL and strips mini handle prefix from pathname', () => {
|
|
53
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
54
|
+
pathname: '/mini/test-mini/products',
|
|
55
|
+
query: {category: 'clothing'},
|
|
56
|
+
hash: '',
|
|
57
|
+
} as any)
|
|
58
|
+
|
|
59
|
+
const {result} = renderHook(() => useDeeplink())
|
|
60
|
+
|
|
61
|
+
expect(parseUrl).toHaveBeenCalledWith(
|
|
62
|
+
'https://shop.app/mini/test-mini/products?category=clothing'
|
|
63
|
+
)
|
|
64
|
+
expect(result.current).toEqual({
|
|
65
|
+
path: '/products',
|
|
66
|
+
queryParams: {category: 'clothing'},
|
|
67
|
+
hash: '',
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns pathname as-is when it does not contain mini handle prefix', () => {
|
|
72
|
+
;(window as any).minisParams = {
|
|
73
|
+
initialUrl: 'https://shop.app/some-other-path',
|
|
74
|
+
handle: 'test-mini',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
78
|
+
pathname: '/some-other-path',
|
|
79
|
+
query: {},
|
|
80
|
+
hash: '',
|
|
81
|
+
} as any)
|
|
82
|
+
|
|
83
|
+
const {result} = renderHook(() => useDeeplink())
|
|
84
|
+
|
|
85
|
+
expect(result.current).toEqual({
|
|
86
|
+
path: '/some-other-path',
|
|
87
|
+
queryParams: {},
|
|
88
|
+
hash: '',
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it('handles URL with multiple query parameters', () => {
|
|
93
|
+
;(window as any).minisParams = {
|
|
94
|
+
initialUrl:
|
|
95
|
+
'https://shop.app/mini/test-mini/search?q=shoes&page=2&sort=price',
|
|
96
|
+
handle: 'test-mini',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
100
|
+
pathname: '/mini/test-mini/search',
|
|
101
|
+
query: {q: 'shoes', page: '2', sort: 'price'},
|
|
102
|
+
hash: '',
|
|
103
|
+
} as any)
|
|
104
|
+
|
|
105
|
+
const {result} = renderHook(() => useDeeplink())
|
|
106
|
+
|
|
107
|
+
expect(result.current).toEqual({
|
|
108
|
+
path: '/search',
|
|
109
|
+
queryParams: {q: 'shoes', page: '2', sort: 'price'},
|
|
110
|
+
hash: '',
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('handles URL with hash', () => {
|
|
115
|
+
;(window as any).minisParams = {
|
|
116
|
+
initialUrl: 'https://shop.app/mini/test-mini/page#section-2',
|
|
117
|
+
handle: 'test-mini',
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
121
|
+
pathname: '/mini/test-mini/page',
|
|
122
|
+
query: {},
|
|
123
|
+
hash: '#section-2',
|
|
124
|
+
} as any)
|
|
125
|
+
|
|
126
|
+
const {result} = renderHook(() => useDeeplink())
|
|
127
|
+
|
|
128
|
+
expect(result.current).toEqual({
|
|
129
|
+
path: '/page',
|
|
130
|
+
queryParams: {},
|
|
131
|
+
hash: '#section-2',
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('handles URL with both query params and hash', () => {
|
|
136
|
+
;(window as any).minisParams = {
|
|
137
|
+
initialUrl: 'https://shop.app/mini/test-mini/products?filter=sale#top',
|
|
138
|
+
handle: 'test-mini',
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
142
|
+
pathname: '/mini/test-mini/products',
|
|
143
|
+
query: {filter: 'sale'},
|
|
144
|
+
hash: '#top',
|
|
145
|
+
} as any)
|
|
146
|
+
|
|
147
|
+
const {result} = renderHook(() => useDeeplink())
|
|
148
|
+
|
|
149
|
+
expect(result.current).toEqual({
|
|
150
|
+
path: '/products',
|
|
151
|
+
queryParams: {filter: 'sale'},
|
|
152
|
+
hash: '#top',
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('handles root path after stripping prefix', () => {
|
|
157
|
+
;(window as any).minisParams = {
|
|
158
|
+
initialUrl: 'https://shop.app/mini/test-mini',
|
|
159
|
+
handle: 'test-mini',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
163
|
+
pathname: '/mini/test-mini',
|
|
164
|
+
query: {},
|
|
165
|
+
hash: '',
|
|
166
|
+
} as any)
|
|
167
|
+
|
|
168
|
+
const {result} = renderHook(() => useDeeplink())
|
|
169
|
+
|
|
170
|
+
expect(result.current).toEqual({
|
|
171
|
+
path: '',
|
|
172
|
+
queryParams: {},
|
|
173
|
+
hash: '',
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('handles root path with trailing slash', () => {
|
|
178
|
+
;(window as any).minisParams = {
|
|
179
|
+
initialUrl: 'https://shop.app/mini/test-mini/',
|
|
180
|
+
handle: 'test-mini',
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
184
|
+
pathname: '/mini/test-mini/',
|
|
185
|
+
query: {},
|
|
186
|
+
hash: '',
|
|
187
|
+
} as any)
|
|
188
|
+
|
|
189
|
+
const {result} = renderHook(() => useDeeplink())
|
|
190
|
+
|
|
191
|
+
expect(result.current).toEqual({
|
|
192
|
+
path: '/',
|
|
193
|
+
queryParams: {},
|
|
194
|
+
hash: '',
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('handles nested paths', () => {
|
|
199
|
+
;(window as any).minisParams = {
|
|
200
|
+
initialUrl:
|
|
201
|
+
'https://shop.app/mini/test-mini/category/shoes/product/123',
|
|
202
|
+
handle: 'test-mini',
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
206
|
+
pathname: '/mini/test-mini/category/shoes/product/123',
|
|
207
|
+
query: {},
|
|
208
|
+
hash: '',
|
|
209
|
+
} as any)
|
|
210
|
+
|
|
211
|
+
const {result} = renderHook(() => useDeeplink())
|
|
212
|
+
|
|
213
|
+
expect(result.current).toEqual({
|
|
214
|
+
path: '/category/shoes/product/123',
|
|
215
|
+
queryParams: {},
|
|
216
|
+
hash: '',
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('handles different mini handle', () => {
|
|
221
|
+
;(window as any).minisParams = {
|
|
222
|
+
initialUrl: 'https://shop.app/mini/another-mini/dashboard',
|
|
223
|
+
handle: 'another-mini',
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
227
|
+
pathname: '/mini/another-mini/dashboard',
|
|
228
|
+
query: {},
|
|
229
|
+
hash: '',
|
|
230
|
+
} as any)
|
|
231
|
+
|
|
232
|
+
const {result} = renderHook(() => useDeeplink())
|
|
233
|
+
|
|
234
|
+
expect(result.current).toEqual({
|
|
235
|
+
path: '/dashboard',
|
|
236
|
+
queryParams: {},
|
|
237
|
+
hash: '',
|
|
238
|
+
})
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
describe('Memoization', () => {
|
|
243
|
+
it('returns same reference when dependencies do not change', () => {
|
|
244
|
+
;(window as any).minisParams = {
|
|
245
|
+
initialUrl: 'https://shop.app/mini/test-mini/products',
|
|
246
|
+
handle: 'test-mini',
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
250
|
+
pathname: '/mini/test-mini/products',
|
|
251
|
+
query: {},
|
|
252
|
+
hash: '',
|
|
253
|
+
} as any)
|
|
254
|
+
|
|
255
|
+
const {result, rerender} = renderHook(() => useDeeplink())
|
|
256
|
+
const firstResult = result.current
|
|
257
|
+
|
|
258
|
+
rerender()
|
|
259
|
+
|
|
260
|
+
expect(result.current).toBe(firstResult)
|
|
261
|
+
expect(parseUrl).toHaveBeenCalledTimes(1)
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('recomputes when initialUrl changes', () => {
|
|
265
|
+
;(window as any).minisParams = {
|
|
266
|
+
initialUrl: 'https://shop.app/mini/test-mini/products',
|
|
267
|
+
handle: 'test-mini',
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
271
|
+
pathname: '/mini/test-mini/products',
|
|
272
|
+
query: {},
|
|
273
|
+
hash: '',
|
|
274
|
+
} as any)
|
|
275
|
+
|
|
276
|
+
const {result, rerender} = renderHook(() => useDeeplink())
|
|
277
|
+
const firstResult = result.current
|
|
278
|
+
|
|
279
|
+
// Change initialUrl
|
|
280
|
+
;(window as any).minisParams = {
|
|
281
|
+
initialUrl: 'https://shop.app/mini/test-mini/cart',
|
|
282
|
+
handle: 'test-mini',
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
286
|
+
pathname: '/mini/test-mini/cart',
|
|
287
|
+
query: {},
|
|
288
|
+
hash: '',
|
|
289
|
+
} as any)
|
|
290
|
+
|
|
291
|
+
rerender()
|
|
292
|
+
|
|
293
|
+
expect(result.current).not.toBe(firstResult)
|
|
294
|
+
expect(result.current.path).toBe('/cart')
|
|
295
|
+
expect(parseUrl).toHaveBeenCalledTimes(2)
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('recomputes when handle changes', () => {
|
|
299
|
+
;(window as any).minisParams = {
|
|
300
|
+
initialUrl: 'https://shop.app/mini/test-mini/products',
|
|
301
|
+
handle: 'test-mini',
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
305
|
+
pathname: '/mini/test-mini/products',
|
|
306
|
+
query: {},
|
|
307
|
+
hash: '',
|
|
308
|
+
} as any)
|
|
309
|
+
|
|
310
|
+
const {result, rerender} = renderHook(() => useDeeplink())
|
|
311
|
+
const firstResult = result.current
|
|
312
|
+
|
|
313
|
+
// Change handle
|
|
314
|
+
;(window as any).minisParams = {
|
|
315
|
+
initialUrl: 'https://shop.app/mini/test-mini/products',
|
|
316
|
+
handle: 'different-mini',
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
320
|
+
pathname: '/mini/test-mini/products',
|
|
321
|
+
query: {},
|
|
322
|
+
hash: '',
|
|
323
|
+
} as any)
|
|
324
|
+
|
|
325
|
+
rerender()
|
|
326
|
+
|
|
327
|
+
expect(result.current).not.toBe(firstResult)
|
|
328
|
+
expect(parseUrl).toHaveBeenCalledTimes(2)
|
|
329
|
+
})
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
describe('Edge Cases', () => {
|
|
333
|
+
it('handles empty initialUrl string', () => {
|
|
334
|
+
;(window as any).minisParams = {
|
|
335
|
+
initialUrl: '',
|
|
336
|
+
handle: 'test-mini',
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const {result} = renderHook(() => useDeeplink())
|
|
340
|
+
|
|
341
|
+
expect(result.current).toEqual({
|
|
342
|
+
path: undefined,
|
|
343
|
+
queryParams: undefined,
|
|
344
|
+
hash: undefined,
|
|
345
|
+
})
|
|
346
|
+
expect(parseUrl).not.toHaveBeenCalled()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('handles null initialUrl', () => {
|
|
350
|
+
;(window as any).minisParams = {
|
|
351
|
+
initialUrl: null,
|
|
352
|
+
handle: 'test-mini',
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const {result} = renderHook(() => useDeeplink())
|
|
356
|
+
|
|
357
|
+
expect(result.current).toEqual({
|
|
358
|
+
path: undefined,
|
|
359
|
+
queryParams: undefined,
|
|
360
|
+
hash: undefined,
|
|
361
|
+
})
|
|
362
|
+
expect(parseUrl).not.toHaveBeenCalled()
|
|
363
|
+
})
|
|
364
|
+
|
|
365
|
+
it('handles query params with undefined values', () => {
|
|
366
|
+
;(window as any).minisParams = {
|
|
367
|
+
initialUrl: 'https://shop.app/mini/test-mini/search?q=&page=1',
|
|
368
|
+
handle: 'test-mini',
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
372
|
+
pathname: '/mini/test-mini/search',
|
|
373
|
+
query: {q: undefined, page: '1'},
|
|
374
|
+
hash: '',
|
|
375
|
+
} as any)
|
|
376
|
+
|
|
377
|
+
const {result} = renderHook(() => useDeeplink())
|
|
378
|
+
|
|
379
|
+
expect(result.current).toEqual({
|
|
380
|
+
path: '/search',
|
|
381
|
+
queryParams: {q: undefined, page: '1'},
|
|
382
|
+
hash: '',
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('handles pathname with special characters', () => {
|
|
387
|
+
;(window as any).minisParams = {
|
|
388
|
+
initialUrl:
|
|
389
|
+
'https://shop.app/mini/test-mini/search?q=%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF',
|
|
390
|
+
handle: 'test-mini',
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
394
|
+
pathname: '/mini/test-mini/search',
|
|
395
|
+
query: {q: 'S�kao'},
|
|
396
|
+
hash: '',
|
|
397
|
+
} as any)
|
|
398
|
+
|
|
399
|
+
const {result} = renderHook(() => useDeeplink())
|
|
400
|
+
|
|
401
|
+
expect(result.current).toEqual({
|
|
402
|
+
path: '/search',
|
|
403
|
+
queryParams: {q: 'S�kao'},
|
|
404
|
+
hash: '',
|
|
405
|
+
})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('handles case where pathname is just the prefix', () => {
|
|
409
|
+
;(window as any).minisParams = {
|
|
410
|
+
initialUrl: 'https://shop.app/mini/test-mini',
|
|
411
|
+
handle: 'test-mini',
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
vi.mocked(parseUrl).mockReturnValue({
|
|
415
|
+
pathname: '/mini/test-mini',
|
|
416
|
+
query: {},
|
|
417
|
+
hash: '',
|
|
418
|
+
} as any)
|
|
419
|
+
|
|
420
|
+
const {result} = renderHook(() => useDeeplink())
|
|
421
|
+
|
|
422
|
+
expect(result.current).toEqual({
|
|
423
|
+
path: '',
|
|
424
|
+
queryParams: {},
|
|
425
|
+
hash: '',
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
})
|
|
429
|
+
})
|
|
@@ -18,7 +18,7 @@ interface UseDeeplinkReturnType {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export const useDeeplink = (): UseDeeplinkReturnType => {
|
|
21
|
-
const {initialUrl} = window.minisParams
|
|
21
|
+
const {initialUrl, handle} = window.minisParams
|
|
22
22
|
|
|
23
23
|
return useMemo(() => {
|
|
24
24
|
if (!initialUrl) {
|
|
@@ -30,11 +30,14 @@ export const useDeeplink = (): UseDeeplinkReturnType => {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const parsedUrl = parseUrl(initialUrl)
|
|
33
|
+
const deeplinkPathnamePrefix = `/mini/${handle}`
|
|
33
34
|
|
|
34
35
|
return {
|
|
35
|
-
path: parsedUrl.pathname
|
|
36
|
+
path: parsedUrl.pathname.startsWith(deeplinkPathnamePrefix)
|
|
37
|
+
? parsedUrl.pathname.replace(deeplinkPathnamePrefix, '')
|
|
38
|
+
: parsedUrl.pathname,
|
|
36
39
|
queryParams: parsedUrl.query,
|
|
37
40
|
hash: parsedUrl.hash,
|
|
38
41
|
}
|
|
39
|
-
}, [initialUrl])
|
|
42
|
+
}, [handle, initialUrl])
|
|
40
43
|
}
|