@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.
- package/README.md +30 -0
- package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
- package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DCXmwKNS.js} +5 -5
- package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DCXmwKNS.js.map} +1 -1
- package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-BYR20I4c.js} +5 -5
- package/dist/{SettingsModal-Dphh3RrI.js.map → SettingsModal-BYR20I4c.js.map} +1 -1
- package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-BAF3CKY7.js} +3 -3
- package/dist/{Skeleton-DxtwJ5gt.js.map → Skeleton-BAF3CKY7.js.map} +1 -1
- package/dist/__tests__/apiErrors.test.d.ts +1 -0
- package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
- package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
- package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
- package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
- package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
- package/dist/apiErrors.d.ts +26 -0
- package/dist/components/BaseInput.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseTextarea.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
- package/dist/components/ConcentrationInput.vue.d.ts +1 -1
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +1 -1
- package/dist/components/FileBrowserModal.vue.d.ts +99 -0
- package/dist/components/FilePicker.vue.d.ts +32 -0
- package/dist/components/FileUploader.vue.d.ts +5 -2
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
- package/dist/components/MobileSupportGate.vue.d.ts +1 -1
- package/dist/components/MoleculeInput.vue.d.ts +1 -1
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/PlotlyChart.vue.d.ts +59 -0
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SecretInput.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/TimePicker.vue.d.ts +1 -1
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +1 -1
- package/dist/components/fileBrowserPresentation.d.ts +10 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +8 -8
- package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
- package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
- package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
- package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
- package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
- package/dist/{components-CgAUzF8F.js → components-DIum5hkx.js} +4286 -1866
- package/dist/components-DIum5hkx.js.map +1 -0
- package/dist/composables/filePicker/localFiles.d.ts +3 -0
- package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
- package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
- package/dist/composables/filePicker/validation.d.ts +5 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +10 -10
- package/dist/composables/useApi.d.ts +14 -7
- package/dist/composables/useFileBrowser.d.ts +55 -0
- package/dist/composables/useFilePicker.d.ts +189 -0
- package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
- package/dist/{composables-B_pia3hO.js → composables-mFXsTA4g.js} +300 -11
- package/dist/composables-mFXsTA4g.js.map +1 -0
- package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
- package/dist/errors-BLuqMee5.js.map +1 -0
- package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
- package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -15
- package/dist/install.js +2 -2
- package/dist/stores/index.js +2 -2
- package/dist/styles.css +2369 -1377
- package/dist/templates/index.js +2 -2
- package/dist/{templates-Qq113pyZ.js → templates-CmGfmBRA.js} +2 -2
- package/dist/{templates-Qq113pyZ.js.map → templates-CmGfmBRA.js.map} +1 -1
- package/dist/types/fileBrowserTypes.d.ts +77 -0
- package/dist/types/filePicker.d.ts +77 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-DldpxNOP.js} +42 -37
- package/dist/useControlSchema-DldpxNOP.js.map +1 -0
- package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
- package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
- package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-CEcIaueJ.js} +4 -4
- package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
- package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-z5L_dUIy.js} +2 -2
- package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
- package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-uwoJGEb6.js} +53 -8
- package/dist/validation-uwoJGEb6.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/apiErrors.test.ts +81 -0
- package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
- package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
- package/src/__tests__/components/FilePicker.state.test.ts +287 -0
- package/src/__tests__/components/FilePicker.test.ts +487 -0
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
- package/src/__tests__/components/PlotlyChart.test.ts +305 -0
- package/src/__tests__/components/SegmentedControl.test.ts +19 -0
- package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
- package/src/__tests__/composables/useApi.test.ts +92 -9
- package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
- package/src/__tests__/composables/usePluginClient.test.ts +3 -0
- package/src/apiErrors.ts +85 -0
- package/src/components/ExperimentPopover.vue +10 -2
- package/src/components/FileBrowserModal.story.vue +203 -0
- package/src/components/FileBrowserModal.vue +384 -0
- package/src/components/FilePicker.story.vue +44 -0
- package/src/components/FilePicker.vue +284 -0
- package/src/components/FileUploader.vue +32 -14
- package/src/components/GeneratedPlotlyResult.vue +34 -69
- package/src/components/GeneratedResultRenderer.vue +0 -5
- package/src/components/PlotlyChart.story.vue +61 -0
- package/src/components/PlotlyChart.vue +233 -0
- package/src/components/SegmentedControl.vue +2 -2
- package/src/components/fileBrowserPresentation.ts +39 -0
- package/src/components/index.ts +5 -0
- package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
- package/src/components/internal/FileBrowserListingInternal.vue +197 -0
- package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
- package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
- package/src/components/internal/FilePickerTreeInternal.vue +238 -0
- package/src/composables/filePicker/localFiles.ts +58 -0
- package/src/composables/filePicker/usePickerNavigation.ts +220 -0
- package/src/composables/filePicker/usePickerSelection.ts +259 -0
- package/src/composables/filePicker/validation.ts +41 -0
- package/src/composables/index.ts +7 -0
- package/src/composables/useApi.ts +57 -13
- package/src/composables/useFileBrowser.ts +314 -0
- package/src/composables/useFilePicker.ts +366 -0
- package/src/composables/useGeneratedAnalysis.ts +10 -17
- package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
- package/src/composables/usePluginClient.ts +3 -0
- package/src/index.ts +3 -0
- package/src/styles/components/file-browser-modal.css +489 -0
- package/src/styles/components/file-picker.css +502 -0
- package/src/styles/components/icon-button.css +9 -0
- package/src/styles/index.css +3 -0
- package/src/types/fileBrowserTypes.ts +85 -0
- package/src/types/filePicker.ts +72 -0
- package/src/types/index.ts +14 -0
- package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
- package/dist/components-CgAUzF8F.js.map +0 -1
- package/dist/composables-B_pia3hO.js.map +0 -1
- package/dist/errors-_W0-LbA6.js.map +0 -1
- package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
- package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import FilePicker from '../../components/FilePicker.vue'
|
|
4
|
+
import FileUploader from '../../components/FileUploader.vue'
|
|
5
|
+
import type { PickerAdapter, PickerNode, PickerSelection } from '../../types/filePicker'
|
|
6
|
+
import { localFilesAdapter } from '../../composables/filePicker/localFiles'
|
|
7
|
+
import { pickerExclusion } from '../../composables/filePicker/validation'
|
|
8
|
+
|
|
9
|
+
enableAutoUnmount(afterEach)
|
|
10
|
+
afterEach(() => vi.useRealTimers())
|
|
11
|
+
const folder: PickerNode = {
|
|
12
|
+
kind: 'folder',
|
|
13
|
+
path: 'opaque:a',
|
|
14
|
+
name: 'Tissue liver',
|
|
15
|
+
fileCount: 3,
|
|
16
|
+
bytes: 60,
|
|
17
|
+
formats: { raw: 2, txt: 1 },
|
|
18
|
+
}
|
|
19
|
+
const other: PickerNode = { kind: 'folder', path: 'opaque:b', name: 'Tissue kidney' }
|
|
20
|
+
const one: PickerNode = {
|
|
21
|
+
kind: 'file',
|
|
22
|
+
path: 'file:one',
|
|
23
|
+
parentPath: folder.path,
|
|
24
|
+
name: 'sample-01.raw',
|
|
25
|
+
bytes: 20,
|
|
26
|
+
format: 'raw',
|
|
27
|
+
}
|
|
28
|
+
const two: PickerNode = {
|
|
29
|
+
kind: 'file',
|
|
30
|
+
path: 'file:two',
|
|
31
|
+
parentPath: folder.path,
|
|
32
|
+
name: 'sample-02.raw',
|
|
33
|
+
bytes: 20,
|
|
34
|
+
format: 'raw',
|
|
35
|
+
}
|
|
36
|
+
const unsupported: PickerNode = {
|
|
37
|
+
kind: 'file',
|
|
38
|
+
path: 'file:notes',
|
|
39
|
+
parentPath: folder.path,
|
|
40
|
+
name: 'notes.txt',
|
|
41
|
+
bytes: 20,
|
|
42
|
+
format: 'txt',
|
|
43
|
+
}
|
|
44
|
+
function adapter(): PickerAdapter {
|
|
45
|
+
return {
|
|
46
|
+
listRoot: vi.fn(async () => [folder, other]),
|
|
47
|
+
listChildren: vi.fn(async (path) => (path === folder.path ? [one, two, unsupported] : [])),
|
|
48
|
+
search: vi.fn(async (query) =>
|
|
49
|
+
[folder, one, two, unsupported].filter((node) =>
|
|
50
|
+
node.name.toLowerCase().includes(query.toLowerCase()),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function setup(props: Record<string, unknown> = {}) {
|
|
56
|
+
const wrapper = mount(FilePicker, {
|
|
57
|
+
props: {
|
|
58
|
+
open: true,
|
|
59
|
+
adapter: adapter(),
|
|
60
|
+
capabilities: { server: { formats: ['raw'] } },
|
|
61
|
+
...props,
|
|
62
|
+
},
|
|
63
|
+
global: { stubs: { teleport: true } },
|
|
64
|
+
})
|
|
65
|
+
await flushPromises()
|
|
66
|
+
return wrapper
|
|
67
|
+
}
|
|
68
|
+
function row(wrapper: Awaited<ReturnType<typeof setup>>, path: string) {
|
|
69
|
+
return wrapper.findAll('[role="treeitem"]').find((item) => item.attributes('data-path') === path)!
|
|
70
|
+
}
|
|
71
|
+
function confirm(wrapper: Awaited<ReturnType<typeof setup>>) {
|
|
72
|
+
return wrapper.findAll('.mint-picker__actions button').at(-1)!
|
|
73
|
+
}
|
|
74
|
+
async function expand(wrapper: Awaited<ReturnType<typeof setup>>) {
|
|
75
|
+
await wrapper.get('[aria-label="Expand Tissue liver"]').trigger('click')
|
|
76
|
+
await flushPromises()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe('FilePicker', () => {
|
|
80
|
+
it('selects a folder without navigating, resolves metadata and excludes unsupported files', async () => {
|
|
81
|
+
const wrapper = await setup()
|
|
82
|
+
await row(wrapper, folder.path).trigger('click')
|
|
83
|
+
await flushPromises()
|
|
84
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('true')
|
|
85
|
+
expect(wrapper.findAll('[role="treeitem"]')).toHaveLength(2)
|
|
86
|
+
await confirm(wrapper).trigger('click')
|
|
87
|
+
const result = wrapper.emitted('select')?.[0]?.[0] as Extract<
|
|
88
|
+
PickerSelection,
|
|
89
|
+
{ source: 'server' }
|
|
90
|
+
>
|
|
91
|
+
expect(result.folders).toEqual([folder.path])
|
|
92
|
+
expect(result.files.map((file) => file.path)).toEqual([one.path, two.path])
|
|
93
|
+
expect(result.excluded).toEqual([
|
|
94
|
+
{ path: unsupported.path, name: unsupported.name, reason: 'unsupported' },
|
|
95
|
+
])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('discloses without selecting and double-clicks to navigate', async () => {
|
|
99
|
+
const wrapper = await setup()
|
|
100
|
+
await expand(wrapper)
|
|
101
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('false')
|
|
102
|
+
expect(wrapper.findAll('[role="treeitem"]')).toHaveLength(5)
|
|
103
|
+
await row(wrapper, folder.path).trigger('dblclick')
|
|
104
|
+
await flushPromises()
|
|
105
|
+
expect(wrapper.findAll('[role="treeitem"]')).toHaveLength(3)
|
|
106
|
+
expect(wrapper.get('[aria-label="Breadcrumb"]').text()).toContain(folder.name)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('drops an inherited file with Ctrl and restores it with another Ctrl click', async () => {
|
|
110
|
+
const wrapper = await setup()
|
|
111
|
+
await row(wrapper, folder.path).trigger('click')
|
|
112
|
+
await expand(wrapper)
|
|
113
|
+
await row(wrapper, one.path).trigger('click', { ctrlKey: true })
|
|
114
|
+
expect(row(wrapper, one.path).attributes('aria-selected')).toBe('false')
|
|
115
|
+
expect(row(wrapper, folder.path).text()).toContain('1 / 3')
|
|
116
|
+
await confirm(wrapper).trigger('click')
|
|
117
|
+
const result = wrapper.emitted('select')?.[0]?.[0] as Extract<
|
|
118
|
+
PickerSelection,
|
|
119
|
+
{ source: 'server' }
|
|
120
|
+
>
|
|
121
|
+
expect(result.excluded).toContainEqual({ path: one.path, name: one.name, reason: 'deselected' })
|
|
122
|
+
await row(wrapper, one.path).trigger('click', { metaKey: true })
|
|
123
|
+
expect(row(wrapper, one.path).attributes('aria-selected')).toBe('true')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('keeps unsupported files inert for click, double-click and keyboard', async () => {
|
|
127
|
+
const wrapper = await setup({ selectionMode: 'single-file' })
|
|
128
|
+
await expand(wrapper)
|
|
129
|
+
const unavailable = row(wrapper, unsupported.path)
|
|
130
|
+
expect(unavailable.attributes('aria-disabled')).toBe('true')
|
|
131
|
+
expect(unavailable.attributes('title')).toContain('not supported')
|
|
132
|
+
await unavailable.trigger('click')
|
|
133
|
+
await unavailable.trigger('dblclick')
|
|
134
|
+
await unavailable.trigger('keydown', { key: 'Enter' })
|
|
135
|
+
expect(wrapper.emitted('select')).toBeUndefined()
|
|
136
|
+
expect(confirm(wrapper).attributes('disabled')).toBeDefined()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('single-file mode replaces selection and folder mode leaves files inert', async () => {
|
|
140
|
+
const wrapper = await setup({ selectionMode: 'single-file' })
|
|
141
|
+
await expand(wrapper)
|
|
142
|
+
await row(wrapper, one.path).trigger('click')
|
|
143
|
+
await row(wrapper, two.path).trigger('click', { metaKey: true })
|
|
144
|
+
await flushPromises()
|
|
145
|
+
await confirm(wrapper).trigger('click')
|
|
146
|
+
expect(
|
|
147
|
+
(wrapper.emitted('select')?.[0]?.[0] as { files: PickerNode[] }).files.map(
|
|
148
|
+
(file) => file.path,
|
|
149
|
+
),
|
|
150
|
+
).toEqual([two.path])
|
|
151
|
+
const folders = await setup({ selectionMode: 'folder' })
|
|
152
|
+
await expand(folders)
|
|
153
|
+
await row(folders, one.path).trigger('click')
|
|
154
|
+
expect(row(folders, one.path).attributes('aria-selected')).toBe('false')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('uses Shift to select the file range, excluding unsupported entries', async () => {
|
|
158
|
+
const wrapper = await setup({ selectionMode: 'multi-file' })
|
|
159
|
+
await expand(wrapper)
|
|
160
|
+
await row(wrapper, one.path).trigger('click')
|
|
161
|
+
await row(wrapper, two.path).trigger('click', { shiftKey: true })
|
|
162
|
+
await flushPromises()
|
|
163
|
+
await confirm(wrapper).trigger('click')
|
|
164
|
+
expect((wrapper.emitted('select')?.[0]?.[0] as { files: PickerNode[] }).files).toHaveLength(2)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('preserves selected paths while searching and passes the recursive scope', async () => {
|
|
168
|
+
vi.useFakeTimers()
|
|
169
|
+
const data = adapter()
|
|
170
|
+
const wrapper = await setup({ adapter: data })
|
|
171
|
+
await row(wrapper, folder.path).trigger('click')
|
|
172
|
+
await flushPromises()
|
|
173
|
+
await wrapper.get('[aria-label="Search folders and files"]').setValue('sample')
|
|
174
|
+
await wrapper.get('.mint-picker__scope').trigger('click')
|
|
175
|
+
await vi.advanceTimersByTimeAsync(250)
|
|
176
|
+
expect(data.search).toHaveBeenCalledWith(
|
|
177
|
+
'sample',
|
|
178
|
+
{ path: '', recursive: true },
|
|
179
|
+
expect.anything(),
|
|
180
|
+
)
|
|
181
|
+
expect(confirm(wrapper).attributes('disabled')).toBeUndefined()
|
|
182
|
+
await confirm(wrapper).trigger('click')
|
|
183
|
+
expect((wrapper.emitted('select')?.[0]?.[0] as { folders: string[] }).folders).toEqual([
|
|
184
|
+
folder.path,
|
|
185
|
+
])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('cancel emits no selection and reopening restores the initial confirmed selection', async () => {
|
|
189
|
+
const initial: PickerSelection = {
|
|
190
|
+
source: 'server',
|
|
191
|
+
folders: [folder.path],
|
|
192
|
+
files: [],
|
|
193
|
+
excluded: [],
|
|
194
|
+
}
|
|
195
|
+
const wrapper = await setup({ initialSelection: initial })
|
|
196
|
+
await row(wrapper, other.path).trigger('click')
|
|
197
|
+
await wrapper.get('[aria-label="Close modal"]').trigger('click')
|
|
198
|
+
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
|
199
|
+
expect(wrapper.emitted('select')).toBeUndefined()
|
|
200
|
+
await wrapper.setProps({ open: false })
|
|
201
|
+
await wrapper.setProps({ open: true })
|
|
202
|
+
await flushPromises()
|
|
203
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('true')
|
|
204
|
+
expect(row(wrapper, other.path).attributes('aria-selected')).toBe('false')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('accumulates plain clicks in multi modes and Shift still extends a range', async () => {
|
|
208
|
+
const wrapper = await setup()
|
|
209
|
+
await expand(wrapper)
|
|
210
|
+
await row(wrapper, one.path).trigger('click')
|
|
211
|
+
await row(wrapper, two.path).trigger('click')
|
|
212
|
+
expect(row(wrapper, one.path).attributes('aria-selected')).toBe('true')
|
|
213
|
+
expect(row(wrapper, two.path).attributes('aria-selected')).toBe('true')
|
|
214
|
+
await row(wrapper, two.path).trigger('click')
|
|
215
|
+
expect(row(wrapper, two.path).attributes('aria-selected')).toBe('false')
|
|
216
|
+
await row(wrapper, folder.path).trigger('click', { shiftKey: true })
|
|
217
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('true')
|
|
218
|
+
expect(row(wrapper, two.path).attributes('aria-selected')).toBe('true')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('toggles a selected folder off with a second plain click', async () => {
|
|
222
|
+
const wrapper = await setup()
|
|
223
|
+
await row(wrapper, folder.path).trigger('click')
|
|
224
|
+
await flushPromises()
|
|
225
|
+
await row(wrapper, folder.path).trigger('click')
|
|
226
|
+
await flushPromises()
|
|
227
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('false')
|
|
228
|
+
expect(confirm(wrapper).attributes('disabled')).toBeDefined()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('toggles inherited files off and on without a modifier or losing the folder', async () => {
|
|
232
|
+
const wrapper = await setup()
|
|
233
|
+
await row(wrapper, folder.path).trigger('click')
|
|
234
|
+
await expand(wrapper)
|
|
235
|
+
await row(wrapper, one.path).trigger('click')
|
|
236
|
+
expect(row(wrapper, one.path).attributes('aria-selected')).toBe('false')
|
|
237
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('true')
|
|
238
|
+
await row(wrapper, one.path).trigger('click')
|
|
239
|
+
expect(row(wrapper, one.path).attributes('aria-selected')).toBe('true')
|
|
240
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('true')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('toggles a single file off but Enter still confirms a selected file', async () => {
|
|
244
|
+
const wrapper = await setup({ selectionMode: 'single-file' })
|
|
245
|
+
await expand(wrapper)
|
|
246
|
+
await row(wrapper, one.path).trigger('click')
|
|
247
|
+
await row(wrapper, one.path).trigger('click')
|
|
248
|
+
await flushPromises()
|
|
249
|
+
expect(confirm(wrapper).attributes('disabled')).toBeDefined()
|
|
250
|
+
await row(wrapper, one.path).trigger('click')
|
|
251
|
+
await row(wrapper, one.path).trigger('keydown', { key: 'Enter' })
|
|
252
|
+
await flushPromises()
|
|
253
|
+
expect(wrapper.emitted('select')).toHaveLength(1)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('deselects one explicit file without losing the other selected files', async () => {
|
|
257
|
+
const wrapper = await setup({ selectionMode: 'multi-file' })
|
|
258
|
+
await expand(wrapper)
|
|
259
|
+
await row(wrapper, one.path).trigger('click')
|
|
260
|
+
await row(wrapper, two.path).trigger('click', { ctrlKey: true })
|
|
261
|
+
await row(wrapper, one.path).trigger('click')
|
|
262
|
+
expect(row(wrapper, one.path).attributes('aria-selected')).toBe('false')
|
|
263
|
+
expect(row(wrapper, two.path).attributes('aria-selected')).toBe('true')
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('does not undo the first click of a double-click or toggle on keyboard navigation', async () => {
|
|
267
|
+
const wrapper = await setup({ selectionMode: 'single-file' })
|
|
268
|
+
await expand(wrapper)
|
|
269
|
+
await row(wrapper, one.path).trigger('click', { detail: 1 })
|
|
270
|
+
await row(wrapper, one.path).trigger('click', { detail: 2 })
|
|
271
|
+
expect(row(wrapper, one.path).attributes('aria-selected')).toBe('true')
|
|
272
|
+
await row(wrapper, one.path).trigger('keydown', { key: 'ArrowDown' })
|
|
273
|
+
await row(wrapper, two.path).trigger('keydown', { key: 'End' })
|
|
274
|
+
expect(row(wrapper, two.path).attributes('aria-selected')).toBe('true')
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it('offers only server and local files and has a text-only confirm action', async () => {
|
|
278
|
+
const wrapper = await setup({ sources: ['server', 'localFile'] })
|
|
279
|
+
expect(
|
|
280
|
+
wrapper
|
|
281
|
+
.findAll('[aria-label="Data source"] button')
|
|
282
|
+
.map((button) => button.attributes('aria-label')),
|
|
283
|
+
).toEqual(['Server', 'Local files'])
|
|
284
|
+
expect(confirm(wrapper).text()).toBe('Use selection')
|
|
285
|
+
expect(wrapper.get('.mint-picker__receipt > span').text()).toBe('No selection')
|
|
286
|
+
await wrapper.get('[aria-label="Local files"]').trigger('click')
|
|
287
|
+
await flushPromises()
|
|
288
|
+
expect(wrapper.get('[aria-label="Local files"]').attributes('aria-pressed')).toBe('true')
|
|
289
|
+
expect(wrapper.find('[aria-label="Absolute path"]').exists()).toBe(false)
|
|
290
|
+
expect(wrapper.text()).toContain('Drop files or a folder')
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it.each(['single-file', 'multi-file', 'folder', 'folder+files'] as const)(
|
|
294
|
+
'uses one upload panel without a lower workspace and preserves %s selection limits',
|
|
295
|
+
async (selectionMode) => {
|
|
296
|
+
const wrapper = await setup({ sources: ['localFile'], selectionMode })
|
|
297
|
+
const uploaders = wrapper.findAllComponents(FileUploader)
|
|
298
|
+
expect(uploaders.map((uploader) => uploader.props('mode'))).toEqual(
|
|
299
|
+
selectionMode === 'single-file'
|
|
300
|
+
? ['file']
|
|
301
|
+
: selectionMode === 'folder'
|
|
302
|
+
? ['folder']
|
|
303
|
+
: ['file+folder'],
|
|
304
|
+
)
|
|
305
|
+
expect(wrapper.find('.mint-picker__workspace').exists()).toBe(false)
|
|
306
|
+
expect(wrapper.find('.mint-file-uploader__actions').exists()).toBe(false)
|
|
307
|
+
expect(wrapper.text()).not.toContain('Files stay in this browser.')
|
|
308
|
+
expect(wrapper.find('.mint-picker__pathbar').exists()).toBe(false)
|
|
309
|
+
expect(wrapper.find('[aria-label="Search folders and files"]').exists()).toBe(false)
|
|
310
|
+
const files = [new File(['a'], 'one.raw'), new File(['b'], 'two.raw')]
|
|
311
|
+
const input = uploaders[0]!.get('input[type="file"]')
|
|
312
|
+
if (selectionMode === 'folder') {
|
|
313
|
+
files.forEach((file) =>
|
|
314
|
+
Object.defineProperty(file, 'webkitRelativePath', { value: `batch/${file.name}` }),
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
Object.defineProperty(input.element, 'files', { value: files })
|
|
318
|
+
await input.trigger('change')
|
|
319
|
+
await flushPromises()
|
|
320
|
+
expect(wrapper.find('.mint-file-uploader__list').exists()).toBe(false)
|
|
321
|
+
expect(wrapper.find('.mint-file-uploader__folder-summary').exists()).toBe(false)
|
|
322
|
+
await confirm(wrapper).trigger('click')
|
|
323
|
+
const result = wrapper.emitted('select')?.[0]?.[0] as Extract<
|
|
324
|
+
PickerSelection,
|
|
325
|
+
{ source: 'localFile' }
|
|
326
|
+
>
|
|
327
|
+
expect(result.files).toEqual(selectionMode === 'single-file' ? [files[0]] : files)
|
|
328
|
+
expect(result.files[0]).toBe(files[0])
|
|
329
|
+
},
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
it.each(['folder+files', 'multi-file'] as const)('accepts dropped directories in %s mode and validates without a workspace', async (selectionMode) => {
|
|
333
|
+
const wrapper = await setup({
|
|
334
|
+
sources: ['localFile'],
|
|
335
|
+
selectionMode,
|
|
336
|
+
capabilities: { localFile: { formats: ['mzML.gz'], maxBytes: 10 } },
|
|
337
|
+
})
|
|
338
|
+
const valid = new File(['data'], 'run.mzML.gz')
|
|
339
|
+
const unsupported = new File(['notes'], 'notes.txt')
|
|
340
|
+
const oversized = new File(['too much data'], 'large.mzML.gz')
|
|
341
|
+
const directory = (name: string, entries: unknown[]) => ({
|
|
342
|
+
name,
|
|
343
|
+
isDirectory: true,
|
|
344
|
+
createReader: () => {
|
|
345
|
+
let pending = entries
|
|
346
|
+
return {
|
|
347
|
+
readEntries: (resolve: (entries: unknown[]) => void) => {
|
|
348
|
+
resolve(pending)
|
|
349
|
+
pending = []
|
|
350
|
+
},
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
})
|
|
354
|
+
const nested = directory('batch', [
|
|
355
|
+
directory('nested', [valid, unsupported, oversized].map((file) => ({
|
|
356
|
+
name: file.name,
|
|
357
|
+
isFile: true,
|
|
358
|
+
file: (resolve: (file: File) => void) => resolve(file),
|
|
359
|
+
}))),
|
|
360
|
+
])
|
|
361
|
+
const uploader = wrapper.getComponent(FileUploader)
|
|
362
|
+
await uploader.get('[role="button"]').trigger('drop', {
|
|
363
|
+
dataTransfer: { items: [{ webkitGetAsEntry: () => nested }] },
|
|
364
|
+
})
|
|
365
|
+
await flushPromises()
|
|
366
|
+
expect(wrapper.get('.mint-picker__receipt').text()).toContain('1 file')
|
|
367
|
+
if (selectionMode === 'folder+files') expect(wrapper.text()).toContain('2 skipped')
|
|
368
|
+
expect(wrapper.find('.mint-picker__workspace').exists()).toBe(false)
|
|
369
|
+
await confirm(wrapper).trigger('click')
|
|
370
|
+
const result = wrapper.emitted('select')?.[0]?.[0] as Extract<
|
|
371
|
+
PickerSelection,
|
|
372
|
+
{ source: 'localFile' }
|
|
373
|
+
>
|
|
374
|
+
expect(result.files).toEqual([valid])
|
|
375
|
+
expect(result.files[0]).toBe(valid)
|
|
376
|
+
await wrapper.setProps({ open: false })
|
|
377
|
+
await wrapper.setProps({ open: true })
|
|
378
|
+
await flushPromises()
|
|
379
|
+
expect(wrapper.get('.mint-picker__receipt').text()).toContain('1 file')
|
|
380
|
+
expect(confirm(wrapper).attributes('disabled')).toBeUndefined()
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
it.each(['entry', 'file'] as const)(
|
|
384
|
+
'captures dropped %s handles before the drag data becomes protected',
|
|
385
|
+
async (secondHandle) => {
|
|
386
|
+
const wrapper = await setup({ sources: ['localFile'], selectionMode: 'multi-file' })
|
|
387
|
+
const files = [new File(['a'], 'one.raw'), new File(['b'], 'two.raw')]
|
|
388
|
+
let protectedMode = false
|
|
389
|
+
const secondEntry = {
|
|
390
|
+
isFile: true,
|
|
391
|
+
file: (resolve: (file: File) => void) => resolve(files[1]!),
|
|
392
|
+
}
|
|
393
|
+
await wrapper.getComponent(FileUploader).get('[role="button"]').trigger('drop', {
|
|
394
|
+
dataTransfer: {
|
|
395
|
+
items: [
|
|
396
|
+
{ webkitGetAsEntry: () => ({
|
|
397
|
+
isFile: true,
|
|
398
|
+
file: (resolve: (file: File) => void) => queueMicrotask(() => {
|
|
399
|
+
protectedMode = true
|
|
400
|
+
resolve(files[0]!)
|
|
401
|
+
}),
|
|
402
|
+
}) },
|
|
403
|
+
{
|
|
404
|
+
webkitGetAsEntry: () =>
|
|
405
|
+
!protectedMode && secondHandle === 'entry' ? secondEntry : null,
|
|
406
|
+
getAsFile: () =>
|
|
407
|
+
!protectedMode && secondHandle === 'file' ? files[1] : null,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
},
|
|
411
|
+
})
|
|
412
|
+
await flushPromises()
|
|
413
|
+
await confirm(wrapper).trigger('click')
|
|
414
|
+
const result = wrapper.emitted('select')?.[0]?.[0] as Extract<
|
|
415
|
+
PickerSelection,
|
|
416
|
+
{ source: 'localFile' }
|
|
417
|
+
>
|
|
418
|
+
expect(result.files).toHaveLength(2)
|
|
419
|
+
expect(result.files[0]).toBe(files[0])
|
|
420
|
+
expect(result.files[1]).toBe(files[1])
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
it('shows folder drop errors without a preview panel', async () => {
|
|
425
|
+
const wrapper = await setup({ sources: ['localFile'] })
|
|
426
|
+
await wrapper.getComponent(FileUploader).get('[role="button"]').trigger('drop', {
|
|
427
|
+
dataTransfer: { items: [{ webkitGetAsEntry: () => ({
|
|
428
|
+
name: 'unreadable', isDirectory: true,
|
|
429
|
+
createReader: () => ({ readEntries: (_resolve: unknown, reject: (error: Error) => void) => reject(new Error('Folder cannot be read')) }),
|
|
430
|
+
}) }] },
|
|
431
|
+
})
|
|
432
|
+
await flushPromises()
|
|
433
|
+
expect(wrapper.get('[role="alert"]').text()).toBe('Folder cannot be read')
|
|
434
|
+
expect(confirm(wrapper).attributes('disabled')).toBeDefined()
|
|
435
|
+
})
|
|
436
|
+
|
|
437
|
+
it('does not render sources the host did not supply', async () => {
|
|
438
|
+
const wrapper = await setup({ sources: ['server'] })
|
|
439
|
+
expect(wrapper.find('[aria-label="Local files"]').exists()).toBe(false)
|
|
440
|
+
expect(wrapper.find('[aria-label="This machine"]').exists()).toBe(false)
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
it('retries a failed expanded level and blocks confirmation when validation fails', async () => {
|
|
444
|
+
const data = adapter()
|
|
445
|
+
vi.mocked(data.listChildren).mockRejectedValueOnce(new Error('Share unavailable'))
|
|
446
|
+
const wrapper = await setup({ adapter: data })
|
|
447
|
+
await row(wrapper, folder.path).trigger('click')
|
|
448
|
+
await flushPromises()
|
|
449
|
+
expect(wrapper.text()).toContain('Share unavailable')
|
|
450
|
+
expect(confirm(wrapper).attributes('disabled')).toBeDefined()
|
|
451
|
+
await wrapper.get('.mint-picker__preview-error button').trigger('click')
|
|
452
|
+
await flushPromises()
|
|
453
|
+
expect(confirm(wrapper).attributes('disabled')).toBeUndefined()
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('keyboard disclosure never selects and Escape cancels', async () => {
|
|
457
|
+
const wrapper = await setup()
|
|
458
|
+
await row(wrapper, folder.path).trigger('keydown', { key: 'ArrowRight' })
|
|
459
|
+
await flushPromises()
|
|
460
|
+
expect(row(wrapper, folder.path).attributes('aria-expanded')).toBe('true')
|
|
461
|
+
expect(row(wrapper, folder.path).attributes('aria-selected')).toBe('false')
|
|
462
|
+
await row(wrapper, folder.path).trigger('keydown', { key: 'Escape' })
|
|
463
|
+
expect(wrapper.emitted('cancel')).toHaveLength(1)
|
|
464
|
+
})
|
|
465
|
+
})
|
|
466
|
+
|
|
467
|
+
describe('local metadata and capability validation', () => {
|
|
468
|
+
it('keeps the original File objects without reading bytes', async () => {
|
|
469
|
+
const file = new File(['data'], 'run.mzML.gz')
|
|
470
|
+
const read = vi.spyOn(file, 'arrayBuffer')
|
|
471
|
+
const data = localFilesAdapter([file])
|
|
472
|
+
const nodes = await data.listRoot()
|
|
473
|
+
expect(nodes[0]).toMatchObject({ kind: 'file', format: 'mzML.gz', file })
|
|
474
|
+
expect(read).not.toHaveBeenCalled()
|
|
475
|
+
expect(pickerExclusion(nodes[0]!, { formats: ['.MZML.GZ'] })).toBeNull()
|
|
476
|
+
expect(pickerExclusion(nodes[0]!, { formats: ['raw'] })?.reason).toBe('unsupported')
|
|
477
|
+
expect(pickerExclusion(nodes[0]!, { formats: ['mzML.gz'], maxBytes: 1 })?.reason).toBe(
|
|
478
|
+
'tooLarge',
|
|
479
|
+
)
|
|
480
|
+
})
|
|
481
|
+
it('preserves duplicate plain file names as distinct browser selections', async () => {
|
|
482
|
+
const files = [new File(['a'], 'run.raw'), new File(['b'], 'run.raw')]
|
|
483
|
+
const nodes = await localFilesAdapter(files).listRoot()
|
|
484
|
+
expect(new Set(nodes.map((node) => node.path)).size).toBe(2)
|
|
485
|
+
expect(nodes.map((node) => node.kind === 'file' && node.file)).toEqual(files)
|
|
486
|
+
})
|
|
487
|
+
})
|
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
2
|
-
import { nextTick } from 'vue'
|
|
3
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
1
|
+
import { enableAutoUnmount, flushPromises, mount, type VueWrapper } from '@vue/test-utils'
|
|
2
|
+
import { nextTick, reactive } from 'vue'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
5
|
import GeneratedPlotlyResult from '../../components/GeneratedPlotlyResult.vue'
|
|
6
6
|
|
|
7
7
|
const plotly = vi.hoisted(() => ({
|
|
8
|
+
Plots: { resize: vi.fn() },
|
|
8
9
|
purge: vi.fn(),
|
|
9
10
|
react: vi.fn(),
|
|
10
11
|
}))
|
|
11
12
|
|
|
12
13
|
vi.mock('plotly.js-dist-min', () => ({ default: plotly }))
|
|
14
|
+
enableAutoUnmount(afterEach)
|
|
13
15
|
|
|
14
16
|
async function mountAndRecover(
|
|
15
17
|
fallback?: { media_type: string; data: string },
|
|
@@ -58,6 +60,18 @@ describe('GeneratedPlotlyResult', () => {
|
|
|
58
60
|
expect(wrapper.get('img').attributes('src')).toBe('data:image/png;base64,cG5n')
|
|
59
61
|
})
|
|
60
62
|
|
|
63
|
+
it('should render a static fallback without the error presentation', async () => {
|
|
64
|
+
const wrapper = mount(GeneratedPlotlyResult, {
|
|
65
|
+
props: {
|
|
66
|
+
spec: { data: [] },
|
|
67
|
+
fallback: { media_type: 'image/png', data: 'cG5n' },
|
|
68
|
+
},
|
|
69
|
+
})
|
|
70
|
+
await flushPromises()
|
|
71
|
+
|
|
72
|
+
expect(wrapper.find('.mint-plotly-chart__error').exists()).toBe(false)
|
|
73
|
+
})
|
|
74
|
+
|
|
61
75
|
it('should not redraw an immutable result for nested spec mutations', async () => {
|
|
62
76
|
plotly.react.mockResolvedValue(undefined)
|
|
63
77
|
const wrapper = mount(GeneratedPlotlyResult, {
|
|
@@ -73,6 +87,21 @@ describe('GeneratedPlotlyResult', () => {
|
|
|
73
87
|
expect(plotly.react).toHaveBeenCalledTimes(1)
|
|
74
88
|
})
|
|
75
89
|
|
|
90
|
+
it('should redraw a replaced spec even when its native arrays are reused', async () => {
|
|
91
|
+
plotly.react.mockResolvedValue(undefined)
|
|
92
|
+
const spec = reactive({ data: [{ x: [1], y: [2] }], layout: {}, config: { displayModeBar: true } })
|
|
93
|
+
const wrapper = mount(GeneratedPlotlyResult, { props: { spec } })
|
|
94
|
+
await flushPromises()
|
|
95
|
+
|
|
96
|
+
spec.data[0]!.x.push(3)
|
|
97
|
+
spec.data[0]!.y.push(4)
|
|
98
|
+
await wrapper.setProps({ spec: { ...spec } })
|
|
99
|
+
await flushPromises()
|
|
100
|
+
|
|
101
|
+
expect(plotly.react).toHaveBeenCalledTimes(2)
|
|
102
|
+
expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [1, 3], y: [2, 4] }])
|
|
103
|
+
})
|
|
104
|
+
|
|
76
105
|
it.each([
|
|
77
106
|
['Heatmap', { type: 'heatmap', z: [[1, 2], [3, 4]] }],
|
|
78
107
|
['Scatter3d', { type: 'scatter3d', x: [1], y: [2], z: [3] }],
|
|
@@ -86,7 +115,7 @@ describe('GeneratedPlotlyResult', () => {
|
|
|
86
115
|
expect(plotly.react).toHaveBeenCalledWith(
|
|
87
116
|
expect.any(HTMLElement),
|
|
88
117
|
[trace],
|
|
89
|
-
{},
|
|
118
|
+
expect.objectContaining({ autosize: true }),
|
|
90
119
|
{ responsive: true, displaylogo: false },
|
|
91
120
|
)
|
|
92
121
|
})
|
|
@@ -112,6 +141,24 @@ describe('GeneratedPlotlyResult', () => {
|
|
|
112
141
|
it('should restore the interactive chart after a new immutable spec renders successfully', async () => {
|
|
113
142
|
const wrapper = await mountAndRecover()
|
|
114
143
|
|
|
115
|
-
expect(wrapper.get('.mint-
|
|
144
|
+
expect(wrapper.get('.mint-plotly-chart__plot').isVisible()).toBe(true)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('should reveal the interactive canvas before retrying Plotly', async () => {
|
|
148
|
+
const wrapper = mount(GeneratedPlotlyResult, {
|
|
149
|
+
props: { spec: { data: [] } },
|
|
150
|
+
})
|
|
151
|
+
await flushPromises()
|
|
152
|
+
|
|
153
|
+
let displayDuringRetry: string | undefined
|
|
154
|
+
plotly.react.mockImplementation(async (element: HTMLElement) => {
|
|
155
|
+
displayDuringRetry = element.style.display
|
|
156
|
+
})
|
|
157
|
+
await wrapper.setProps({
|
|
158
|
+
spec: { data: [{ type: 'scatter', x: [1], y: [2] }] },
|
|
159
|
+
})
|
|
160
|
+
await flushPromises()
|
|
161
|
+
|
|
162
|
+
expect(displayDuringRetry).not.toBe('none')
|
|
116
163
|
})
|
|
117
164
|
})
|