@morscherlab/mint-sdk 1.3.0-beta.1 → 1.3.0-beta.10
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 +23 -140
- package/dist/{ResourcePickerListInternal-c3IT8PIu.js → ResourcePickerListInternal-CFAw4vTZ.js} +26 -19
- package/dist/ResourcePickerListInternal-CFAw4vTZ.js.map +1 -0
- package/dist/components/FilePicker.vue.d.ts +4 -1
- package/dist/components/SettingsModal.schema.d.ts +1 -1
- package/dist/components/SettingsModal.vue.d.ts +2 -0
- package/dist/components/WellPlate.colors.d.ts +1 -5
- package/dist/components/WellPlate.rendering.d.ts +5 -2
- package/dist/components/index.d.ts +32 -17
- package/dist/components/index.js +3 -3
- package/dist/components/internal/FilePickerListInternal.vue.d.ts +2 -0
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +9 -3
- package/dist/{components-CvY105kJ.js → components-GDDvof8D.js} +414 -368
- package/dist/components-GDDvof8D.js.map +1 -0
- package/dist/composables/createFilePickerAdapter.d.ts +5 -1
- package/dist/composables/filePicker/usePickerNavigation.d.ts +3 -2
- package/dist/composables/index.d.ts +0 -3
- package/dist/composables/index.js +4 -4
- package/dist/composables/useAutoGroup.d.ts +1 -1
- package/dist/composables/useBioTemplateComponents.d.ts +15 -3
- package/dist/composables/useBioTemplateControls.d.ts +5 -1
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +2 -0
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +5 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +5 -1
- package/dist/composables/useExperimentSave.d.ts +5 -1
- package/dist/composables/useFilePicker.d.ts +4 -1
- package/dist/composables/useGroupAssignment.d.ts +5 -1
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/useTemplateCollection.d.ts +5 -1
- package/dist/composables-D6uqp0UF.js +591 -0
- package/dist/composables-D6uqp0UF.js.map +1 -0
- package/dist/index.js +24 -24
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/lcms.d.ts +11 -11
- package/dist/styles.css +170 -200
- package/dist/templates/index.js +1 -1
- package/dist/{templates-Bj9hZ7iV.js → templates-BU1pet28.js} +31 -44
- package/dist/templates-BU1pet28.js.map +1 -0
- package/dist/types/auth.d.ts +0 -27
- package/dist/types/components.d.ts +8 -0
- package/dist/types/fileBrowserTypes.d.ts +13 -0
- package/dist/types/filePicker.d.ts +8 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +2 -0
- package/dist/utils/rack.d.ts +16 -16
- package/dist/{validation-B4Y8_eEe.js → validation-Dmp9Q5W0.js} +50 -12
- package/dist/validation-Dmp9Q5W0.js.map +1 -0
- package/package.json +1 -6
- package/src/__tests__/components/ActionMenu.test.ts +10 -0
- package/src/__tests__/components/AppTopBar.test.ts +24 -8
- package/src/__tests__/components/EmptyState.test.ts +15 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +12 -5
- package/src/__tests__/components/FilePicker.test.ts +65 -1
- package/src/__tests__/components/FileUploader.test.ts +6 -9
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +23 -4
- package/src/__tests__/components/SettingsModal.test.ts +22 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +4 -28
- package/src/__tests__/components/WellPlate.rendering.test.ts +20 -33
- package/src/__tests__/components/WellPlate.sampleTypeIndicator.test.ts +7 -7
- package/src/__tests__/composables/createFilePickerAdapter.test.ts +92 -1
- package/src/__tests__/composables/usePickerNavigation.test.ts +61 -1
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +18 -6
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +2 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +21 -8
- package/src/__tests__/styles/menusShellRefresh.test.ts +20 -5
- package/src/__tests__/templates/templatesDeprecation.test.ts +58 -11
- package/src/components/ActionMenu.vue +10 -2
- package/src/components/AppTopBar.vue +6 -4
- package/src/components/EmptyState.vue +3 -1
- package/src/components/ExperimentSelectorModal.vue +4 -1
- package/src/components/FilePicker.vue +42 -0
- package/src/components/FileUploader.vue +30 -46
- package/src/components/SettingsModal.schema.ts +4 -2
- package/src/components/SettingsModal.vue +4 -2
- package/src/components/WellPlate.colors.ts +6 -10
- package/src/components/WellPlate.legend.ts +4 -4
- package/src/components/WellPlate.rendering.ts +9 -8
- package/src/components/WellPlate.vue +5 -22
- package/src/components/index.ts +32 -17
- package/src/components/internal/FilePickerListInternal.vue +11 -2
- package/src/components/internal/ResourcePickerListInternal.vue +21 -27
- package/src/composables/autoGroup/index.ts +1 -1
- package/src/composables/createFilePickerAdapter.ts +82 -22
- package/src/composables/filePicker/usePickerNavigation.ts +29 -2
- package/src/composables/index.ts +0 -7
- package/src/composables/useAutoGroup.ts +1 -1
- package/src/composables/useBioTemplateComponents.ts +15 -3
- package/src/composables/useBioTemplateControls.ts +5 -1
- package/src/composables/useBioTemplatePackWorkspace.ts +2 -0
- package/src/composables/useBioTemplatePresetWorkspace.ts +5 -1
- package/src/composables/useBioTemplateWorkspace.ts +5 -1
- package/src/composables/useExperimentSave.ts +5 -1
- package/src/composables/useFilePicker.ts +8 -1
- package/src/composables/useGroupAssignment.ts +5 -1
- package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
- package/src/composables/useTemplateCollection.ts +5 -1
- package/src/index.ts +1 -1
- package/src/lcms.ts +11 -11
- package/src/styles/components/action-menu.css +10 -0
- package/src/styles/components/app-page-selector.css +3 -3
- package/src/styles/components/app-pill-nav.css +1 -1
- package/src/styles/components/app-top-bar.css +18 -38
- package/src/styles/components/empty-state.css +3 -2
- package/src/styles/components/experiment-selector-modal.css +3 -9
- package/src/styles/components/file-picker.css +23 -1
- package/src/styles/components/file-uploader.css +22 -55
- package/src/styles/components/resource-picker-list.css +70 -36
- package/src/styles/components/well-plate.css +18 -55
- package/src/types/auth.ts +0 -37
- package/src/types/components.ts +8 -0
- package/src/types/fileBrowserTypes.ts +15 -0
- package/src/types/filePicker.ts +8 -1
- package/src/types/index.ts +2 -5
- package/src/types/platform.ts +2 -0
- package/src/utils/rack.ts +16 -16
- package/dist/ResourcePickerListInternal-c3IT8PIu.js.map +0 -1
- package/dist/__tests__/composables/useAuth.test.d.ts +0 -1
- package/dist/__tests__/composables/usePasskey.test.d.ts +0 -1
- package/dist/components-CvY105kJ.js.map +0 -1
- package/dist/composables/useAuth.d.ts +0 -47
- package/dist/composables/usePasskey.d.ts +0 -10
- package/dist/composables/useWellPlateEditor.d.ts +0 -33
- package/dist/composables-DL4Vca7L.js +0 -1193
- package/dist/composables-DL4Vca7L.js.map +0 -1
- package/dist/plotly.min-D6Ge6qgX.js +0 -168582
- package/dist/plotly.min-D6Ge6qgX.js.map +0 -1
- package/dist/templates-Bj9hZ7iV.js.map +0 -1
- package/dist/validation-B4Y8_eEe.js.map +0 -1
- package/src/__tests__/composables/useAuth.test.ts +0 -290
- package/src/__tests__/composables/usePasskey.test.ts +0 -38
- package/src/composables/useAuth.ts +0 -470
- package/src/composables/usePasskey.ts +0 -216
- package/src/composables/useWellPlateEditor.ts +0 -415
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.3.0-beta.
|
|
3
|
+
"version": "1.3.0-beta.10",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -67,16 +67,12 @@
|
|
|
67
67
|
"story:preview": "histoire preview"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@simplewebauthn/browser": "^14.0.0",
|
|
71
70
|
"pinia": "^4.0.3",
|
|
72
71
|
"tailwindcss": "^4.3.3",
|
|
73
72
|
"vue": "^3.5.43",
|
|
74
73
|
"vue-router": "^5.3.1"
|
|
75
74
|
},
|
|
76
75
|
"peerDependenciesMeta": {
|
|
77
|
-
"@simplewebauthn/browser": {
|
|
78
|
-
"optional": true
|
|
79
|
-
},
|
|
80
76
|
"vue-router": {
|
|
81
77
|
"optional": true
|
|
82
78
|
}
|
|
@@ -89,7 +85,6 @@
|
|
|
89
85
|
},
|
|
90
86
|
"devDependencies": {
|
|
91
87
|
"@histoire/plugin-vue": "1.0.0-beta.1",
|
|
92
|
-
"@simplewebauthn/browser": "14.0.0",
|
|
93
88
|
"@types/node": "22.20.4",
|
|
94
89
|
"@vitejs/plugin-vue": "6.0.9",
|
|
95
90
|
"@vue/language-core": "3.3.11",
|
|
@@ -267,4 +267,14 @@ describe('ActionMenu', () => {
|
|
|
267
267
|
|
|
268
268
|
expect(document.activeElement).toBe(trigger.element)
|
|
269
269
|
})
|
|
270
|
+
|
|
271
|
+
it('should render a checked item as a checked menu radio when checked is set', async () => {
|
|
272
|
+
const wrapper = mount(ActionMenu, {
|
|
273
|
+
attachTo: document.body,
|
|
274
|
+
props: { label: 'Sort', items: [{ id: 'a', label: 'A', checked: true }, { id: 'b', label: 'B', checked: false }] },
|
|
275
|
+
})
|
|
276
|
+
await wrapper.get('button').trigger('click')
|
|
277
|
+
const radios = [...document.body.querySelectorAll('[role="menuitemradio"]')]
|
|
278
|
+
expect(radios.map((item) => item.getAttribute('aria-checked'))).toEqual(['true', 'false'])
|
|
279
|
+
})
|
|
270
280
|
})
|
|
@@ -1211,12 +1211,20 @@ describe('AppTopBar', () => {
|
|
|
1211
1211
|
expect(wrapper.get('.mint-topbar__plugin-name').text()).toBe('LC-MS Analysis')
|
|
1212
1212
|
})
|
|
1213
1213
|
|
|
1214
|
-
it('should render the
|
|
1214
|
+
it('should render the platform display name when integrated', () => {
|
|
1215
|
+
mockPlatformCtx({ isIntegrated: true, plugin: { ...integratedPlugin, display_name: 'LC-MS Peak Review' } })
|
|
1216
|
+
|
|
1217
|
+
const wrapper = createWrapper()
|
|
1218
|
+
|
|
1219
|
+
expect(wrapper.get('.mint-topbar__plugin-name').text()).toBe('LC-MS Peak Review')
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
it('should not render the plugin version when integrated', () => {
|
|
1215
1223
|
mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
|
|
1216
1224
|
|
|
1217
1225
|
const wrapper = createWrapper()
|
|
1218
1226
|
|
|
1219
|
-
expect(wrapper.
|
|
1227
|
+
expect(wrapper.text()).not.toContain('v2.4.0')
|
|
1220
1228
|
})
|
|
1221
1229
|
|
|
1222
1230
|
it('should not render the plugin identity text when standalone', () => {
|
|
@@ -1225,21 +1233,29 @@ describe('AppTopBar', () => {
|
|
|
1225
1233
|
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1226
1234
|
})
|
|
1227
1235
|
|
|
1228
|
-
it('should
|
|
1236
|
+
it('should hide the plugin name when a page selector is shown', () => {
|
|
1229
1237
|
mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
|
|
1230
1238
|
|
|
1231
1239
|
const wrapper = createWrapper({ pageSelector: pages, currentPageSelectorId: 'peaks' })
|
|
1232
1240
|
|
|
1233
|
-
expect(wrapper.
|
|
1234
|
-
.toContain('mint-page-selector')
|
|
1241
|
+
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1235
1242
|
})
|
|
1236
1243
|
|
|
1237
|
-
it('should
|
|
1238
|
-
mockPlatformCtx({
|
|
1244
|
+
it('should hide the plugin name when the plugin declares several nav items', () => {
|
|
1245
|
+
mockPlatformCtx({
|
|
1246
|
+
isIntegrated: true,
|
|
1247
|
+
plugin: {
|
|
1248
|
+
...integratedPlugin,
|
|
1249
|
+
nav_items: [
|
|
1250
|
+
{ id: 'peaks', path: '/', label: 'Peaks' },
|
|
1251
|
+
{ id: 'report', path: '/report', label: 'Report' },
|
|
1252
|
+
],
|
|
1253
|
+
},
|
|
1254
|
+
})
|
|
1239
1255
|
|
|
1240
1256
|
const wrapper = createWrapper()
|
|
1241
1257
|
|
|
1242
|
-
expect(wrapper.find('.mint-
|
|
1258
|
+
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1243
1259
|
})
|
|
1244
1260
|
|
|
1245
1261
|
it('should not repeat the title next to the integrated identity', () => {
|
|
@@ -20,4 +20,19 @@ describe('EmptyState', () => {
|
|
|
20
20
|
|
|
21
21
|
expect(wrapper.emitted('action')).toHaveLength(1)
|
|
22
22
|
})
|
|
23
|
+
|
|
24
|
+
it('should wrap default-slot content in the spaced content block', () => {
|
|
25
|
+
const wrapper = mount(EmptyState, {
|
|
26
|
+
props: { title: 'No instruments yet', description: 'Add the lab systems.' },
|
|
27
|
+
slots: { default: '<button class="slot-action">New instrument</button>' },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(wrapper.find('.mint-empty-state__content .slot-action').exists()).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not render the content block without default-slot content', () => {
|
|
34
|
+
const wrapper = mount(EmptyState, { props: { title: 'Empty' } })
|
|
35
|
+
|
|
36
|
+
expect(wrapper.find('.mint-empty-state__content').exists()).toBe(false)
|
|
37
|
+
})
|
|
23
38
|
})
|
|
@@ -129,25 +129,32 @@ describe('ExperimentSelectorModal', () => {
|
|
|
129
129
|
expect(wrapper.emitted('update:modelValue')).toEqual([[false]])
|
|
130
130
|
})
|
|
131
131
|
|
|
132
|
-
it('should
|
|
132
|
+
it('should show the experiment code as a pill on the second line', () => {
|
|
133
133
|
experiments.value = [experiment()]
|
|
134
134
|
const wrapper = mountModal({ modelValue: true })
|
|
135
135
|
|
|
136
136
|
expect(wrapper.get('.mint-resource-picker__code').text()).toBe('EXP-007')
|
|
137
137
|
})
|
|
138
138
|
|
|
139
|
-
it('should state the status as text
|
|
139
|
+
it('should state the status as text in the right-hand column', () => {
|
|
140
140
|
experiments.value = [experiment()]
|
|
141
141
|
const wrapper = mountModal({ modelValue: true })
|
|
142
142
|
|
|
143
|
-
expect(wrapper.get('.mint-resource-
|
|
143
|
+
expect(wrapper.get('.mint-resource-picker__aside').text()).toContain('Ongoing')
|
|
144
144
|
})
|
|
145
145
|
|
|
146
|
-
it('should
|
|
146
|
+
it('should tint the status with the experiment status variant', () => {
|
|
147
|
+
experiments.value = [experiment()]
|
|
148
|
+
const wrapper = mountModal({ modelValue: true })
|
|
149
|
+
|
|
150
|
+
expect(wrapper.get('.mint-resource-picker__status').classes()).toContain('mint-resource-picker__status--primary')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('should fill the current experiment row', () => {
|
|
147
154
|
experiments.value = [experiment()]
|
|
148
155
|
const wrapper = mountModal({ modelValue: true, currentExperimentId: 7 })
|
|
149
156
|
|
|
150
|
-
expect(wrapper.find('.mint-resource-
|
|
157
|
+
expect(wrapper.find('.mint-resource-picker__row--selected').exists()).toBe(true)
|
|
151
158
|
})
|
|
152
159
|
|
|
153
160
|
it('clears the shared store from the clear-selection footer', async () => {
|
|
@@ -548,9 +548,10 @@ describe('FilePicker list view', () => {
|
|
|
548
548
|
await row(wrapper, folder.path).trigger('dblclick')
|
|
549
549
|
await flushPromises()
|
|
550
550
|
expect(wrapper.findAll('[role="option"]').map((item) => item.attributes('data-path'))).toEqual([
|
|
551
|
+
// Undated rows fall back to name order under the default newest-first sort.
|
|
552
|
+
unsupported.path,
|
|
551
553
|
one.path,
|
|
552
554
|
two.path,
|
|
553
|
-
unsupported.path,
|
|
554
555
|
])
|
|
555
556
|
})
|
|
556
557
|
|
|
@@ -767,4 +768,67 @@ describe('formatModified', () => {
|
|
|
767
768
|
it('should show the full date outside the current year', () => {
|
|
768
769
|
expect(formatModified('2025-01-02T08:00:00', now)).toBe('2025-01-02')
|
|
769
770
|
})
|
|
771
|
+
|
|
772
|
+
it('should list the newest folder first when opened', async () => {
|
|
773
|
+
const wrapper = await setup({
|
|
774
|
+
adapter: {
|
|
775
|
+
...adapter(),
|
|
776
|
+
listRoot: vi.fn(async () => [
|
|
777
|
+
{ ...folder, modifiedAt: '2026-01-01T00:00:00Z' },
|
|
778
|
+
{ ...other, modifiedAt: '2026-09-29T00:00:00Z' },
|
|
779
|
+
]),
|
|
780
|
+
},
|
|
781
|
+
})
|
|
782
|
+
expect(wrapper.findAll('[role="treeitem"]').map((item) => item.attributes('data-path'))).toEqual([
|
|
783
|
+
other.path, folder.path,
|
|
784
|
+
])
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
it('should reorder rows by name when Name A–Z is chosen from the sort menu', async () => {
|
|
788
|
+
const wrapper = await setup({ defaultSort: 'name-desc' })
|
|
789
|
+
await wrapper.get('.mint-picker__sort').trigger('click')
|
|
790
|
+
await wrapper.findAll('[role="menuitemradio"]').find((item) => item.text() === 'Name A–Z')!.trigger('click')
|
|
791
|
+
expect(wrapper.findAll('[role="treeitem"]').map((item) => item.attributes('data-path'))).toEqual([
|
|
792
|
+
other.path, folder.path,
|
|
793
|
+
])
|
|
794
|
+
})
|
|
795
|
+
|
|
796
|
+
it('should label the sort trigger with the active order', async () => {
|
|
797
|
+
const wrapper = await setup()
|
|
798
|
+
expect(wrapper.get('.mint-picker__sort').text()).toContain('Newest first')
|
|
799
|
+
})
|
|
800
|
+
|
|
801
|
+
it('should tell the user when search results are partial', async () => {
|
|
802
|
+
vi.useFakeTimers()
|
|
803
|
+
const base = adapter()
|
|
804
|
+
const wrapper = await setup({
|
|
805
|
+
adapter: { ...base, search: vi.fn(async () => ({ nodes: [one], truncated: true })) },
|
|
806
|
+
})
|
|
807
|
+
await wrapper.get('[aria-label="Search folders and files"]').setValue('sample')
|
|
808
|
+
await vi.advanceTimersByTimeAsync(400)
|
|
809
|
+
await flushPromises()
|
|
810
|
+
expect(wrapper.get('.mint-picker__truncated').text()).toContain('first 1')
|
|
811
|
+
})
|
|
812
|
+
|
|
813
|
+
it('should still list the partial search results', async () => {
|
|
814
|
+
vi.useFakeTimers()
|
|
815
|
+
const wrapper = await setup({
|
|
816
|
+
adapter: { ...adapter(), search: vi.fn(async () => ({ nodes: [one], truncated: true })) },
|
|
817
|
+
})
|
|
818
|
+
await wrapper.get('[aria-label="Search folders and files"]').setValue('sample')
|
|
819
|
+
await vi.advanceTimersByTimeAsync(400)
|
|
820
|
+
await flushPromises()
|
|
821
|
+
expect(wrapper.findAll('[role="treeitem"]').map((item) => item.attributes('data-path'))).toEqual([one.path])
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
it('should say the search stopped early when a truncated search found nothing', async () => {
|
|
825
|
+
vi.useFakeTimers()
|
|
826
|
+
const wrapper = await setup({
|
|
827
|
+
adapter: { ...adapter(), search: vi.fn(async () => ({ nodes: [], truncated: true })) },
|
|
828
|
+
})
|
|
829
|
+
await wrapper.get('[aria-label="Search folders and files"]').setValue('sample')
|
|
830
|
+
await vi.advanceTimersByTimeAsync(400)
|
|
831
|
+
await flushPromises()
|
|
832
|
+
expect(wrapper.get('.mint-picker__empty').text()).toContain('stopped')
|
|
833
|
+
})
|
|
770
834
|
})
|
|
@@ -3,16 +3,13 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import FileUploader from '../../components/FileUploader.vue'
|
|
4
4
|
|
|
5
5
|
describe('FileUploader', () => {
|
|
6
|
-
it('should
|
|
7
|
-
const wrapper = mount(FileUploader, { props: {
|
|
8
|
-
|
|
9
|
-
dataTransfer: { items: { length: 3 } },
|
|
10
|
-
})
|
|
11
|
-
expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Release to add 3 files')
|
|
6
|
+
it('should name the accepted formats in the prompt when accept is set', () => {
|
|
7
|
+
const wrapper = mount(FileUploader, { props: { accept: '.raw, .mzML' } })
|
|
8
|
+
expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Drop RAW, MZML or click')
|
|
12
9
|
})
|
|
13
10
|
|
|
14
|
-
it('should
|
|
15
|
-
const wrapper = mount(FileUploader
|
|
16
|
-
expect(wrapper.get('.mint-file-
|
|
11
|
+
it('should prompt for any file when accept is not set', () => {
|
|
12
|
+
const wrapper = mount(FileUploader)
|
|
13
|
+
expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Drop any file or click')
|
|
17
14
|
})
|
|
18
15
|
})
|
|
@@ -40,8 +40,27 @@ describe('ResourcePickerListInternal', () => {
|
|
|
40
40
|
expect(setup().get('.mint-resource-picker__code').text()).toBe('EXP-042')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
it('should
|
|
44
|
-
expect(setup().get('.mint-resource-
|
|
43
|
+
it('should render the code as the shared gray code pill', () => {
|
|
44
|
+
expect(setup().get('.mint-resource-picker__code').classes()).toContain('mint-exp-code')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should tint the status by its variant', () => {
|
|
48
|
+
const wrapper = setup({
|
|
49
|
+
describe: (item: unknown) => ({ title: (item as Item).name, status: { label: 'Completed', variant: 'success' } }),
|
|
50
|
+
})
|
|
51
|
+
expect(wrapper.get('.mint-resource-picker__status').classes()).toContain('mint-resource-picker__status--success')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should keep the title line to the title alone', () => {
|
|
55
|
+
expect(setup().get('.mint-resource-picker__title').text()).toBe('Liver microsome stability')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should lead the second line with the code and meta', () => {
|
|
59
|
+
expect(setup().get('.mint-resource-picker__meta').text()).toBe('EXP-042Zhang')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should put the date in the right-hand column', () => {
|
|
63
|
+
expect(setup().get('.mint-resource-picker__aside .mint-resource-picker__date').text()).toBe('09-24')
|
|
45
64
|
})
|
|
46
65
|
|
|
47
66
|
it('should mark the selected row', () => {
|
|
@@ -49,8 +68,8 @@ describe('ResourcePickerListInternal', () => {
|
|
|
49
68
|
expect(wrapper.findAll('[role="option"]')[1]!.attributes('aria-selected')).toBe('true')
|
|
50
69
|
})
|
|
51
70
|
|
|
52
|
-
it('should
|
|
53
|
-
expect(setup({ selectedKey: 2 }).findAll('.mint-resource-
|
|
71
|
+
it('should fill the selected row only', () => {
|
|
72
|
+
expect(setup({ selectedKey: 2 }).findAll('.mint-resource-picker__row--selected')).toHaveLength(1)
|
|
54
73
|
})
|
|
55
74
|
|
|
56
75
|
it('should emit select when a row is clicked', async () => {
|
|
@@ -540,6 +540,28 @@ describe('SettingsModal', () => {
|
|
|
540
540
|
expect(wrapper.findAll('.mint-modal__rail-section').map(heading => heading.text())).toEqual(['Plugin', 'Personal'])
|
|
541
541
|
})
|
|
542
542
|
|
|
543
|
+
it('should list and open Appearance first when appearanceFirst is set', () => {
|
|
544
|
+
const wrapper = mountSettingsModal({
|
|
545
|
+
tabs: [{ id: 'plugins', label: 'Plugins' }],
|
|
546
|
+
showAppearance: true,
|
|
547
|
+
appearanceFirst: true,
|
|
548
|
+
layout: 'vertical',
|
|
549
|
+
})
|
|
550
|
+
|
|
551
|
+
expect(wrapper.get('.mint-modal__rail-item--active').text()).toContain('Appearance')
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
it('should label a manual tab rail section from its section', () => {
|
|
555
|
+
const wrapper = mountSettingsModal({
|
|
556
|
+
tabs: [{ id: 'plugins', label: 'Plugins', section: 'Platform' }],
|
|
557
|
+
showAppearance: true,
|
|
558
|
+
appearanceFirst: true,
|
|
559
|
+
layout: 'vertical',
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
expect(wrapper.findAll('.mint-modal__rail-section').map(heading => heading.text())).toEqual(['Personal', 'Platform'])
|
|
563
|
+
})
|
|
564
|
+
|
|
543
565
|
it('should not render rail section headings when appearance is hidden', () => {
|
|
544
566
|
const wrapper = mountSettingsModal({ schema: railSchema })
|
|
545
567
|
|
|
@@ -26,7 +26,7 @@ describe('WellPlate per-well decoration', () => {
|
|
|
26
26
|
.toEqual(['2px', 'solid', 'var(--mint-warning)'])
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
it('should render
|
|
29
|
+
it('should render no glyph when a QC well has no label or id', () => {
|
|
30
30
|
const wrapper = mount(WellPlate, {
|
|
31
31
|
props: {
|
|
32
32
|
format: 6,
|
|
@@ -34,37 +34,13 @@ describe('WellPlate per-well decoration', () => {
|
|
|
34
34
|
},
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
expect(wrapper.findAll('.mint-well-plate__well')[0].text()).toBe('
|
|
37
|
+
expect(wrapper.findAll('.mint-well-plate__well')[0].text()).toBe('')
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it('should
|
|
41
|
-
const wrapper = mount(WellPlate, {
|
|
42
|
-
props: {
|
|
43
|
-
format: 6,
|
|
44
|
-
showWellIds: true,
|
|
45
|
-
wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType: 'iqc' } },
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
expect(wrapper.findAll('.mint-well-plate__well')[0].find('.mint-well-plate__corner-glyph').text()).toBe('i')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('should keep the iQC marker glyph when a custom label is shown', () => {
|
|
53
|
-
const wrapper = mount(WellPlate, {
|
|
54
|
-
props: {
|
|
55
|
-
format: 6,
|
|
56
|
-
showWellLabels: true,
|
|
57
|
-
wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType: 'iqc', metadata: { label: 'Std 1' } } },
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
expect(wrapper.findAll('.mint-well-plate__well')[0].find('.mint-well-plate__corner-glyph').text()).toBe('i')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('should draw legend swatches with the sample-type marker when the legend is shown', () => {
|
|
40
|
+
it('should draw plain legend swatches when the legend is shown', () => {
|
|
65
41
|
const wrapper = mount(WellPlate, { props: { format: 6, showLegend: true } })
|
|
66
42
|
|
|
67
43
|
const dots = wrapper.findAll('.mint-well-plate__sample-legend-dot')
|
|
68
|
-
expect(dots.map(dot => dot.text())).toEqual(['', '', '
|
|
44
|
+
expect(dots.map(dot => dot.text())).toEqual(['', '', '', ''])
|
|
69
45
|
})
|
|
70
46
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
-
sampleTypeGlyph,
|
|
4
3
|
wellBadge,
|
|
5
4
|
wellClasses,
|
|
6
5
|
wellLabel,
|
|
@@ -70,45 +69,45 @@ describe('WellPlate rendering helpers', () => {
|
|
|
70
69
|
heatmap: { enabled: false },
|
|
71
70
|
sampleColors: { treated: '#123456' },
|
|
72
71
|
})).toEqual({
|
|
73
|
-
backgroundColor: 'color-mix(in srgb, #123456
|
|
74
|
-
border: '1px solid #123456',
|
|
72
|
+
backgroundColor: 'color-mix(in srgb, #123456 15%, transparent)',
|
|
73
|
+
border: '1px solid color-mix(in srgb, #123456 40%, transparent)',
|
|
75
74
|
})
|
|
76
75
|
|
|
77
76
|
expect(wellStyle(well({ sampleType: 'qc' }), {
|
|
78
77
|
heatmap: { enabled: false },
|
|
79
78
|
sampleColors: {},
|
|
80
79
|
})).toEqual({
|
|
81
|
-
backgroundColor: '
|
|
82
|
-
border: '
|
|
80
|
+
backgroundColor: 'color-mix(in srgb, #8b5cf6 15%, transparent)',
|
|
81
|
+
border: '1px solid color-mix(in srgb, #8b5cf6 40%, transparent)',
|
|
83
82
|
})
|
|
84
83
|
})
|
|
85
84
|
|
|
86
|
-
it('should
|
|
85
|
+
it('should tint a sample well green when no custom color is set', () => {
|
|
87
86
|
expect(wellStyle(well({ sampleType: 'sample' }), {
|
|
88
87
|
heatmap: { enabled: false },
|
|
89
88
|
sampleColors: {},
|
|
90
|
-
}).border).toBe('1px solid
|
|
89
|
+
}).border).toBe('1px solid color-mix(in srgb, #10b981 40%, transparent)')
|
|
91
90
|
})
|
|
92
91
|
|
|
93
|
-
it('should
|
|
94
|
-
expect(wellStyle(well({ sampleType: '
|
|
92
|
+
it('should tint a blank well orange when no custom color is set', () => {
|
|
93
|
+
expect(wellStyle(well({ sampleType: 'blank' }), {
|
|
95
94
|
heatmap: { enabled: false },
|
|
96
|
-
sampleColors: {
|
|
97
|
-
}).border).toBe('1px solid #
|
|
95
|
+
sampleColors: {},
|
|
96
|
+
}).border).toBe('1px solid color-mix(in srgb, #f97316 40%, transparent)')
|
|
98
97
|
})
|
|
99
98
|
|
|
100
|
-
it('should
|
|
101
|
-
expect(wellStyle(well({ sampleType: '
|
|
99
|
+
it('should color a well by its group before its sample type', () => {
|
|
100
|
+
expect(wellStyle(well({ sampleType: 'iqc', group: 'trt' }), {
|
|
102
101
|
heatmap: { enabled: false },
|
|
103
102
|
sampleColors: { trt: '#123456' },
|
|
104
|
-
}).border).toBe('
|
|
103
|
+
}).border).toBe('1px solid color-mix(in srgb, #123456 40%, transparent)')
|
|
105
104
|
})
|
|
106
105
|
|
|
107
|
-
it('should
|
|
108
|
-
expect(wellStyle(well({ sampleType: '
|
|
106
|
+
it('should tint a well with a group color token', () => {
|
|
107
|
+
expect(wellStyle(well({ sampleType: 'sample', group: 'trt' }), {
|
|
109
108
|
heatmap: { enabled: false },
|
|
110
|
-
sampleColors: {
|
|
111
|
-
}).
|
|
109
|
+
sampleColors: { trt: 'var(--mint-sample-3)' },
|
|
110
|
+
}).backgroundColor).toBe('color-mix(in srgb, var(--mint-sample-3) 15%, transparent)')
|
|
112
111
|
})
|
|
113
112
|
|
|
114
113
|
it('should leave empty wells to the stylesheet when the well has no sample type', () => {
|
|
@@ -125,29 +124,17 @@ describe('WellPlate rendering helpers', () => {
|
|
|
125
124
|
})).toEqual({})
|
|
126
125
|
})
|
|
127
126
|
|
|
128
|
-
it('should return the Q glyph when the well is QC', () => {
|
|
129
|
-
expect(sampleTypeGlyph(well({ sampleType: 'qc' }))).toBe('Q')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should return the i glyph when the well is iQC', () => {
|
|
133
|
-
expect(sampleTypeGlyph(well({ sampleType: 'iqc' }))).toBe('i')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('should return no glyph when the well is a sample', () => {
|
|
137
|
-
expect(sampleTypeGlyph(well({ sampleType: 'sample' }))).toBeNull()
|
|
138
|
-
})
|
|
139
|
-
|
|
140
127
|
it('formats labels and badges for optional overlays', () => {
|
|
141
128
|
expect(wellLabel(well({ metadata: { label: 'Dose A' } }), true)).toBe('Dose A')
|
|
142
129
|
expect(wellLabel(well({ metadata: { label: 'Dose A' } }), false)).toBeUndefined()
|
|
143
130
|
|
|
144
131
|
expect(wellBadge(well({ metadata: { injectionCount: 3 } }), true)).toEqual({
|
|
145
132
|
text: '3',
|
|
146
|
-
color: '
|
|
133
|
+
color: '#6366f1',
|
|
147
134
|
})
|
|
148
135
|
expect(wellBadge(well({ metadata: { customMethod: 'LCMS-2' } }), true)).toEqual({
|
|
149
|
-
text: '',
|
|
150
|
-
color: '
|
|
136
|
+
text: '+',
|
|
137
|
+
color: '#ec4899',
|
|
151
138
|
})
|
|
152
139
|
expect(wellBadge(well({ metadata: { injectionCount: 1 } }), true)).toBeNull()
|
|
153
140
|
})
|
|
@@ -4,9 +4,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
4
4
|
import WellPlate from '../../components/WellPlate.vue'
|
|
5
5
|
import type { Well } from '../../types'
|
|
6
6
|
|
|
7
|
-
// `showSampleTypeIndicator` is deprecated (3a):
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// `showSampleTypeIndicator` is deprecated (3a): wells carry no letters, color
|
|
8
|
+
// encodes the type. The warning fires once per module, so the order of the
|
|
9
|
+
// first two tests matters.
|
|
10
10
|
|
|
11
11
|
function mountPlate(sampleType: string, showSampleTypeIndicator: boolean) {
|
|
12
12
|
const wells: Record<string, Partial<Well>> = {
|
|
@@ -48,11 +48,11 @@ describe('WellPlate showSampleTypeIndicator (deprecated)', () => {
|
|
|
48
48
|
expect(firstWellText('blank')).toBe('')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it('should
|
|
52
|
-
expect(firstWellText('qc')).toBe('
|
|
51
|
+
it('should render no letter for a QC well when showSampleTypeIndicator is on', () => {
|
|
52
|
+
expect(firstWellText('qc')).toBe('')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
it('should
|
|
56
|
-
expect(firstWellText('iqc')).toBe('
|
|
55
|
+
it('should render no letter for an iQC well when showSampleTypeIndicator is on', () => {
|
|
56
|
+
expect(firstWellText('iqc')).toBe('')
|
|
57
57
|
})
|
|
58
58
|
})
|
|
@@ -110,7 +110,7 @@ it('bounds recursive traversal and rejects truncated listings', async () => {
|
|
|
110
110
|
mount_id, path,
|
|
111
111
|
entries: [{ kind: 'directory', name: 'next', path: path ? `${path}/next` : 'next' }],
|
|
112
112
|
}))
|
|
113
|
-
await
|
|
113
|
+
expect(await adapter.search('missing', { path: '', recursive: true })).toEqual({ nodes: [], truncated: true })
|
|
114
114
|
expect(transport.browse).toHaveBeenCalledTimes(500)
|
|
115
115
|
vi.mocked(transport.browse).mockResolvedValue({ mount_id: 'raw', path: '', entries: [], truncated: true })
|
|
116
116
|
await expect(adapter.listRoot({ refresh: true })).rejects.toThrow('listing limit')
|
|
@@ -162,3 +162,94 @@ it('names the mount holding a path and nothing for a path it cannot decode', ()
|
|
|
162
162
|
const adapter = createFilePickerAdapter(setup().transport)
|
|
163
163
|
expect([adapter.mountOf!(identity('study/run')), adapter.mountOf!('not-json')]).toEqual([identity(), undefined])
|
|
164
164
|
})
|
|
165
|
+
|
|
166
|
+
function treeSetup() {
|
|
167
|
+
const study = { kind: 'directory' as const, name: 'study', path: 'study' }
|
|
168
|
+
const run = { kind: 'file' as const, name: 'run.raw', path: 'study/run.raw' }
|
|
169
|
+
const transport: FilePickerTransport = {
|
|
170
|
+
listMounts: vi.fn(async () => [{ id: 'raw', label: 'Raw files' }]),
|
|
171
|
+
browse: vi.fn(async ({ mount_id, path }) => ({ mount_id, path, entries: [] })),
|
|
172
|
+
tree: vi.fn(async ({ mount_id }) => ({
|
|
173
|
+
listings: [
|
|
174
|
+
{ mount_id, path: '', entries: [study] },
|
|
175
|
+
{ mount_id, path: 'study', entries: [run] },
|
|
176
|
+
],
|
|
177
|
+
truncated: false,
|
|
178
|
+
})),
|
|
179
|
+
search: vi.fn(async ({ path }) => ({
|
|
180
|
+
entries: [study, run].filter((entry) => !path || entry.path.startsWith(`${path}/`)),
|
|
181
|
+
searched_directories: 2,
|
|
182
|
+
truncated: false,
|
|
183
|
+
})),
|
|
184
|
+
}
|
|
185
|
+
return { transport, adapter: createFilePickerAdapter(transport, { rootLocation: { mount_id: 'raw', path: '' } }) }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
it('should serve a subfolder from the tree response when the transport has tree', async () => {
|
|
189
|
+
const { adapter, transport } = treeSetup()
|
|
190
|
+
await adapter.listRoot()
|
|
191
|
+
await adapter.listChildren(identity('study'))
|
|
192
|
+
expect(transport.tree).toHaveBeenCalledTimes(1)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should not call browse when the transport has tree', async () => {
|
|
196
|
+
const { adapter, transport } = treeSetup()
|
|
197
|
+
await adapter.listRoot()
|
|
198
|
+
expect(transport.browse).not.toHaveBeenCalled()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should map tree listings to their own parent folders', async () => {
|
|
202
|
+
const { adapter } = treeSetup()
|
|
203
|
+
await adapter.listRoot()
|
|
204
|
+
expect(await adapter.listChildren(identity('study'))).toMatchObject([
|
|
205
|
+
{ path: identity('study/run.raw'), parentPath: identity('study'), kind: 'file' },
|
|
206
|
+
])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should search on the server when the transport has search', async () => {
|
|
210
|
+
const { adapter, transport } = treeSetup()
|
|
211
|
+
await adapter.search('ru', { path: identity('study'), recursive: true })
|
|
212
|
+
expect(transport.search).toHaveBeenCalledWith({ mount_id: 'raw', path: 'study' }, 'ru', true, undefined)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should give server search results their parent folder', async () => {
|
|
216
|
+
const { adapter } = treeSetup()
|
|
217
|
+
expect(await adapter.search('ru', { path: '', recursive: true })).toMatchObject([
|
|
218
|
+
{ path: identity('study'), parentPath: null },
|
|
219
|
+
{ path: identity('study/run.raw'), parentPath: identity('study') },
|
|
220
|
+
])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should return partial server results marked truncated when the server stops early', async () => {
|
|
224
|
+
const { adapter, transport } = treeSetup()
|
|
225
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
226
|
+
entries: [{ kind: 'file', name: 'run.raw', path: 'study/run.raw' }], searched_directories: 5000, truncated: true,
|
|
227
|
+
})
|
|
228
|
+
expect(await adapter.search('ru', { path: '', recursive: true })).toMatchObject({
|
|
229
|
+
nodes: [{ path: identity('study/run.raw') }], truncated: true,
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should reject a search entry whose name is only a suffix of its last path segment', async () => {
|
|
234
|
+
const { adapter, transport } = treeSetup()
|
|
235
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
236
|
+
entries: [{ kind: 'file', name: 'b', path: 'study/ab' }], searched_directories: 1, truncated: false,
|
|
237
|
+
})
|
|
238
|
+
await expect(adapter.search('b', { path: '', recursive: true })).rejects.toThrow(/Invalid/)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should reject a server search entry outside the searched folder', async () => {
|
|
242
|
+
const { adapter, transport } = treeSetup()
|
|
243
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
244
|
+
entries: [{ kind: 'file', name: 'x.raw', path: 'other/x.raw' }], searched_directories: 1, truncated: false,
|
|
245
|
+
})
|
|
246
|
+
await expect(adapter.search('x', { path: identity('study'), recursive: true })).rejects.toThrow(/Invalid/)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should reject a nested entry from a search limited to this folder', async () => {
|
|
250
|
+
const { adapter, transport } = treeSetup()
|
|
251
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
252
|
+
entries: [{ kind: 'file', name: 'x.raw', path: 'study/private/x.raw' }], searched_directories: 1, truncated: false,
|
|
253
|
+
})
|
|
254
|
+
await expect(adapter.search('x', { path: identity('study'), recursive: false })).rejects.toThrow(/Invalid/)
|
|
255
|
+
})
|