@shopify/shop-minis-react 0.2.1 → 0.2.3
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/index4.js +3 -2
- package/dist/_virtual/index4.js.map +1 -1
- package/dist/_virtual/index7.js +2 -3
- package/dist/_virtual/index7.js.map +1 -1
- package/dist/_virtual/index9.js +2 -2
- package/dist/internal/useReportInteraction.js +21 -0
- package/dist/internal/useReportInteraction.js.map +1 -0
- package/dist/providers/ImagePickerProvider.js +122 -63
- package/dist/providers/ImagePickerProvider.js.map +1 -1
- package/dist/shop-minis-react/node_modules/.pnpm/@radix-ui_react-use-is-hydrated@0.1.0_@types_react@19.1.6_react@19.1.0/node_modules/@radix-ui/react-use-is-hydrated/dist/index.js +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/querystringify@2.2.0/node_modules/querystringify/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/utils/getWindowLocationPathname.js +7 -0
- package/dist/utils/getWindowLocationPathname.js.map +1 -0
- package/eslint/rules/prefer-sdk-components.cjs +0 -79
- package/eslint/rules/validate-manifest.cjs +86 -2
- package/generated-hook-maps/component-scopes-map.json +18 -0
- package/package.json +1 -2
- package/src/providers/ImagePickerProvider.test.tsx +391 -0
- package/src/providers/ImagePickerProvider.tsx +93 -12
|
@@ -9,6 +9,14 @@ const path = require('path')
|
|
|
9
9
|
// Load the hook-scopes map
|
|
10
10
|
const hookScopesMap = require('../../generated-hook-maps/hook-scopes-map.json')
|
|
11
11
|
|
|
12
|
+
// Load the component-scopes map (may not exist yet)
|
|
13
|
+
let componentScopesMap = {}
|
|
14
|
+
try {
|
|
15
|
+
componentScopesMap = require('../../generated-hook-maps/component-scopes-map.json')
|
|
16
|
+
} catch (err) {
|
|
17
|
+
// Component scopes map doesn't exist yet, that's okay
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
// Module-level cache for manifest.json to avoid repeated file I/O
|
|
13
21
|
// ESLint reuses the same rule module across all files, so this cache
|
|
14
22
|
// persists for the entire lint run, dramatically improving performance
|
|
@@ -73,7 +81,7 @@ module.exports = {
|
|
|
73
81
|
fixable: 'code',
|
|
74
82
|
messages: {
|
|
75
83
|
missingScope:
|
|
76
|
-
'
|
|
84
|
+
'{{source}} requires scope "{{scope}}" in src/manifest.json. Add "{{scope}}" to the "scopes" array.',
|
|
77
85
|
missingPermission:
|
|
78
86
|
'{{reason}} requires permission "{{permission}}" in src/manifest.json. Add "{{permission}}" to the "permissions" array.',
|
|
79
87
|
missingTrustedDomain:
|
|
@@ -93,6 +101,7 @@ module.exports = {
|
|
|
93
101
|
let manifest = null
|
|
94
102
|
let manifestParseError = null
|
|
95
103
|
const usedHooks = new Set()
|
|
104
|
+
const usedComponents = new Set()
|
|
96
105
|
const requiredPermissions = new Set()
|
|
97
106
|
const requiredDomains = new Set()
|
|
98
107
|
const fixedIssues = new Set()
|
|
@@ -171,6 +180,35 @@ module.exports = {
|
|
|
171
180
|
})
|
|
172
181
|
})
|
|
173
182
|
}
|
|
183
|
+
|
|
184
|
+
// Check if it's a component that requires scopes
|
|
185
|
+
// We need to check all possible paths where the component might be defined
|
|
186
|
+
const possibleComponentPaths = Object.keys(
|
|
187
|
+
componentScopesMap
|
|
188
|
+
).filter(componentPath => {
|
|
189
|
+
// Extract the component name from the path (e.g., "commerce/add-to-cart" -> "AddToCartButton")
|
|
190
|
+
// We'll check if the import name matches common component naming patterns
|
|
191
|
+
const pathParts = componentPath.split('/')
|
|
192
|
+
const fileName = pathParts[pathParts.length - 1]
|
|
193
|
+
// Convert kebab-case or snake_case to PascalCase
|
|
194
|
+
const componentName = fileName
|
|
195
|
+
.split(/[-_]/)
|
|
196
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
|
197
|
+
.join('')
|
|
198
|
+
return (
|
|
199
|
+
componentName === importedName || fileName === importedName
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
if (possibleComponentPaths.length > 0) {
|
|
204
|
+
possibleComponentPaths.forEach(componentPath => {
|
|
205
|
+
usedComponents.add({
|
|
206
|
+
path: componentPath,
|
|
207
|
+
name: importedName,
|
|
208
|
+
node,
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
}
|
|
174
212
|
}
|
|
175
213
|
})
|
|
176
214
|
}
|
|
@@ -408,6 +446,7 @@ module.exports = {
|
|
|
408
446
|
if (
|
|
409
447
|
manifestParseError &&
|
|
410
448
|
(usedHooks.size > 0 ||
|
|
449
|
+
usedComponents.size > 0 ||
|
|
411
450
|
requiredPermissions.size > 0 ||
|
|
412
451
|
requiredDomains.size > 0)
|
|
413
452
|
) {
|
|
@@ -447,6 +486,47 @@ module.exports = {
|
|
|
447
486
|
})
|
|
448
487
|
})
|
|
449
488
|
|
|
489
|
+
// Check scopes for components
|
|
490
|
+
usedComponents.forEach(
|
|
491
|
+
({path: componentPath, name: componentName, node}) => {
|
|
492
|
+
const componentData = componentScopesMap[componentPath]
|
|
493
|
+
if (
|
|
494
|
+
!componentData ||
|
|
495
|
+
!componentData.scopes ||
|
|
496
|
+
componentData.scopes.length === 0
|
|
497
|
+
) {
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const requiredScopes = componentData.scopes
|
|
502
|
+
|
|
503
|
+
if (!manifest) {
|
|
504
|
+
issues.push({
|
|
505
|
+
messageId: 'manifestNotFound',
|
|
506
|
+
data: {
|
|
507
|
+
hookName: componentName,
|
|
508
|
+
scopes: requiredScopes.join(', '),
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const manifestScopes = manifest.scopes || []
|
|
515
|
+
|
|
516
|
+
requiredScopes.forEach(requiredScope => {
|
|
517
|
+
if (!manifestScopes.includes(requiredScope)) {
|
|
518
|
+
issues.push({
|
|
519
|
+
type: 'scope',
|
|
520
|
+
scope: requiredScope,
|
|
521
|
+
componentName,
|
|
522
|
+
componentPath,
|
|
523
|
+
node,
|
|
524
|
+
})
|
|
525
|
+
}
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
)
|
|
529
|
+
|
|
450
530
|
// Check permissions
|
|
451
531
|
requiredPermissions.forEach(({permission, reason, node}) => {
|
|
452
532
|
if (!manifest) {
|
|
@@ -513,11 +593,15 @@ module.exports = {
|
|
|
513
593
|
|
|
514
594
|
// Only report if not fixed
|
|
515
595
|
if (!fixedIssues.has(issueKey)) {
|
|
596
|
+
// Determine if this is from a hook or component
|
|
597
|
+
const sourceName = issue.hookName || issue.componentName
|
|
598
|
+
const sourceType = issue.hookName ? 'Hook' : 'Component'
|
|
599
|
+
|
|
516
600
|
context.report({
|
|
517
601
|
loc: {line: 1, column: 0},
|
|
518
602
|
messageId: 'missingScope',
|
|
519
603
|
data: {
|
|
520
|
-
|
|
604
|
+
source: `${sourceType} "${sourceName}"`,
|
|
521
605
|
scope: issue.scope,
|
|
522
606
|
},
|
|
523
607
|
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"commerce/product-card": {
|
|
3
|
+
"scopes": [
|
|
4
|
+
"product_list:write"
|
|
5
|
+
],
|
|
6
|
+
"hooks": [
|
|
7
|
+
"useSavedProductsActions"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
"commerce/product-link": {
|
|
11
|
+
"scopes": [
|
|
12
|
+
"product_list:write"
|
|
13
|
+
],
|
|
14
|
+
"hooks": [
|
|
15
|
+
"useSavedProductsActions"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopify/shop-minis-react",
|
|
3
3
|
"license": "SEE LICENSE IN LICENSE.txt",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
@@ -79,7 +79,6 @@
|
|
|
79
79
|
},
|
|
80
80
|
"devDependencies": {
|
|
81
81
|
"@playwright/test": "^1.54.2",
|
|
82
|
-
"@shopify/generate-docs": "^0.16.6",
|
|
83
82
|
"@storybook/addon-docs": "^9.0.16",
|
|
84
83
|
"@storybook/react-vite": "^9.0.16",
|
|
85
84
|
"@testing-library/jest-dom": "^6.6.4",
|
|
@@ -17,6 +17,14 @@ vi.mock('../hooks/util/useRequestPermissions', () => ({
|
|
|
17
17
|
}),
|
|
18
18
|
}))
|
|
19
19
|
|
|
20
|
+
// Mock useReportInteraction hook
|
|
21
|
+
const mockReportInteraction = vi.fn()
|
|
22
|
+
vi.mock('../internal/useReportInteraction', () => ({
|
|
23
|
+
useReportInteraction: () => ({
|
|
24
|
+
reportInteraction: mockReportInteraction,
|
|
25
|
+
}),
|
|
26
|
+
}))
|
|
27
|
+
|
|
20
28
|
describe('ImagePickerProvider', () => {
|
|
21
29
|
const originalMinisParams = window.minisParams
|
|
22
30
|
|
|
@@ -24,6 +32,8 @@ describe('ImagePickerProvider', () => {
|
|
|
24
32
|
vi.clearAllMocks()
|
|
25
33
|
// Default to granting permission
|
|
26
34
|
mockRequestPermission.mockResolvedValue({granted: true})
|
|
35
|
+
// Clear interaction reporting mock
|
|
36
|
+
mockReportInteraction.mockClear()
|
|
27
37
|
})
|
|
28
38
|
|
|
29
39
|
afterEach(() => {
|
|
@@ -716,4 +726,385 @@ describe('ImagePickerProvider', () => {
|
|
|
716
726
|
expect(galleryInput.value).toBe('')
|
|
717
727
|
})
|
|
718
728
|
})
|
|
729
|
+
|
|
730
|
+
describe('Interaction Reporting', () => {
|
|
731
|
+
describe('Gallery interactions', () => {
|
|
732
|
+
it('reports image_picker_open when gallery is opened', async () => {
|
|
733
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
734
|
+
|
|
735
|
+
const TestComponent = () => {
|
|
736
|
+
const {openGallery} = useImagePickerContext()
|
|
737
|
+
|
|
738
|
+
return (
|
|
739
|
+
<button
|
|
740
|
+
type="button"
|
|
741
|
+
onClick={() =>
|
|
742
|
+
openGallery().catch(() => {
|
|
743
|
+
// Ignore errors from cleanup
|
|
744
|
+
})
|
|
745
|
+
}
|
|
746
|
+
>
|
|
747
|
+
Open Gallery
|
|
748
|
+
</button>
|
|
749
|
+
)
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
render(
|
|
753
|
+
<ImagePickerProvider>
|
|
754
|
+
<TestComponent />
|
|
755
|
+
</ImagePickerProvider>
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
const button = screen.getByText('Open Gallery')
|
|
759
|
+
fireEvent.click(button)
|
|
760
|
+
|
|
761
|
+
// Wait for interaction to be reported
|
|
762
|
+
await vi.waitFor(() => {
|
|
763
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
764
|
+
interactionType: 'image_picker_open',
|
|
765
|
+
})
|
|
766
|
+
})
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
it('reports image_picker_success when file is selected', async () => {
|
|
770
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
771
|
+
|
|
772
|
+
const TestComponent = () => {
|
|
773
|
+
const {openGallery} = useImagePickerContext()
|
|
774
|
+
|
|
775
|
+
return (
|
|
776
|
+
<button
|
|
777
|
+
type="button"
|
|
778
|
+
onClick={() =>
|
|
779
|
+
openGallery().catch(() => {
|
|
780
|
+
// Ignore errors from cleanup
|
|
781
|
+
})
|
|
782
|
+
}
|
|
783
|
+
>
|
|
784
|
+
Open Gallery
|
|
785
|
+
</button>
|
|
786
|
+
)
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const {container} = render(
|
|
790
|
+
<ImagePickerProvider>
|
|
791
|
+
<TestComponent />
|
|
792
|
+
</ImagePickerProvider>
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
const galleryInput = container.querySelector(
|
|
796
|
+
'input[type="file"]:not([capture])'
|
|
797
|
+
) as HTMLInputElement
|
|
798
|
+
|
|
799
|
+
const button = screen.getByText('Open Gallery')
|
|
800
|
+
fireEvent.click(button)
|
|
801
|
+
|
|
802
|
+
// Wait for permission and initial interaction
|
|
803
|
+
await vi.waitFor(() => {
|
|
804
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
805
|
+
interactionType: 'image_picker_open',
|
|
806
|
+
})
|
|
807
|
+
})
|
|
808
|
+
|
|
809
|
+
// Clear mock to check only for success interaction
|
|
810
|
+
mockReportInteraction.mockClear()
|
|
811
|
+
|
|
812
|
+
// Simulate file selection
|
|
813
|
+
const file = new File(['test'], 'test.jpg', {type: 'image/jpeg'})
|
|
814
|
+
Object.defineProperty(galleryInput, 'files', {
|
|
815
|
+
value: [file],
|
|
816
|
+
configurable: true,
|
|
817
|
+
})
|
|
818
|
+
|
|
819
|
+
await act(async () => {
|
|
820
|
+
fireEvent.change(galleryInput)
|
|
821
|
+
})
|
|
822
|
+
|
|
823
|
+
// Check success was reported
|
|
824
|
+
await vi.waitFor(() => {
|
|
825
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
826
|
+
interactionType: 'image_picker_success',
|
|
827
|
+
})
|
|
828
|
+
})
|
|
829
|
+
})
|
|
830
|
+
|
|
831
|
+
it('reports image_picker_error when interrupted by new picker', async () => {
|
|
832
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
833
|
+
|
|
834
|
+
const TestComponent = () => {
|
|
835
|
+
const {openGallery, openCamera} = useImagePickerContext()
|
|
836
|
+
|
|
837
|
+
return (
|
|
838
|
+
<>
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
onClick={() =>
|
|
842
|
+
openGallery().catch(() => {
|
|
843
|
+
// Expected to fail
|
|
844
|
+
})
|
|
845
|
+
}
|
|
846
|
+
>
|
|
847
|
+
Open Gallery
|
|
848
|
+
</button>
|
|
849
|
+
<button
|
|
850
|
+
type="button"
|
|
851
|
+
onClick={() =>
|
|
852
|
+
openCamera().catch(() => {
|
|
853
|
+
// Ignore errors
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
>
|
|
857
|
+
Open Camera
|
|
858
|
+
</button>
|
|
859
|
+
</>
|
|
860
|
+
)
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
render(
|
|
864
|
+
<ImagePickerProvider>
|
|
865
|
+
<TestComponent />
|
|
866
|
+
</ImagePickerProvider>
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
// Open gallery first
|
|
870
|
+
const galleryButton = screen.getByText('Open Gallery')
|
|
871
|
+
fireEvent.click(galleryButton)
|
|
872
|
+
|
|
873
|
+
// Wait for gallery open to be reported
|
|
874
|
+
await vi.waitFor(() => {
|
|
875
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
876
|
+
interactionType: 'image_picker_open',
|
|
877
|
+
})
|
|
878
|
+
})
|
|
879
|
+
|
|
880
|
+
mockReportInteraction.mockClear()
|
|
881
|
+
|
|
882
|
+
// Open camera to interrupt gallery
|
|
883
|
+
const cameraButton = screen.getByText('Open Camera')
|
|
884
|
+
fireEvent.click(cameraButton)
|
|
885
|
+
|
|
886
|
+
// Check error was reported for interrupted gallery operation
|
|
887
|
+
await vi.waitFor(() => {
|
|
888
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
889
|
+
interactionType: 'image_picker_error',
|
|
890
|
+
interactionValue:
|
|
891
|
+
'New file picker opened before previous completed',
|
|
892
|
+
})
|
|
893
|
+
})
|
|
894
|
+
})
|
|
895
|
+
})
|
|
896
|
+
|
|
897
|
+
describe('Camera interactions', () => {
|
|
898
|
+
it('reports camera_open when camera is opened', async () => {
|
|
899
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
900
|
+
|
|
901
|
+
const TestComponent = () => {
|
|
902
|
+
const {openCamera} = useImagePickerContext()
|
|
903
|
+
|
|
904
|
+
return (
|
|
905
|
+
<button
|
|
906
|
+
type="button"
|
|
907
|
+
onClick={() =>
|
|
908
|
+
openCamera().catch(() => {
|
|
909
|
+
// Ignore errors from cleanup
|
|
910
|
+
})
|
|
911
|
+
}
|
|
912
|
+
>
|
|
913
|
+
Open Camera
|
|
914
|
+
</button>
|
|
915
|
+
)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
render(
|
|
919
|
+
<ImagePickerProvider>
|
|
920
|
+
<TestComponent />
|
|
921
|
+
</ImagePickerProvider>
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
const button = screen.getByText('Open Camera')
|
|
925
|
+
fireEvent.click(button)
|
|
926
|
+
|
|
927
|
+
// Wait for interaction to be reported
|
|
928
|
+
await vi.waitFor(() => {
|
|
929
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
930
|
+
interactionType: 'camera_open',
|
|
931
|
+
})
|
|
932
|
+
})
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it('reports camera_success when photo is captured', async () => {
|
|
936
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
937
|
+
|
|
938
|
+
const TestComponent = () => {
|
|
939
|
+
const {openCamera} = useImagePickerContext()
|
|
940
|
+
|
|
941
|
+
return (
|
|
942
|
+
<button
|
|
943
|
+
type="button"
|
|
944
|
+
onClick={() =>
|
|
945
|
+
openCamera().catch(() => {
|
|
946
|
+
// Ignore errors from cleanup
|
|
947
|
+
})
|
|
948
|
+
}
|
|
949
|
+
>
|
|
950
|
+
Open Camera
|
|
951
|
+
</button>
|
|
952
|
+
)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const {container} = render(
|
|
956
|
+
<ImagePickerProvider>
|
|
957
|
+
<TestComponent />
|
|
958
|
+
</ImagePickerProvider>
|
|
959
|
+
)
|
|
960
|
+
|
|
961
|
+
const cameraInput = container.querySelector(
|
|
962
|
+
'input[capture="environment"]'
|
|
963
|
+
) as HTMLInputElement
|
|
964
|
+
|
|
965
|
+
const button = screen.getByText('Open Camera')
|
|
966
|
+
fireEvent.click(button)
|
|
967
|
+
|
|
968
|
+
// Wait for camera open to be reported
|
|
969
|
+
await vi.waitFor(() => {
|
|
970
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
971
|
+
interactionType: 'camera_open',
|
|
972
|
+
})
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
mockReportInteraction.mockClear()
|
|
976
|
+
|
|
977
|
+
// Simulate photo capture
|
|
978
|
+
const file = new File(['photo'], 'photo.jpg', {type: 'image/jpeg'})
|
|
979
|
+
Object.defineProperty(cameraInput, 'files', {
|
|
980
|
+
value: [file],
|
|
981
|
+
configurable: true,
|
|
982
|
+
})
|
|
983
|
+
|
|
984
|
+
await act(async () => {
|
|
985
|
+
fireEvent.change(cameraInput)
|
|
986
|
+
})
|
|
987
|
+
|
|
988
|
+
// Check success was reported
|
|
989
|
+
await vi.waitFor(() => {
|
|
990
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
991
|
+
interactionType: 'camera_success',
|
|
992
|
+
})
|
|
993
|
+
})
|
|
994
|
+
})
|
|
995
|
+
|
|
996
|
+
it('reports camera_error when interrupted by new picker', async () => {
|
|
997
|
+
mockRequestPermission.mockResolvedValue({granted: true})
|
|
998
|
+
|
|
999
|
+
const TestComponent = () => {
|
|
1000
|
+
const {openCamera, openGallery} = useImagePickerContext()
|
|
1001
|
+
|
|
1002
|
+
return (
|
|
1003
|
+
<>
|
|
1004
|
+
<button
|
|
1005
|
+
type="button"
|
|
1006
|
+
onClick={() =>
|
|
1007
|
+
openCamera().catch(() => {
|
|
1008
|
+
// Expected to fail
|
|
1009
|
+
})
|
|
1010
|
+
}
|
|
1011
|
+
>
|
|
1012
|
+
Open Camera
|
|
1013
|
+
</button>
|
|
1014
|
+
<button
|
|
1015
|
+
type="button"
|
|
1016
|
+
onClick={() =>
|
|
1017
|
+
openGallery().catch(() => {
|
|
1018
|
+
// Ignore errors
|
|
1019
|
+
})
|
|
1020
|
+
}
|
|
1021
|
+
>
|
|
1022
|
+
Open Gallery
|
|
1023
|
+
</button>
|
|
1024
|
+
</>
|
|
1025
|
+
)
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
render(
|
|
1029
|
+
<ImagePickerProvider>
|
|
1030
|
+
<TestComponent />
|
|
1031
|
+
</ImagePickerProvider>
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
// Open camera first
|
|
1035
|
+
const cameraButton = screen.getByText('Open Camera')
|
|
1036
|
+
fireEvent.click(cameraButton)
|
|
1037
|
+
|
|
1038
|
+
// Wait for camera open to be reported
|
|
1039
|
+
await vi.waitFor(() => {
|
|
1040
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1041
|
+
interactionType: 'camera_open',
|
|
1042
|
+
})
|
|
1043
|
+
})
|
|
1044
|
+
|
|
1045
|
+
mockReportInteraction.mockClear()
|
|
1046
|
+
|
|
1047
|
+
// Open gallery to interrupt camera
|
|
1048
|
+
const galleryButton = screen.getByText('Open Gallery')
|
|
1049
|
+
fireEvent.click(galleryButton)
|
|
1050
|
+
|
|
1051
|
+
// Check error was reported for interrupted camera operation
|
|
1052
|
+
await vi.waitFor(() => {
|
|
1053
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1054
|
+
interactionType: 'camera_error',
|
|
1055
|
+
interactionValue:
|
|
1056
|
+
'New file picker opened before previous completed',
|
|
1057
|
+
})
|
|
1058
|
+
})
|
|
1059
|
+
})
|
|
1060
|
+
|
|
1061
|
+
it('reports camera_error when permission is denied', async () => {
|
|
1062
|
+
mockRequestPermission.mockResolvedValue({granted: false})
|
|
1063
|
+
|
|
1064
|
+
const TestComponent = () => {
|
|
1065
|
+
const {openCamera} = useImagePickerContext()
|
|
1066
|
+
|
|
1067
|
+
return (
|
|
1068
|
+
<button
|
|
1069
|
+
type="button"
|
|
1070
|
+
onClick={() =>
|
|
1071
|
+
openCamera().catch(() => {
|
|
1072
|
+
// Expected to fail
|
|
1073
|
+
})
|
|
1074
|
+
}
|
|
1075
|
+
>
|
|
1076
|
+
Open Camera
|
|
1077
|
+
</button>
|
|
1078
|
+
)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
render(
|
|
1082
|
+
<ImagePickerProvider>
|
|
1083
|
+
<TestComponent />
|
|
1084
|
+
</ImagePickerProvider>
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
const button = screen.getByText('Open Camera')
|
|
1088
|
+
fireEvent.click(button)
|
|
1089
|
+
|
|
1090
|
+
// Wait for permission request
|
|
1091
|
+
await vi.waitFor(() => {
|
|
1092
|
+
expect(mockRequestPermission).toHaveBeenCalled()
|
|
1093
|
+
})
|
|
1094
|
+
|
|
1095
|
+
// Camera open should be reported
|
|
1096
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1097
|
+
interactionType: 'camera_open',
|
|
1098
|
+
})
|
|
1099
|
+
|
|
1100
|
+
// Error interaction should be reported for permission denial
|
|
1101
|
+
await vi.waitFor(() => {
|
|
1102
|
+
expect(mockReportInteraction).toHaveBeenCalledWith({
|
|
1103
|
+
interactionType: 'camera_error',
|
|
1104
|
+
interactionValue: 'Camera permission not granted',
|
|
1105
|
+
})
|
|
1106
|
+
})
|
|
1107
|
+
})
|
|
1108
|
+
})
|
|
1109
|
+
})
|
|
719
1110
|
})
|