@morscherlab/mint-sdk 1.2.0-beta.9 → 1.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.
Files changed (152) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DCXmwKNS.js} +5 -5
  5. package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DCXmwKNS.js.map} +1 -1
  6. package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-BYR20I4c.js} +5 -5
  7. package/dist/{SettingsModal-Dphh3RrI.js.map → SettingsModal-BYR20I4c.js.map} +1 -1
  8. package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-BAF3CKY7.js} +3 -3
  9. package/dist/{Skeleton-DxtwJ5gt.js.map → Skeleton-BAF3CKY7.js.map} +1 -1
  10. package/dist/__tests__/apiErrors.test.d.ts +1 -0
  11. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  12. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  14. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  15. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  16. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  17. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  18. package/dist/apiErrors.d.ts +26 -0
  19. package/dist/components/BaseInput.vue.d.ts +1 -1
  20. package/dist/components/BaseSelect.vue.d.ts +1 -1
  21. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  22. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  23. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  24. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  25. package/dist/components/DatePicker.vue.d.ts +1 -1
  26. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  27. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  28. package/dist/components/FilePicker.vue.d.ts +32 -0
  29. package/dist/components/FileUploader.vue.d.ts +5 -2
  30. package/dist/components/FormulaInput.vue.d.ts +1 -1
  31. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  32. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  33. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  34. package/dist/components/NumberInput.vue.d.ts +1 -1
  35. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  36. package/dist/components/ResourceCard.vue.d.ts +1 -1
  37. package/dist/components/SecretInput.vue.d.ts +1 -1
  38. package/dist/components/SequenceInput.vue.d.ts +1 -1
  39. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  40. package/dist/components/TagsInput.vue.d.ts +1 -1
  41. package/dist/components/TimePicker.vue.d.ts +1 -1
  42. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  43. package/dist/components/UnitInput.vue.d.ts +1 -1
  44. package/dist/components/WellPlate.vue.d.ts +1 -1
  45. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  46. package/dist/components/index.d.ts +4 -0
  47. package/dist/components/index.js +8 -8
  48. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  49. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  50. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  51. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  52. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  53. package/dist/{components-CgAUzF8F.js → components-DIum5hkx.js} +4286 -1866
  54. package/dist/components-DIum5hkx.js.map +1 -0
  55. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  56. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  57. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  58. package/dist/composables/filePicker/validation.d.ts +5 -0
  59. package/dist/composables/index.d.ts +2 -0
  60. package/dist/composables/index.js +10 -10
  61. package/dist/composables/useApi.d.ts +14 -7
  62. package/dist/composables/useFileBrowser.d.ts +55 -0
  63. package/dist/composables/useFilePicker.d.ts +189 -0
  64. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  65. package/dist/{composables-B_pia3hO.js → composables-mFXsTA4g.js} +300 -11
  66. package/dist/composables-mFXsTA4g.js.map +1 -0
  67. package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
  68. package/dist/errors-BLuqMee5.js.map +1 -0
  69. package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
  70. package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
  71. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  72. package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
  73. package/dist/index.d.ts +1 -0
  74. package/dist/index.js +15 -15
  75. package/dist/install.js +2 -2
  76. package/dist/stores/index.js +2 -2
  77. package/dist/styles.css +2369 -1377
  78. package/dist/templates/index.js +2 -2
  79. package/dist/{templates-Qq113pyZ.js → templates-CmGfmBRA.js} +2 -2
  80. package/dist/{templates-Qq113pyZ.js.map → templates-CmGfmBRA.js.map} +1 -1
  81. package/dist/types/fileBrowserTypes.d.ts +77 -0
  82. package/dist/types/filePicker.d.ts +77 -0
  83. package/dist/types/index.d.ts +2 -0
  84. package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-DldpxNOP.js} +42 -37
  85. package/dist/useControlSchema-DldpxNOP.js.map +1 -0
  86. package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
  87. package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
  88. package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-CEcIaueJ.js} +4 -4
  89. package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
  90. package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-z5L_dUIy.js} +2 -2
  91. package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
  92. package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-uwoJGEb6.js} +53 -8
  93. package/dist/validation-uwoJGEb6.js.map +1 -0
  94. package/package.json +1 -1
  95. package/src/__tests__/apiErrors.test.ts +81 -0
  96. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  97. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  98. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  99. package/src/__tests__/components/FilePicker.test.ts +487 -0
  100. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  101. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  102. package/src/__tests__/components/SegmentedControl.test.ts +19 -0
  103. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  104. package/src/__tests__/composables/useApi.test.ts +92 -9
  105. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  106. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  107. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  108. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  109. package/src/apiErrors.ts +85 -0
  110. package/src/components/ExperimentPopover.vue +10 -2
  111. package/src/components/FileBrowserModal.story.vue +203 -0
  112. package/src/components/FileBrowserModal.vue +384 -0
  113. package/src/components/FilePicker.story.vue +44 -0
  114. package/src/components/FilePicker.vue +284 -0
  115. package/src/components/FileUploader.vue +32 -14
  116. package/src/components/GeneratedPlotlyResult.vue +34 -69
  117. package/src/components/GeneratedResultRenderer.vue +0 -5
  118. package/src/components/PlotlyChart.story.vue +61 -0
  119. package/src/components/PlotlyChart.vue +233 -0
  120. package/src/components/SegmentedControl.vue +2 -2
  121. package/src/components/fileBrowserPresentation.ts +39 -0
  122. package/src/components/index.ts +5 -0
  123. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  124. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  125. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  126. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  127. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  128. package/src/composables/filePicker/localFiles.ts +58 -0
  129. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  130. package/src/composables/filePicker/usePickerSelection.ts +259 -0
  131. package/src/composables/filePicker/validation.ts +41 -0
  132. package/src/composables/index.ts +7 -0
  133. package/src/composables/useApi.ts +57 -13
  134. package/src/composables/useFileBrowser.ts +314 -0
  135. package/src/composables/useFilePicker.ts +366 -0
  136. package/src/composables/useGeneratedAnalysis.ts +10 -17
  137. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  138. package/src/composables/usePluginClient.ts +3 -0
  139. package/src/index.ts +3 -0
  140. package/src/styles/components/file-browser-modal.css +489 -0
  141. package/src/styles/components/file-picker.css +502 -0
  142. package/src/styles/components/icon-button.css +9 -0
  143. package/src/styles/index.css +3 -0
  144. package/src/types/fileBrowserTypes.ts +85 -0
  145. package/src/types/filePicker.ts +72 -0
  146. package/src/types/index.ts +14 -0
  147. package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
  148. package/dist/components-CgAUzF8F.js.map +0 -1
  149. package/dist/composables-B_pia3hO.js.map +0 -1
  150. package/dist/errors-_W0-LbA6.js.map +0 -1
  151. package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
  152. package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
@@ -0,0 +1,287 @@
1
+ import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
2
+ import { afterEach, expect, it, vi } from 'vitest'
3
+ import { ref } from 'vue'
4
+ import FilePicker from '../../components/FilePicker.vue'
5
+ import FileUploader from '../../components/FileUploader.vue'
6
+ import type { PickerAdapter, PickerNode, PickerSelection } from '../../types/filePicker'
7
+
8
+ enableAutoUnmount(afterEach)
9
+ const folder: PickerNode = { kind: 'folder', path: 'opaque-root', name: 'Root' }
10
+ const child: PickerNode = {
11
+ kind: 'folder',
12
+ path: 'unrelated-child',
13
+ name: 'Child',
14
+ parentPath: folder.path,
15
+ }
16
+ const file: PickerNode = {
17
+ kind: 'file',
18
+ path: 'independent-file',
19
+ name: 'run.raw',
20
+ format: 'raw',
21
+ bytes: 7,
22
+ parentPath: child.path,
23
+ }
24
+ function adapter(): PickerAdapter {
25
+ return {
26
+ listRoot: vi.fn(async () => [folder]),
27
+ listChildren: vi.fn(async (path) => (path === folder.path ? [child] : [file])),
28
+ search: vi.fn(async () => []),
29
+ }
30
+ }
31
+ async function setup(data = adapter(), extra: Record<string, unknown> = {}) {
32
+ const wrapper = mount(FilePicker, {
33
+ props: { open: true, adapter: data, ...extra },
34
+ global: { stubs: { teleport: true } },
35
+ })
36
+ await flushPromises()
37
+ return wrapper
38
+ }
39
+ function row(wrapper: Awaited<ReturnType<typeof setup>>, path: string) {
40
+ return wrapper.findAll('[role="treeitem"]').find((item) => item.attributes('data-path') === path)!
41
+ }
42
+ function confirm(wrapper: Awaited<ReturnType<typeof setup>>) {
43
+ return wrapper.findAll('.mint-picker__actions button').at(-1)!
44
+ }
45
+
46
+ it('root breadcrumb returns directly to mounts and navigation keeps a tabbable row', async () => {
47
+ const wrapper = await setup()
48
+ await row(wrapper, folder.path).trigger('click')
49
+ await row(wrapper, folder.path).trigger('dblclick')
50
+ await flushPromises()
51
+ expect(row(wrapper, child.path).attributes('tabindex')).toBe('0')
52
+ await row(wrapper, child.path).trigger('dblclick')
53
+ await flushPromises()
54
+ await wrapper.get('[aria-label="Breadcrumb"] button').trigger('click')
55
+ await flushPromises()
56
+ expect(row(wrapper, folder.path)).toBeDefined()
57
+ })
58
+
59
+ it('restored nested files inherit their folder and disappear when that folder is deselected', async () => {
60
+ const wrapper = await setup()
61
+ await row(wrapper, folder.path).trigger('click')
62
+ await flushPromises()
63
+ await confirm(wrapper).trigger('click')
64
+ const result = wrapper.emitted('select')![0]![0] as PickerSelection
65
+ await wrapper.setProps({ open: false, initialSelection: result })
66
+ await wrapper.setProps({ open: true })
67
+ await flushPromises()
68
+ await row(wrapper, folder.path).trigger('click', { ctrlKey: true })
69
+ await flushPromises()
70
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
71
+ })
72
+
73
+ it('refresh removes deleted descendants from a selected folder', async () => {
74
+ const data = adapter()
75
+ const wrapper = await setup(data)
76
+ await row(wrapper, folder.path).trigger('click')
77
+ await flushPromises()
78
+ expect(confirm(wrapper).attributes('disabled')).toBeUndefined()
79
+ vi.mocked(data.listChildren).mockImplementation(async (path) =>
80
+ path === folder.path ? [child] : [],
81
+ )
82
+ await wrapper.get('[aria-label="Refresh location"]').trigger('click')
83
+ await flushPromises()
84
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
85
+ expect(wrapper.get('.mint-picker__receipt').text()).not.toContain('7 B')
86
+ })
87
+
88
+ it('marks only an explicit refresh so adapters can bypass their caches', async () => {
89
+ const data = adapter()
90
+ const wrapper = await setup(data)
91
+ await row(wrapper, folder.path).trigger('dblclick')
92
+ await flushPromises()
93
+ expect(vi.mocked(data.listRoot).mock.calls[0]![0]).not.toHaveProperty('refresh')
94
+ expect(vi.mocked(data.listChildren).mock.calls[0]![1]).not.toHaveProperty('refresh')
95
+ await wrapper.get('[aria-label="Refresh location"]').trigger('click')
96
+ await flushPromises()
97
+ expect(vi.mocked(data.listChildren).mock.calls.at(-1)![1]).toMatchObject({ refresh: true })
98
+ await wrapper.get('[aria-label="Breadcrumb"] button').trigger('click')
99
+ await flushPromises()
100
+ expect(vi.mocked(data.listRoot).mock.calls.at(-1)![0]).not.toHaveProperty('refresh')
101
+ await wrapper.get('[aria-label="Refresh location"]').trigger('click')
102
+ await flushPromises()
103
+ expect(vi.mocked(data.listRoot).mock.calls.at(-1)![0]).toMatchObject({ refresh: true })
104
+ })
105
+
106
+ it('late server responses cannot overwrite browser source metadata', async () => {
107
+ const data = adapter()
108
+ let release!: (nodes: PickerNode[]) => void
109
+ vi.mocked(data.listRoot).mockReturnValueOnce(
110
+ new Promise((resolve) => {
111
+ release = resolve
112
+ }),
113
+ )
114
+ const wrapper = await setup(data, { sources: ['server', 'localFile'] })
115
+ await wrapper.get('[aria-label="Local files"]').trigger('click')
116
+ await flushPromises()
117
+ release([folder])
118
+ await flushPromises()
119
+ expect(wrapper.findAll('[role="treeitem"]')).toHaveLength(0)
120
+ expect(wrapper.text()).toContain('No selection')
121
+ })
122
+
123
+ it('browser source confirms original File objects without reading or uploading them', async () => {
124
+ const wrapper = await setup(adapter(), { sources: ['localFile'], selectionMode: 'multi-file' })
125
+ const local = new File(['content'], 'run.raw')
126
+ const read = vi.spyOn(local, 'arrayBuffer')
127
+ const input = wrapper.get('input[type="file"]')
128
+ Object.defineProperty(input.element, 'files', { value: [local], configurable: true })
129
+ await input.trigger('change')
130
+ await flushPromises()
131
+ await confirm(wrapper).trigger('click')
132
+ expect(wrapper.emitted('select')?.[0]?.[0]).toEqual({ source: 'localFile', files: [local] })
133
+ expect(read).not.toHaveBeenCalled()
134
+ })
135
+
136
+ it('restores dropped folder paths after remounting with the confirmed local selection', async () => {
137
+ const options = { sources: ['localFile'], selectionMode: 'folder' }
138
+ const wrapper = await setup(adapter(), options)
139
+ const file = new File(['data'], 'run.raw')
140
+ wrapper.getComponent(FileUploader).vm.$emit('upload', [file], 'batch', ['batch/run.raw'])
141
+ await flushPromises()
142
+ await confirm(wrapper).trigger('click')
143
+ const result = wrapper.emitted('select')![0]![0] as PickerSelection
144
+ expect(result).toEqual({ source: 'localFile', files: [file], relativePaths: ['batch/run.raw'] })
145
+ expect(result.files[0]).toBe(file)
146
+ wrapper.unmount()
147
+
148
+ const restored = await setup(adapter(), { ...options, initialSelection: result })
149
+ expect(confirm(restored).attributes('disabled')).toBeUndefined()
150
+ await confirm(restored).trigger('click')
151
+ const confirmed = restored.emitted('select')![0]![0] as PickerSelection
152
+ expect(confirmed).toEqual(result)
153
+ expect(confirmed.files[0]).toBe(file)
154
+ })
155
+
156
+ it('restores a nested selected folder display name without exposing its opaque identity', async () => {
157
+ const wrapper = await setup()
158
+ await row(wrapper, folder.path).trigger('dblclick')
159
+ await flushPromises()
160
+ await row(wrapper, child.path).trigger('click')
161
+ await flushPromises()
162
+ await wrapper.get('[aria-label="Breadcrumb"] button').trigger('click')
163
+ await flushPromises()
164
+ await confirm(wrapper).trigger('click')
165
+ await wrapper.setProps({ open: false })
166
+ await wrapper.setProps({ open: true })
167
+ await flushPromises()
168
+ expect(wrapper.get('.mint-picker__preview-name').text()).toBe('Child')
169
+ })
170
+
171
+ it('blocks confirmation while refresh is replacing stale folder metadata', async () => {
172
+ const data = adapter()
173
+ const wrapper = await setup(data)
174
+ await row(wrapper, folder.path).trigger('click')
175
+ await flushPromises()
176
+ let release!: (nodes: PickerNode[]) => void
177
+ vi.mocked(data.listRoot).mockReturnValueOnce(
178
+ new Promise((resolve) => {
179
+ release = resolve
180
+ }),
181
+ )
182
+ await wrapper.get('[aria-label="Refresh location"]').trigger('click')
183
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
184
+ await confirm(wrapper).trigger('click')
185
+ expect(wrapper.emitted('select')).toBeUndefined()
186
+ release([folder])
187
+ await flushPromises()
188
+ expect(confirm(wrapper).attributes('disabled')).toBeUndefined()
189
+ })
190
+
191
+ it('does not confirm a folder after the host switches the same picker to single-file mode', async () => {
192
+ const wrapper = await setup()
193
+ await row(wrapper, folder.path).trigger('click')
194
+ await flushPromises()
195
+ await wrapper.setProps({ selectionMode: 'single-file' })
196
+ await flushPromises()
197
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
198
+ await confirm(wrapper).trigger('click')
199
+ expect(wrapper.emitted('select')).toBeUndefined()
200
+ })
201
+
202
+ it('system filter hides denied folders and skips their contents during folder selection', async () => {
203
+ const allowed: PickerNode = { ...file, path: 'allowed', name: 'allowed.raw', parentPath: folder.path }
204
+ const data = adapter()
205
+ vi.mocked(data.listChildren).mockImplementation(async (path) =>
206
+ path === folder.path ? [child, allowed] : [file],
207
+ )
208
+ const filter = vi.fn((node: PickerNode) => node.path !== child.path)
209
+ const wrapper = await setup(data, { systemFilter: filter })
210
+ await row(wrapper, folder.path).trigger('click')
211
+ await flushPromises()
212
+ expect(data.listChildren).not.toHaveBeenCalledWith(child.path, expect.anything())
213
+ await row(wrapper, folder.path).trigger('dblclick')
214
+ await flushPromises()
215
+ expect(row(wrapper, child.path)).toBeUndefined()
216
+ await confirm(wrapper).trigger('click')
217
+ expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ files: [allowed], excluded: [] })
218
+ expect(filter).toHaveBeenCalledWith(expect.objectContaining({ path: child.path }), 'server')
219
+ })
220
+
221
+ it('system filter applies to search results and restored or already selected files', async () => {
222
+ const data = adapter()
223
+ vi.mocked(data.listRoot).mockResolvedValue([file])
224
+ vi.mocked(data.search).mockResolvedValue([file])
225
+ const wrapper = await setup(data, { selectionMode: 'multi-file' })
226
+ await row(wrapper, file.path).trigger('click')
227
+ await flushPromises()
228
+ const deny = (node: PickerNode) => node.path !== file.path
229
+ await wrapper.setProps({ systemFilter: deny })
230
+ expect(row(wrapper, file.path)).toBeUndefined()
231
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
232
+ await wrapper.get('input[aria-label="Search folders and files"]').setValue('run')
233
+ await new Promise((resolve) => setTimeout(resolve, 250))
234
+ await flushPromises()
235
+ expect(data.search).toHaveBeenCalled()
236
+ expect(row(wrapper, file.path)).toBeUndefined()
237
+ await wrapper.setProps({ open: false, initialSelection: {
238
+ source: 'server', folders: [], files: [file], excluded: [],
239
+ } })
240
+ await wrapper.setProps({ open: true })
241
+ await flushPromises()
242
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
243
+ await confirm(wrapper).trigger('click')
244
+ expect(wrapper.emitted('select')).toBeUndefined()
245
+ })
246
+
247
+ it('system filter receives local source and prevents denied browser files from being confirmed', async () => {
248
+ const filter = vi.fn((node: PickerNode) => node.name !== 'private.raw')
249
+ const wrapper = await setup(adapter(), {
250
+ sources: ['localFile'], selectionMode: 'multi-file', systemFilter: filter,
251
+ })
252
+ const allowed = new File(['public'], 'public.raw')
253
+ const denied = new File(['private'], 'private.raw')
254
+ const input = wrapper.get('input[type="file"]')
255
+ Object.defineProperty(input.element, 'files', { value: [allowed, denied], configurable: true })
256
+ await input.trigger('change')
257
+ await flushPromises()
258
+ expect(wrapper.get('.mint-picker__receipt').text()).toContain('1 file')
259
+ expect(wrapper.text()).not.toContain('private.raw')
260
+ await confirm(wrapper).trigger('click')
261
+ expect(wrapper.emitted('select')?.[0]?.[0]).toEqual({ source: 'localFile', files: [allowed] })
262
+ expect(filter).toHaveBeenCalledWith(expect.objectContaining({ name: denied.name }), 'localFile')
263
+ })
264
+
265
+ it('reactive permission changes restore visible rows and resolve newly allowed selected subfolders', async () => {
266
+ const allowed = ref(false)
267
+ const data = adapter()
268
+ const wrapper = await setup(data, {
269
+ systemFilter: (node: PickerNode) => node.path !== child.path || allowed.value,
270
+ })
271
+ await row(wrapper, folder.path).trigger('click')
272
+ await flushPromises()
273
+ await row(wrapper, folder.path).trigger('dblclick')
274
+ await flushPromises()
275
+ expect(row(wrapper, child.path)).toBeUndefined()
276
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
277
+ allowed.value = true
278
+ await flushPromises()
279
+ expect(row(wrapper, child.path)).toBeDefined()
280
+ expect(confirm(wrapper).attributes('disabled')).toBeUndefined()
281
+ await confirm(wrapper).trigger('click')
282
+ expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ files: [file] })
283
+ allowed.value = false
284
+ await flushPromises()
285
+ expect(row(wrapper, child.path)).toBeUndefined()
286
+ expect(confirm(wrapper).attributes('disabled')).toBeDefined()
287
+ })