@morscherlab/mint-sdk 1.4.0-beta.1 → 1.4.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/dist/{ActionMenu-CScINahU.js → AlertBox-B2Bgr3ei.js} +40 -209
- package/dist/AlertBox-B2Bgr3ei.js.map +1 -0
- package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-mvm--GxN.js} +161 -110
- package/dist/AppSidebar-mvm--GxN.js.map +1 -0
- package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-nbJj51is.js} +2 -2
- package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-nbJj51is.js.map} +1 -1
- package/dist/ExperimentPopover-5T1fAq2L.js +337 -0
- package/dist/ExperimentPopover-5T1fAq2L.js.map +1 -0
- package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-65peG4Up.js} +198 -181
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +1 -0
- package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-BtnnjWzs.js} +2 -2
- package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-BtnnjWzs.js.map} +1 -1
- package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-DO3IZ7qz.js} +9 -9
- package/dist/SettingsModal-DO3IZ7qz.js.map +1 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
- package/dist/components/AuditTrail.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +2 -2
- package/dist/components/ChartContainer.vue.d.ts +2 -0
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +18 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +0 -4
- package/dist/components/FilePicker.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/PluginIcon.vue.d.ts +2 -2
- package/dist/components/SampleSelector.vue.d.ts +6 -6
- package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
- package/dist/components/StepWizard.vue.d.ts +3 -0
- package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +1 -1
- package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
- package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
- package/dist/composables/createClient.d.ts +2 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/pluginIdentity.d.ts +24 -0
- package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
- package/dist/composables/useAppExperiment.d.ts +8 -0
- package/dist/composables/useCalendarGrid.d.ts +1 -1
- package/dist/composables/useExperimentSelector.d.ts +6 -2
- package/dist/composables/usePlatform.d.ts +1 -0
- package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-B8CTXagP.js} +2 -2
- package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-B8CTXagP.js.map} +1 -1
- package/dist/{data-C2PwI_dn.js → data-CgILxDLW.js} +178 -124
- package/dist/data-CgILxDLW.js.map +1 -0
- package/dist/data.js +8 -8
- package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-Cqp6qNrR.js} +3 -3
- package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-Cqp6qNrR.js.map} +1 -1
- package/dist/index.js +19 -19
- package/dist/{install-DAID6c2-.js → install-CsPFJSx3.js} +15 -14
- package/dist/{install-DAID6c2-.js.map → install-CsPFJSx3.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-DLwRl_Ql.js → lab-MHzp-_J8.js} +128 -147
- package/dist/lab-MHzp-_J8.js.map +1 -0
- package/dist/lab.js +4 -3
- package/dist/{records-BzTcxekG.js → records-B6gpM_3e.js} +472 -274
- package/dist/records-B6gpM_3e.js.map +1 -0
- package/dist/{runtime-D6gVat6r.js → runtime-8bs8EogX.js} +27 -31
- package/dist/runtime-8bs8EogX.js.map +1 -0
- package/dist/runtime.js +6 -6
- package/dist/{shell-5ju97nOS.js → shell-CywENwBT.js} +5 -5
- package/dist/{shell-5ju97nOS.js.map → shell-CywENwBT.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +1606 -1666
- package/dist/types/componentWorkflowTypes.d.ts +2 -0
- package/dist/types/generated/platformContext.d.ts +1 -0
- package/dist/types/generated-ui.d.ts +4 -0
- package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-8yWMu-7_.js} +60 -2
- package/dist/useDropdownState-8yWMu-7_.js.map +1 -0
- package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-DHnxKFAF.js} +3 -3
- package/dist/{useExperimentSamples-CxcnxZ8h.js.map → useExperimentSamples-DHnxKFAF.js.map} +1 -1
- package/dist/{useListReorder-CcbEYxX-.js → useListReorder-BitdUhxn.js} +109 -277
- package/dist/useListReorder-BitdUhxn.js.map +1 -0
- package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-DiwCt77M.js} +71 -57
- package/dist/useRackEditor-DiwCt77M.js.map +1 -0
- package/dist/{useRealtime-PalKue84.js → useRealtime-C3iy9QkU.js} +7 -4
- package/dist/useRealtime-C3iy9QkU.js.map +1 -0
- package/dist/useRequest-D506eHi8.js +244 -0
- package/dist/useRequest-D506eHi8.js.map +1 -0
- package/dist/utils/time.d.ts +7 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +51 -2
- package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
- package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
- package/src/__tests__/components/BaseSelect.test.ts +2 -1
- package/src/__tests__/components/BatchProgressList.test.ts +16 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
- package/src/__tests__/components/ChartContainer.test.ts +11 -0
- package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
- package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
- package/src/__tests__/components/DatePicker.test.ts +21 -0
- package/src/__tests__/components/DatePicker.time.test.ts +2 -2
- package/src/__tests__/components/EmptyState.test.ts +1 -1
- package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +119 -13
- package/src/__tests__/components/GeneratedPluginView.test.ts +22 -1
- package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
- package/src/__tests__/components/PluginWorkspaceView.test.ts +7 -0
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
- package/src/__tests__/components/SampleSelector.test.ts +13 -8
- package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
- package/src/__tests__/components/StepWizard.test.ts +11 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
- package/src/__tests__/components/deprecationTags.test.ts +1 -1
- package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
- package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
- package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
- package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
- package/src/__tests__/styles/cssTokens.test.ts +2 -2
- package/src/__tests__/styles/menusShellRefresh.test.ts +14 -10
- package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
- package/src/__tests__/utils/time.test.ts +18 -1
- package/src/components/AdductText.story.vue +7 -0
- package/src/components/AdductText.vue +1 -1
- package/src/components/AlertBox.story.vue +2 -2
- package/src/components/AlertBox.vue +18 -16
- package/src/components/AppSidebar.story.vue +0 -28
- package/src/components/AppTopBar.story.vue +47 -12
- package/src/components/AppTopBar.vue +118 -114
- package/src/components/ArtifactSaveDialog.story.vue +23 -0
- package/src/components/ArtifactSaveDialog.vue +102 -12
- package/src/components/AuditTrail.story.vue +2 -1
- package/src/components/AuditTrail.vue +2 -21
- package/src/components/Avatar.vue +10 -14
- package/src/components/BaseSelect.multiple.story.vue +13 -2
- package/src/components/BaseSelect.vue +4 -3
- package/src/components/BatchProgressList.vue +35 -25
- package/src/components/ChartContainer.story.vue +2 -1
- package/src/components/ChartContainer.vue +8 -6
- package/src/components/ConfirmDialog.story.vue +1 -1
- package/src/components/EmptyState.vue +1 -1
- package/src/components/ExperimentPopover.story.vue +165 -53
- package/src/components/ExperimentPopover.vue +169 -187
- package/src/components/ExperimentSelectorModal.story.vue +40 -36
- package/src/components/ExperimentSelectorModal.vue +124 -110
- package/src/components/FormField.story.vue +8 -48
- package/src/components/GeneratedPluginView.vue +7 -1
- package/src/components/InstrumentStateBadge.vue +1 -1
- package/src/components/JobsStatusTray.vue +1 -1
- package/src/components/LoadingSpinner.vue +1 -1
- package/src/components/NumberInput.vue +3 -6
- package/src/components/PluginIcon.story.vue +2 -10
- package/src/components/PluginIcon.vue +2 -2
- package/src/components/PluginWorkspaceView.story.vue +1 -1
- package/src/components/RuntimeUpdateNotice.vue +1 -1
- package/src/components/SampleSelector.story.vue +1 -1
- package/src/components/SampleSelector.vue +27 -37
- package/src/components/SampleSelectorGroupNode.vue +1 -1
- package/src/components/SectionCard.story.vue +2 -2
- package/src/components/SequenceProgressBar.story.vue +9 -0
- package/src/components/SequenceProgressBar.vue +6 -1
- package/src/components/SettingsModal.schema.ts +1 -1
- package/src/components/SettingsModal.vue +2 -2
- package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
- package/src/components/SmartGroupManual.story.vue +1 -1
- package/src/components/StepWizard.story.vue +33 -0
- package/src/components/StepWizard.vue +8 -1
- package/src/components/WellPlate.story.vue +3 -1
- package/src/components/WellPlate.vue +7 -0
- package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
- package/src/components/internal/DatePickerDateInternal.vue +16 -40
- package/src/components/internal/DatePickerDateTimeInternal.vue +3 -13
- package/src/components/internal/DatePickerTimeInternal.vue +5 -8
- package/src/components/internal/ResourcePickerListInternal.vue +45 -85
- package/src/components/internal/SelectMultipleInternal.vue +182 -65
- package/src/components/internal/SelectSearchableInternal.vue +2 -3
- package/src/composables/createClient.ts +2 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/pluginIdentity.ts +31 -0
- package/src/composables/pluginWorkspaceModel.ts +2 -2
- package/src/composables/useAppExperiment.ts +20 -0
- package/src/composables/useCalendarGrid.ts +4 -3
- package/src/composables/useExperimentSelector.ts +77 -31
- package/src/composables/usePluginWorkspace.ts +1 -1
- package/src/styles/components/adduct-text.css +13 -5
- package/src/styles/components/alert-box.css +39 -58
- package/src/styles/components/app-layout.css +6 -5
- package/src/styles/components/app-page-selector.css +3 -1
- package/src/styles/components/app-pill-nav.css +11 -12
- package/src/styles/components/app-sidebar.css +9 -18
- package/src/styles/components/app-top-bar.css +57 -55
- package/src/styles/components/artifact-save-dialog.css +153 -2
- package/src/styles/components/audit-trail.css +16 -19
- package/src/styles/components/avatar.css +6 -4
- package/src/styles/components/batch-progress-list.css +38 -36
- package/src/styles/components/button.css +28 -59
- package/src/styles/components/calendar.css +12 -12
- package/src/styles/components/chart-container.css +34 -23
- package/src/styles/components/checkbox.css +2 -12
- package/src/styles/components/collapsible-card.css +27 -29
- package/src/styles/components/confirm-dialog.css +13 -14
- package/src/styles/components/dataframe.css +12 -4
- package/src/styles/components/date-picker.css +80 -92
- package/src/styles/components/datetime-picker.css +31 -43
- package/src/styles/components/divider.css +20 -20
- package/src/styles/components/dose-calculator.css +3 -3
- package/src/styles/components/empty-state.css +20 -17
- package/src/styles/components/experiment-code-badge.css +2 -0
- package/src/styles/components/experiment-popover.css +174 -329
- package/src/styles/components/experiment-selector-modal.css +75 -56
- package/src/styles/components/file-picker.css +3 -3
- package/src/styles/components/file-uploader.css +38 -49
- package/src/styles/components/form-builder.css +3 -3
- package/src/styles/components/formula-input.css +1 -1
- package/src/styles/components/icon-button.css +18 -24
- package/src/styles/components/input.css +5 -4
- package/src/styles/components/instrument-monitor.css +13 -15
- package/src/styles/components/jobs-status-tray.css +31 -25
- package/src/styles/components/modal.css +2 -4
- package/src/styles/components/molecule-input.css +2 -2
- package/src/styles/components/multi-select.css +65 -117
- package/src/styles/components/number-input.css +26 -19
- package/src/styles/components/pill.css +3 -2
- package/src/styles/components/plate-editor.css +0 -1
- package/src/styles/components/plugin-icon.css +26 -23
- package/src/styles/components/popover.css +2 -2
- package/src/styles/components/presence-avatars.css +1 -1
- package/src/styles/components/protocol-step-editor.css +1 -3
- package/src/styles/components/resource-card.css +2 -2
- package/src/styles/components/resource-picker-list.css +6 -57
- package/src/styles/components/sample-hierarchy-tree.css +4 -4
- package/src/styles/components/sample-selector.css +95 -79
- package/src/styles/components/schedule-calendar.css +8 -8
- package/src/styles/components/searchable-select.css +12 -12
- package/src/styles/components/section-card.css +28 -47
- package/src/styles/components/segmented-control.css +18 -15
- package/src/styles/components/select.css +3 -3
- package/src/styles/components/sequence-input.css +2 -2
- package/src/styles/components/sequence-progress-bar.css +17 -8
- package/src/styles/components/smart-group.css +18 -18
- package/src/styles/components/step-wizard.css +52 -16
- package/src/styles/components/tabs.css +2 -2
- package/src/styles/components/textarea.css +3 -3
- package/src/styles/components/time-picker.css +36 -21
- package/src/styles/components/toast.css +12 -10
- package/src/styles/components/toggle.css +1 -11
- package/src/styles/components/tooltip.css +10 -9
- package/src/styles/components/unit-input.css +8 -7
- package/src/styles/components/well-plate.css +9 -2
- package/src/styles/variables.css +106 -75
- package/src/types/componentWorkflowTypes.ts +2 -0
- package/src/types/generated/platformContext.ts +1 -0
- package/src/types/generated-ui.ts +4 -0
- package/src/utils/time.ts +21 -0
- package/dist/ActionMenu-CScINahU.js.map +0 -1
- package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
- package/dist/BaseModal-DfRIuVoj.js.map +0 -1
- package/dist/ExperimentPopover-C4sGw-is.js +0 -359
- package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
- package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
- package/dist/SettingsModal-CSLE31yg.js.map +0 -1
- package/dist/data-C2PwI_dn.js.map +0 -1
- package/dist/lab-DLwRl_Ql.js.map +0 -1
- package/dist/records-BzTcxekG.js.map +0 -1
- package/dist/runtime-D6gVat6r.js.map +0 -1
- package/dist/useListReorder-CcbEYxX-.js.map +0 -1
- package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
- package/dist/useRealtime-PalKue84.js.map +0 -1
- package/dist/useRequest-D5JDfsgZ.js +0 -76
- package/dist/useRequest-D5JDfsgZ.js.map +0 -1
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
import { mount } from '@vue/test-utils'
|
|
3
3
|
import { createPinia, setActivePinia } from 'pinia'
|
|
4
|
-
import {
|
|
4
|
+
import { reactive, ref } from 'vue'
|
|
5
5
|
|
|
6
6
|
import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vue'
|
|
7
7
|
import { useExperimentSelector } from '../../composables/useExperimentSelector'
|
|
8
8
|
import { useExperimentStore } from '../../stores/experiment'
|
|
9
|
-
import type { ExperimentSummary } from '../../types'
|
|
9
|
+
import type { ExperimentFilters, ExperimentSummary } from '../../types'
|
|
10
10
|
|
|
11
11
|
const fetchExperiments = vi.fn(async () => undefined)
|
|
12
12
|
const fetchFilterOptions = vi.fn(async () => undefined)
|
|
13
|
+
const fetchStatusCounts = vi.fn(async () => undefined)
|
|
13
14
|
const experiments = ref<ExperimentSummary[]>([])
|
|
15
|
+
const experimentTypes = ref<{ value: string; label: string }[]>([])
|
|
16
|
+
const projects = ref<{ value: string; label: string }[]>([])
|
|
17
|
+
const statusCounts = ref<Record<string, number>>({})
|
|
18
|
+
let filters: ExperimentFilters = newFilters()
|
|
19
|
+
|
|
20
|
+
function newFilters(): ExperimentFilters {
|
|
21
|
+
return reactive({
|
|
22
|
+
search: undefined,
|
|
23
|
+
status: undefined,
|
|
24
|
+
project: undefined,
|
|
25
|
+
experimentType: undefined,
|
|
26
|
+
datePreset: undefined,
|
|
27
|
+
mine: undefined,
|
|
28
|
+
})
|
|
29
|
+
}
|
|
14
30
|
|
|
15
31
|
vi.mock('../../composables/useExperimentSelector', () => ({
|
|
16
32
|
useExperimentSelector: vi.fn(() => ({
|
|
17
33
|
experiments,
|
|
18
|
-
filters
|
|
19
|
-
search: undefined,
|
|
20
|
-
status: undefined,
|
|
21
|
-
project: undefined,
|
|
22
|
-
experimentType: undefined,
|
|
23
|
-
datePreset: undefined,
|
|
24
|
-
}),
|
|
34
|
+
filters,
|
|
25
35
|
isLoading: ref(false),
|
|
26
36
|
error: ref(null),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
groupedByProject: computed(() => []),
|
|
37
|
+
experimentTypes,
|
|
38
|
+
projects,
|
|
39
|
+
statusCounts,
|
|
31
40
|
fetch: fetchExperiments,
|
|
32
41
|
fetchFilterOptions,
|
|
42
|
+
fetchStatusCounts,
|
|
33
43
|
})),
|
|
34
44
|
}))
|
|
35
45
|
|
|
@@ -49,6 +59,7 @@ function experiment(overrides: Partial<ExperimentSummary> = {}): ExperimentSumma
|
|
|
49
59
|
|
|
50
60
|
interface ModalProps {
|
|
51
61
|
open: boolean
|
|
62
|
+
experimentType?: string
|
|
52
63
|
allowedExperimentTypes?: string[] | null
|
|
53
64
|
currentExperimentId?: number | null
|
|
54
65
|
}
|
|
@@ -62,6 +73,15 @@ function mountModal(props: ModalProps) {
|
|
|
62
73
|
props: ['open', 'title', 'size'],
|
|
63
74
|
template: '<div><slot /></div>',
|
|
64
75
|
},
|
|
76
|
+
// Renders the trigger and every item inline, without the teleported panel.
|
|
77
|
+
ActionMenu: {
|
|
78
|
+
props: ['label', 'items'],
|
|
79
|
+
emits: ['select'],
|
|
80
|
+
template: `<div class="menu-stub" :data-label="label">
|
|
81
|
+
<slot name="trigger" :open="false" :toggle="() => {}" :keydown="() => {}" />
|
|
82
|
+
<button v-for="item in items" :key="item.id" class="menu-stub__item" :data-checked="item.checked" @click="$emit('select', item)">{{ item.label }}</button>
|
|
83
|
+
</div>`,
|
|
84
|
+
},
|
|
65
85
|
},
|
|
66
86
|
},
|
|
67
87
|
})
|
|
@@ -71,8 +91,13 @@ describe('ExperimentSelectorModal', () => {
|
|
|
71
91
|
beforeEach(() => {
|
|
72
92
|
setActivePinia(createPinia())
|
|
73
93
|
experiments.value = []
|
|
94
|
+
experimentTypes.value = []
|
|
95
|
+
projects.value = []
|
|
96
|
+
statusCounts.value = {}
|
|
97
|
+
filters = newFilters()
|
|
74
98
|
fetchExperiments.mockClear()
|
|
75
99
|
fetchFilterOptions.mockClear()
|
|
100
|
+
fetchStatusCounts.mockClear()
|
|
76
101
|
vi.mocked(useExperimentSelector).mockClear()
|
|
77
102
|
})
|
|
78
103
|
|
|
@@ -81,6 +106,7 @@ describe('ExperimentSelectorModal', () => {
|
|
|
81
106
|
|
|
82
107
|
expect(fetchFilterOptions).toHaveBeenCalledTimes(1)
|
|
83
108
|
expect(fetchExperiments).toHaveBeenCalledTimes(1)
|
|
109
|
+
expect(fetchStatusCounts).toHaveBeenCalledTimes(1)
|
|
84
110
|
})
|
|
85
111
|
|
|
86
112
|
it('does not fetch while mounted closed', () => {
|
|
@@ -96,6 +122,7 @@ describe('ExperimentSelectorModal', () => {
|
|
|
96
122
|
expect(useExperimentSelector).toHaveBeenCalledWith({
|
|
97
123
|
experimentType: undefined,
|
|
98
124
|
allowedExperimentTypes: ['mass_spectrometry'],
|
|
125
|
+
withStatusCounts: true,
|
|
99
126
|
})
|
|
100
127
|
})
|
|
101
128
|
|
|
@@ -129,6 +156,12 @@ describe('ExperimentSelectorModal', () => {
|
|
|
129
156
|
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
|
130
157
|
})
|
|
131
158
|
|
|
159
|
+
it('should mark the search field autofocus when the modal opens', () => {
|
|
160
|
+
const wrapper = mountModal({ open: true })
|
|
161
|
+
|
|
162
|
+
expect(wrapper.get('input[type="search"]').attributes()).toHaveProperty('autofocus')
|
|
163
|
+
})
|
|
164
|
+
|
|
132
165
|
it('should show the experiment code as a pill on the second line', () => {
|
|
133
166
|
experiments.value = [experiment()]
|
|
134
167
|
const wrapper = mountModal({ open: true })
|
|
@@ -168,4 +201,77 @@ describe('ExperimentSelectorModal', () => {
|
|
|
168
201
|
expect(store.currentId).toBeNull()
|
|
169
202
|
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
|
170
203
|
})
|
|
204
|
+
|
|
205
|
+
it('should list each status with its count and check the current one', async () => {
|
|
206
|
+
statusCounts.value = { ongoing: 3, planned: 1, completed: 0, cancelled: 2 }
|
|
207
|
+
filters.status = 'ongoing'
|
|
208
|
+
const wrapper = mountModal({ open: true })
|
|
209
|
+
|
|
210
|
+
const items = wrapper.get('[data-label="Status"]').findAll('.menu-stub__item')
|
|
211
|
+
expect(items.map(item => item.text())).toEqual([
|
|
212
|
+
'Any status', 'Ongoing · 3', 'Planned · 1', 'Completed · 0', 'Cancelled · 2',
|
|
213
|
+
])
|
|
214
|
+
expect(items.map(item => item.attributes('data-checked'))).toEqual(['false', 'true', 'false', 'false', 'false'])
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should show the active status with its dot on the trigger and reset it from Any status', async () => {
|
|
218
|
+
filters.status = 'completed'
|
|
219
|
+
const wrapper = mountModal({ open: true })
|
|
220
|
+
const menu = wrapper.get('[data-label="Status"]')
|
|
221
|
+
|
|
222
|
+
const trigger = menu.get('.mint-experiment-selector__toggle')
|
|
223
|
+
expect(trigger.classes()).toContain('mint-experiment-selector__toggle--active')
|
|
224
|
+
expect(trigger.text()).toBe('Completed')
|
|
225
|
+
expect(trigger.find('.mint-experiment-selector__dot--success').exists()).toBe(true)
|
|
226
|
+
|
|
227
|
+
await menu.findAll('.menu-stub__item')[0].trigger('click')
|
|
228
|
+
|
|
229
|
+
expect(filters.status).toBeUndefined()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should filter by the chosen project', async () => {
|
|
233
|
+
projects.value = [{ value: 'Screen', label: 'Screen' }]
|
|
234
|
+
const wrapper = mountModal({ open: true })
|
|
235
|
+
const menu = wrapper.get('[data-label="Project"]')
|
|
236
|
+
|
|
237
|
+
await menu.findAll('.menu-stub__item')[1].trigger('click')
|
|
238
|
+
|
|
239
|
+
expect(filters.project).toBe('Screen')
|
|
240
|
+
expect(menu.get('.mint-experiment-selector__toggle').text()).toBe('Screen')
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it.each([
|
|
244
|
+
{ experimentType: undefined, shown: true },
|
|
245
|
+
{ experimentType: 'screen', shown: false },
|
|
246
|
+
])('should show the type menu only when the type is choosable (experimentType=$experimentType)', ({ experimentType, shown }) => {
|
|
247
|
+
experimentTypes.value = [{ value: 'screen', label: 'Screen' }, { value: 'lcms', label: 'LC-MS' }]
|
|
248
|
+
const wrapper = mountModal({ open: true, experimentType })
|
|
249
|
+
|
|
250
|
+
expect(wrapper.find('[data-label="Type"]').exists()).toBe(shown)
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('should toggle the mine filter from the Mine button', async () => {
|
|
254
|
+
const wrapper = mountModal({ open: true })
|
|
255
|
+
const mine = wrapper.get('[aria-pressed]')
|
|
256
|
+
|
|
257
|
+
await mine.trigger('click')
|
|
258
|
+
|
|
259
|
+
expect(filters.mine).toBe(true)
|
|
260
|
+
expect(mine.attributes('aria-pressed')).toBe('true')
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should clear status, project and mine but keep the search when Clear N is clicked', async () => {
|
|
264
|
+
const wrapper = mountModal({ open: true })
|
|
265
|
+
expect(wrapper.find('.mint-experiment-selector__clear-filters').exists()).toBe(false)
|
|
266
|
+
|
|
267
|
+
Object.assign(filters, { search: 'glu', status: 'planned', project: 'Screen', mine: true })
|
|
268
|
+
await wrapper.vm.$nextTick()
|
|
269
|
+
const clear = wrapper.get('.mint-experiment-selector__clear-filters')
|
|
270
|
+
expect(clear.text()).toBe('Clear 3')
|
|
271
|
+
|
|
272
|
+
await clear.trigger('click')
|
|
273
|
+
|
|
274
|
+
expect(filters).toMatchObject({ search: 'glu', status: undefined, project: undefined, mine: undefined })
|
|
275
|
+
expect(wrapper.find('.mint-experiment-selector__clear-filters').exists()).toBe(false)
|
|
276
|
+
})
|
|
171
277
|
})
|
|
@@ -7,6 +7,7 @@ import GeneratedPluginView from '../../components/GeneratedPluginView.vue'
|
|
|
7
7
|
import GeneratedResultRenderer from '../../components/GeneratedResultRenderer.vue'
|
|
8
8
|
import JobsStatusTray from '../../components/JobsStatusTray.vue'
|
|
9
9
|
import PluginWorkspaceView from '../../components/PluginWorkspaceView.vue'
|
|
10
|
+
import { pluginIdentity, providePluginIdentity } from '../../composables/pluginIdentity'
|
|
10
11
|
|
|
11
12
|
const generated = vi.hoisted(() => ({
|
|
12
13
|
refresh: vi.fn(),
|
|
@@ -16,6 +17,7 @@ const generated = vi.hoisted(() => ({
|
|
|
16
17
|
options: undefined as unknown,
|
|
17
18
|
jobs: [] as Array<Record<string, unknown>>,
|
|
18
19
|
error: null as string | null,
|
|
20
|
+
ui: {} as Record<string, string>,
|
|
19
21
|
}))
|
|
20
22
|
|
|
21
23
|
vi.mock('../../composables/useJobsEndpoint', async () => {
|
|
@@ -30,7 +32,7 @@ vi.mock('../../composables/useJobsEndpoint', async () => {
|
|
|
30
32
|
plugin: {
|
|
31
33
|
name: 'Demo', version: '1.0.0', description: 'Demo plugin', routes_prefix: '/demo',
|
|
32
34
|
},
|
|
33
|
-
ui:
|
|
35
|
+
ui: generated.ui,
|
|
34
36
|
jobs: [
|
|
35
37
|
{
|
|
36
38
|
id: 'run',
|
|
@@ -120,6 +122,8 @@ describe('GeneratedPluginView', () => {
|
|
|
120
122
|
{ ...baseJob, id: 'job-2', job_id: 'job-2', session_id: 'shared-session' },
|
|
121
123
|
]
|
|
122
124
|
generated.error = null
|
|
125
|
+
generated.ui = { title: 'Demo analysis', description: 'Backend-only workspace' }
|
|
126
|
+
providePluginIdentity({ plugin: { name: '' } })
|
|
123
127
|
generated.refresh.mockResolvedValue(undefined)
|
|
124
128
|
generated.submit.mockResolvedValue(undefined)
|
|
125
129
|
generated.result.mockResolvedValue({ kind: 'text', text: 'complete' })
|
|
@@ -159,6 +163,23 @@ describe('GeneratedPluginView', () => {
|
|
|
159
163
|
})
|
|
160
164
|
})
|
|
161
165
|
|
|
166
|
+
it('should seed the top bar identity from the manifest ui subtitle and icon', async () => {
|
|
167
|
+
generated.ui = {
|
|
168
|
+
title: 'Demo analysis', subtitle: 'Peak triage', description: 'Backend-only workspace', icon: 'M4 4h16v16H4z',
|
|
169
|
+
}
|
|
170
|
+
const wrapper = mount(GeneratedPluginView, {
|
|
171
|
+
props: { baseUrl: '/api/demo' },
|
|
172
|
+
global: {
|
|
173
|
+
plugins: [createPinia()],
|
|
174
|
+
stubs: { 'router-link': { template: '<a><slot /></a>' }, Teleport: true },
|
|
175
|
+
},
|
|
176
|
+
})
|
|
177
|
+
await flushPromises()
|
|
178
|
+
|
|
179
|
+
expect(wrapper.findComponent(PluginWorkspaceView).props('spec')).toMatchObject({ subtitle: 'Peak triage' })
|
|
180
|
+
expect(pluginIdentity).toMatchObject({ name: 'Demo analysis', subtitle: 'Peak triage', icon: 'M4 4h16v16H4z' })
|
|
181
|
+
})
|
|
182
|
+
|
|
162
183
|
it('should show the load error text when the workspace fails to load', async () => {
|
|
163
184
|
generated.error = 'Manifest request failed'
|
|
164
185
|
const wrapper = mount(GeneratedPluginView, {
|
|
@@ -18,9 +18,9 @@ describe('InstrumentStateBadge', () => {
|
|
|
18
18
|
expect(wrapper.classes()).toContain('mint-instrument-state--pulse')
|
|
19
19
|
})
|
|
20
20
|
|
|
21
|
-
it('should use the
|
|
21
|
+
it('should use the success tone when the instrument is running', () => {
|
|
22
22
|
const wrapper = mount(InstrumentStateBadge, { props: { state: 'running' } })
|
|
23
|
-
expect(wrapper.classes()).toContain('mint-instrument-state--
|
|
23
|
+
expect(wrapper.classes()).toContain('mint-instrument-state--success')
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
it('should use the success tone when the instrument is connected', () => {
|
|
@@ -28,7 +28,7 @@ describe('InstrumentStateBadge', () => {
|
|
|
28
28
|
expect(wrapper.classes()).toContain('mint-instrument-state--success')
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
it('should use the
|
|
31
|
+
it('should use the hollow square shape when the instrument never reported', () => {
|
|
32
32
|
const wrapper = mount(InstrumentStateBadge, { props: { state: 'never' } })
|
|
33
33
|
expect(wrapper.classes()).toContain('mint-instrument-state--never')
|
|
34
34
|
})
|
|
@@ -38,7 +38,7 @@ describe('InstrumentStateBadge', () => {
|
|
|
38
38
|
expect(wrapper.text()).toBe('Never reported')
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
it('should use the
|
|
41
|
+
it('should use the dash shape when the instrument is inactive', () => {
|
|
42
42
|
const wrapper = mount(InstrumentStateBadge, { props: { state: 'inactive' } })
|
|
43
43
|
expect(wrapper.classes()).toContain('mint-instrument-state--inactive')
|
|
44
44
|
})
|
|
@@ -131,6 +131,13 @@ function mountIntegratedPageSelectorWorkspace(): VueWrapper {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
describe('PluginWorkspaceView', () => {
|
|
134
|
+
it('should leave the top bar title unset when the spec has no title', () => {
|
|
135
|
+
// The top bar then shows the plugin identity, not a generic "Workspace".
|
|
136
|
+
const wrapper = mount(PluginWorkspaceView, { props: { spec: {} }, global: globalOptions })
|
|
137
|
+
|
|
138
|
+
expect(wrapper.findComponent(AppTopBar).props('title')).toBeUndefined()
|
|
139
|
+
})
|
|
140
|
+
|
|
134
141
|
it('should not forward the retired profile-button prop', () => {
|
|
135
142
|
const wrapper = mount(PluginWorkspaceView, {
|
|
136
143
|
global: globalOptions,
|
|
@@ -107,17 +107,6 @@ describe('ResourcePickerListInternal', () => {
|
|
|
107
107
|
expect(wrapper.emitted('update:search')?.[0]).toEqual(['plasma'])
|
|
108
108
|
})
|
|
109
109
|
|
|
110
|
-
it('should head each group with its label and a count', () => {
|
|
111
|
-
const wrapper = setup({ groups: [{ label: 'Recent', items }] })
|
|
112
|
-
expect(wrapper.get('.mint-resource-picker__group').text()).toBe('Recent2')
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should hide the rows of a collapsed group', async () => {
|
|
116
|
-
const wrapper = setup({ groups: [{ label: 'Recent', items }] })
|
|
117
|
-
await wrapper.get('.mint-resource-picker__group').trigger('click')
|
|
118
|
-
expect(wrapper.findAll('[role="option"]')).toHaveLength(0)
|
|
119
|
-
})
|
|
120
|
-
|
|
121
110
|
it('should show skeleton rows while loading', () => {
|
|
122
111
|
expect(setup({ loading: true }).findAll('.mint-resource-picker__skeleton-row')).toHaveLength(4)
|
|
123
112
|
})
|
|
@@ -129,13 +118,4 @@ describe('ResourcePickerListInternal', () => {
|
|
|
129
118
|
it('should show the empty title when there are no items', () => {
|
|
130
119
|
expect(setup({ items: [], emptyTitle: 'Nothing here' }).text()).toContain('Nothing here')
|
|
131
120
|
})
|
|
132
|
-
|
|
133
|
-
it('should leave Enter to a group header while a row is highlighted', async () => {
|
|
134
|
-
const wrapper = setup({ groups: [{ label: 'Recent', items }] })
|
|
135
|
-
await wrapper.findAll('[role="option"]')[0]!.trigger('mouseenter')
|
|
136
|
-
|
|
137
|
-
await wrapper.get('.mint-resource-picker__group').trigger('keydown', { key: 'Enter' })
|
|
138
|
-
|
|
139
|
-
expect(wrapper.emitted('select')).toBeUndefined()
|
|
140
|
-
})
|
|
141
121
|
})
|
|
@@ -97,7 +97,7 @@ describe('SampleSelector', () => {
|
|
|
97
97
|
},
|
|
98
98
|
})
|
|
99
99
|
|
|
100
|
-
expect(wrapper.find('.mint-sample-selector__select-all-count').text()).toBe('0
|
|
100
|
+
expect(wrapper.find('.mint-sample-selector__select-all-count').text()).toBe('0/2')
|
|
101
101
|
expect(wrapper.findAll('.mint-sample-selector__flat-name').map(item => item.text())).toEqual(['S1', 'S2'])
|
|
102
102
|
})
|
|
103
103
|
|
|
@@ -115,9 +115,9 @@ describe('SampleSelector', () => {
|
|
|
115
115
|
},
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
-
const
|
|
119
|
-
expect(
|
|
120
|
-
expect(wrapper.
|
|
118
|
+
const buttons = wrapper.findAll('.mint-sample-selector__header-actions > .mint-sample-selector__action-btn')
|
|
119
|
+
expect(buttons.map(button => button.text())).toEqual(['Smart group'])
|
|
120
|
+
expect(wrapper.find('.mint-sample-selector__header').text()).toContain('Samples0 selected')
|
|
121
121
|
|
|
122
122
|
await wrapper.findAllComponents({ name: 'BaseButton' })[0].trigger('click')
|
|
123
123
|
|
|
@@ -140,8 +140,8 @@ describe('SampleSelector', () => {
|
|
|
140
140
|
},
|
|
141
141
|
})
|
|
142
142
|
|
|
143
|
-
const
|
|
144
|
-
expect(
|
|
143
|
+
const buttons = wrapper.findAll('.mint-sample-selector__header-actions > .mint-sample-selector__action-btn')
|
|
144
|
+
expect(buttons.map(button => button.text())).toEqual(['Group'])
|
|
145
145
|
|
|
146
146
|
await wrapper.findAllComponents({ name: 'BaseButton' })[0].trigger('click')
|
|
147
147
|
|
|
@@ -472,9 +472,14 @@ describe('SampleSelector checkboxes (7c)', () => {
|
|
|
472
472
|
}
|
|
473
473
|
const stubs = { global: { stubs: { AutoGroupModalInternal: true } } }
|
|
474
474
|
|
|
475
|
-
it('should show the mono selected
|
|
475
|
+
it('should show the mono selected/total count when some samples are selected', () => {
|
|
476
476
|
const wrapper = mount(SampleSelector, { props: grouped, ...stubs })
|
|
477
|
-
expect(wrapper.find('.mint-sample-selector__select-all-count').text()).toBe('1
|
|
477
|
+
expect(wrapper.find('.mint-sample-selector__select-all-count').text()).toBe('1/3')
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
it('should offer clear-all-groups in the header when groups exist', () => {
|
|
481
|
+
const wrapper = mount(SampleSelector, { props: { ...grouped, enableGrouping: true }, ...stubs })
|
|
482
|
+
expect(wrapper.find('.mint-sample-selector__header-actions [aria-label="Clear all groups"]').exists()).toBe(true)
|
|
478
483
|
})
|
|
479
484
|
|
|
480
485
|
it('should mark the select-all checkbox mixed when some samples are selected', () => {
|
|
@@ -36,4 +36,16 @@ describe('SequenceProgressBar', () => {
|
|
|
36
36
|
expect(wrapper.get('.mint-sequence-progress__label').text()).toBe('2 / 4 injections')
|
|
37
37
|
expect(wrapper.find('.mint-sequence-progress__footer').exists()).toBe(false)
|
|
38
38
|
})
|
|
39
|
+
|
|
40
|
+
it.each([
|
|
41
|
+
[0, null],
|
|
42
|
+
[2, '2 failed'],
|
|
43
|
+
])('should show the failed count when %i injections failed', (failed, expected) => {
|
|
44
|
+
const wrapper = mount(SequenceProgressBar, {
|
|
45
|
+
props: { failed, progress: { current_sample: 3, total_samples: 6 } },
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const node = wrapper.find('.mint-sequence-progress__failed')
|
|
49
|
+
expect(node.exists() ? node.text() : null).toBe(expected)
|
|
50
|
+
})
|
|
39
51
|
})
|
|
@@ -73,3 +73,14 @@ describe('StepWizard navigation (2c)', () => {
|
|
|
73
73
|
expect(wrapper.find('.mint-wizard__counter').text()).toBe('2 / 2')
|
|
74
74
|
})
|
|
75
75
|
})
|
|
76
|
+
|
|
77
|
+
describe('StepWizard orientation', () => {
|
|
78
|
+
it.each([
|
|
79
|
+
['horizontal', false],
|
|
80
|
+
['vertical', true],
|
|
81
|
+
] as const)('should toggle the vertical modifier when orientation is %s', (orientation, vertical) => {
|
|
82
|
+
const wrapper = mount(StepWizard, { props: { steps, orientation } })
|
|
83
|
+
|
|
84
|
+
expect(wrapper.classes('mint-wizard--vertical')).toBe(vertical)
|
|
85
|
+
})
|
|
86
|
+
})
|
|
@@ -28,15 +28,22 @@ describe('WellPlate per-well decoration', () => {
|
|
|
28
28
|
.toEqual(['2px', 'solid', 'var(--mint-warning)'])
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
-
it(
|
|
31
|
+
it.each([
|
|
32
|
+
['qc', {}, 'Q'],
|
|
33
|
+
['iqc', {}, 'i'],
|
|
34
|
+
['sample', {}, ''],
|
|
35
|
+
['qc', { showWellIds: true }, 'A1'],
|
|
36
|
+
] as const)('should render the %s glyph when the well has no label (spec %o)', (sampleType, spec, text) => {
|
|
32
37
|
const wrapper = mount(WellPlate, {
|
|
33
38
|
props: {
|
|
34
|
-
spec: { format: 6 },
|
|
35
|
-
wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType
|
|
39
|
+
spec: { format: 6, ...spec },
|
|
40
|
+
wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType } },
|
|
36
41
|
},
|
|
37
42
|
})
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
const well = wrapper.findAll('.mint-well-plate__well')[0]
|
|
45
|
+
expect(well.text()).toBe(text)
|
|
46
|
+
expect(well.find('.mint-well-plate__indicator').exists()).toBe(text === 'Q' || text === 'i')
|
|
40
47
|
})
|
|
41
48
|
|
|
42
49
|
it('should draw plain legend swatches when the legend is shown', () => {
|
|
@@ -68,7 +68,7 @@ describe('root exports outside L1 and L2', () => {
|
|
|
68
68
|
|
|
69
69
|
expect(values.sort()).toEqual([
|
|
70
70
|
'defineControlModel', 'defineDataFrame', 'definePluginWorkspaceView', 'downloadBlob',
|
|
71
|
-
'resolvePluginFrontendBase', 'useExperiment', 'usePlatform', 'useRequest', 'useTheme', 'useToast',
|
|
71
|
+
'providePluginIdentity', 'resolvePluginFrontendBase', 'useExperiment', 'usePlatform', 'useRequest', 'useTheme', 'useToast',
|
|
72
72
|
])
|
|
73
73
|
})
|
|
74
74
|
|
|
@@ -490,6 +490,20 @@ describe('useAppExperiment', () => {
|
|
|
490
490
|
expect(state.saveLoading.value).toBe(false)
|
|
491
491
|
})
|
|
492
492
|
|
|
493
|
+
it('should show the refetched update time after a successful save', async () => {
|
|
494
|
+
const store = useExperimentStore()
|
|
495
|
+
useAppExperiment({ onSave: vi.fn().mockResolvedValue('Saved') })
|
|
496
|
+
const state = getState()
|
|
497
|
+
store.select(makeExperiment({ updated_at: '2025-01-01T00:00:00Z' }))
|
|
498
|
+
|
|
499
|
+
const fetch = stubExperimentFetch()
|
|
500
|
+
const saving = state.handleSave()
|
|
501
|
+
fetch.resolveWith(makeExperiment({ updated_at: '2026-10-28T09:30:00Z' }))
|
|
502
|
+
await saving
|
|
503
|
+
|
|
504
|
+
expect(state.popover.value.experimentUpdatedAt).toBe('2026-10-28T09:30:00Z')
|
|
505
|
+
})
|
|
506
|
+
|
|
493
507
|
it('should not set saveSuccessMessage when onSave returns null', async () => {
|
|
494
508
|
const onSave = vi.fn().mockResolvedValue(null)
|
|
495
509
|
useAppExperiment({ onSave })
|
|
@@ -716,8 +730,9 @@ describe('useAppExperiment', () => {
|
|
|
716
730
|
it('exposes live ExperimentPopover props from the provided state', async () => {
|
|
717
731
|
const saveDisabled = ref(false)
|
|
718
732
|
const saveDisabledMessage = ref<string | undefined>('Ready to save')
|
|
733
|
+
const dirty = ref(false)
|
|
719
734
|
const onSave = vi.fn().mockResolvedValue('Saved')
|
|
720
|
-
useAppExperiment({ onSave, saveDisabled, saveDisabledMessage })
|
|
735
|
+
useAppExperiment({ onSave, saveDisabled, saveDisabledMessage, dirty })
|
|
721
736
|
const state = getState()
|
|
722
737
|
|
|
723
738
|
useExperimentStore().select(makeExperiment({
|
|
@@ -725,26 +740,37 @@ describe('useAppExperiment', () => {
|
|
|
725
740
|
name: 'Dose response',
|
|
726
741
|
status: 'ongoing',
|
|
727
742
|
experiment_code: 'EXP-012',
|
|
743
|
+
project: 'legacy-project',
|
|
744
|
+
project_name: 'Hypoxia atlas',
|
|
745
|
+
created_by_user: { id: 3, username: 'jdoe', first_name: 'Jane', last_name: 'Doe' },
|
|
746
|
+
updated_at: '2026-10-28T10:00:00Z',
|
|
728
747
|
}))
|
|
729
748
|
|
|
730
749
|
expect(state.popover.value).toMatchObject({
|
|
731
750
|
experimentName: 'Dose response',
|
|
732
751
|
experimentCode: 'EXP-012',
|
|
733
752
|
experimentStatus: 'ongoing',
|
|
753
|
+
experimentId: 12,
|
|
754
|
+
experimentProject: 'Hypoxia atlas',
|
|
755
|
+
experimentOwner: 'Jane Doe',
|
|
756
|
+
experimentUpdatedAt: '2026-10-28T10:00:00Z',
|
|
734
757
|
isResolving: false,
|
|
735
758
|
showSave: true,
|
|
736
759
|
showDetach: true,
|
|
737
760
|
saveDisabled: false,
|
|
738
761
|
saveDisabledMessage: 'Ready to save',
|
|
739
762
|
saveLoading: false,
|
|
763
|
+
dirty: false,
|
|
740
764
|
})
|
|
741
765
|
expect(state.popover.value.error).toBeUndefined()
|
|
742
766
|
|
|
743
767
|
saveDisabled.value = true
|
|
744
768
|
saveDisabledMessage.value = 'Locked'
|
|
769
|
+
dirty.value = true
|
|
745
770
|
|
|
746
771
|
expect(state.popover.value.saveDisabled).toBe(true)
|
|
747
772
|
expect(state.popover.value.saveDisabledMessage).toBe('Locked')
|
|
773
|
+
expect(state.popover.value.dirty).toBe(true)
|
|
748
774
|
|
|
749
775
|
await state.handleSave()
|
|
750
776
|
|
|
@@ -2,14 +2,16 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { useCalendarGrid } from '../../composables/useCalendarGrid'
|
|
3
3
|
|
|
4
4
|
describe('useCalendarGrid', () => {
|
|
5
|
-
it('builds a stable 42-day month grid with previous and next month padding', () => {
|
|
5
|
+
it('builds a stable Monday-first 42-day month grid with previous and next month padding', () => {
|
|
6
6
|
const calendar = useCalendarGrid()
|
|
7
7
|
calendar.setCurrentMonthFromDate(new Date('2026-05-07T12:00:00'))
|
|
8
8
|
|
|
9
|
-
expect(calendar.weekDays).toEqual(['
|
|
9
|
+
expect(calendar.weekDays).toEqual(['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'])
|
|
10
10
|
expect(calendar.monthYear.value).toBe('May 2026')
|
|
11
11
|
expect(calendar.calendarDays.value).toHaveLength(42)
|
|
12
12
|
expect(calendar.calendarDays.value[0].date.getMonth()).toBe(3)
|
|
13
|
+
expect(calendar.calendarDays.value[0].date.getDate()).toBe(27)
|
|
14
|
+
expect(calendar.calendarDays.value[0].date.getDay()).toBe(1)
|
|
13
15
|
expect(calendar.calendarDays.value.some(day => day.isCurrentMonth && day.date.getDate() === 7)).toBe(true)
|
|
14
16
|
})
|
|
15
17
|
|
|
@@ -194,6 +194,117 @@ describe('useExperimentSelector', () => {
|
|
|
194
194
|
])
|
|
195
195
|
})
|
|
196
196
|
|
|
197
|
+
it('should send the scope filters but not status to the stats route when counting', async () => {
|
|
198
|
+
fakeResponse = { total: 3, by_status: { planned: 1, ongoing: 2, completed: 0, cancelled: 0 } } as never
|
|
199
|
+
const hook = useExperimentSelector()
|
|
200
|
+
Object.assign(hook.filters, { search: 'glu', project: 'Screen', mine: true, status: 'planned' })
|
|
201
|
+
|
|
202
|
+
await hook.fetchStatusCounts()
|
|
203
|
+
|
|
204
|
+
const url = new URL(recordedUrls[0], 'http://mint.test')
|
|
205
|
+
expect(url.pathname).toBe('/experiments/stats')
|
|
206
|
+
expect(Object.fromEntries(url.searchParams)).toEqual({ search: 'glu', project: 'Screen', mine: 'true' })
|
|
207
|
+
expect(hook.statusCounts.value).toEqual({ planned: 1, ongoing: 2, completed: 0, cancelled: 0 })
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('should send mine=true on the list request when the mine filter is set', async () => {
|
|
211
|
+
const hook = useExperimentSelector()
|
|
212
|
+
hook.filters.mine = true
|
|
213
|
+
|
|
214
|
+
await hook.fetch()
|
|
215
|
+
|
|
216
|
+
expect(recordedUrls[0]).toContain('mine=true')
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
it('should sum the per-type stats when several experiment types are allowed', async () => {
|
|
220
|
+
vi.spyOn(useApi().client, 'get').mockImplementation(async (url: string) => {
|
|
221
|
+
recordedUrls.push(url)
|
|
222
|
+
const type = new URL(url, 'http://mint.test').searchParams.get('experiment_type')
|
|
223
|
+
return { data: { total: 2, by_status: { planned: type === 'drug-response' ? 2 : 0, ongoing: type === 'metabolomics' ? 2 : 0 } } }
|
|
224
|
+
})
|
|
225
|
+
const hook = useExperimentSelector({ allowedExperimentTypes: ['drug-response', 'metabolomics'] })
|
|
226
|
+
|
|
227
|
+
await hook.fetchStatusCounts()
|
|
228
|
+
|
|
229
|
+
expect(recordedUrls).toHaveLength(2)
|
|
230
|
+
expect(hook.statusCounts.value).toEqual({ planned: 2, ongoing: 2 })
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should not query the stats route when the selected type is not allowed', async () => {
|
|
234
|
+
const hook = useExperimentSelector({ experimentType: 'proteomics', allowedExperimentTypes: ['drug-response'] })
|
|
235
|
+
|
|
236
|
+
await hook.fetchStatusCounts()
|
|
237
|
+
|
|
238
|
+
expect(recordedUrls).toEqual([])
|
|
239
|
+
expect(hook.statusCounts.value).toEqual({})
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('should refetch status counts with the list when a filter or the search changes and counts are on', async () => {
|
|
243
|
+
vi.useFakeTimers()
|
|
244
|
+
const hook = useExperimentSelector({ withStatusCounts: true })
|
|
245
|
+
const statsUrls = () => recordedUrls.filter(url => url.includes('/experiments/stats'))
|
|
246
|
+
|
|
247
|
+
hook.filters.project = 'Screen'
|
|
248
|
+
await nextTick()
|
|
249
|
+
|
|
250
|
+
expect(statsUrls()).toHaveLength(1)
|
|
251
|
+
expect(recordedUrls.filter(url => url.includes('/experiments?'))).toHaveLength(1)
|
|
252
|
+
|
|
253
|
+
hook.filters.search = 'glu'
|
|
254
|
+
await vi.advanceTimersByTimeAsync(300)
|
|
255
|
+
|
|
256
|
+
expect(statsUrls()).toHaveLength(2)
|
|
257
|
+
expect(statsUrls()[1]).toContain('search=glu')
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
it.each([
|
|
261
|
+
['status', (hook: ReturnType<typeof useExperimentSelector>) => { hook.filters.status = 'planned' }, 1],
|
|
262
|
+
['sort', (hook: ReturnType<typeof useExperimentSelector>) => { hook.sortKey.value = 'name:asc' }, 1],
|
|
263
|
+
['project', (hook: ReturnType<typeof useExperimentSelector>) => { hook.filters.project = 'Screen' }, 2],
|
|
264
|
+
] as const)('should refetch status counts on a %s change only when it changes the counts', async (_name, change, statsCalls) => {
|
|
265
|
+
// One body that serves both routes, so the first stats request succeeds.
|
|
266
|
+
fakeResponse = { ...fakeResponse, by_status: { planned: 1 } } as never
|
|
267
|
+
const hook = useExperimentSelector({ withStatusCounts: true })
|
|
268
|
+
await hook.fetchStatusCounts()
|
|
269
|
+
|
|
270
|
+
change(hook)
|
|
271
|
+
await nextTick()
|
|
272
|
+
|
|
273
|
+
expect(recordedUrls.filter(url => url.includes('/experiments/stats'))).toHaveLength(statsCalls)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('should refetch status counts on a status change when the previous stats request failed', async () => {
|
|
277
|
+
const stats = vi.fn()
|
|
278
|
+
.mockRejectedValueOnce(new Error('offline'))
|
|
279
|
+
.mockResolvedValue({ data: { total: 1, by_status: { planned: 1 } } })
|
|
280
|
+
vi.spyOn(useApi().client, 'get').mockImplementation(async (url: string) =>
|
|
281
|
+
url.includes('/experiments/stats') ? stats() : { data: fakeResponse })
|
|
282
|
+
const hook = useExperimentSelector({ withStatusCounts: true })
|
|
283
|
+
await hook.fetchStatusCounts()
|
|
284
|
+
expect(hook.statusCounts.value).toEqual({})
|
|
285
|
+
|
|
286
|
+
hook.filters.status = 'planned'
|
|
287
|
+
await nextTick()
|
|
288
|
+
|
|
289
|
+
expect(stats).toHaveBeenCalledTimes(2)
|
|
290
|
+
await vi.waitFor(() => expect(hook.statusCounts.value).toEqual({ planned: 1 }))
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('should refetch status counts when a status change flushes a pending search', async () => {
|
|
294
|
+
vi.useFakeTimers()
|
|
295
|
+
const hook = useExperimentSelector({ withStatusCounts: true })
|
|
296
|
+
await hook.fetchStatusCounts()
|
|
297
|
+
const statsUrls = () => recordedUrls.filter(url => url.includes('/experiments/stats'))
|
|
298
|
+
|
|
299
|
+
hook.filters.search = 'glu'
|
|
300
|
+
await nextTick()
|
|
301
|
+
hook.filters.status = 'planned'
|
|
302
|
+
await nextTick()
|
|
303
|
+
|
|
304
|
+
expect(statsUrls()).toHaveLength(2)
|
|
305
|
+
expect(statsUrls()[1]).toContain('search=glu')
|
|
306
|
+
})
|
|
307
|
+
|
|
197
308
|
it('loadMore appends the next page', async () => {
|
|
198
309
|
const hook = useExperimentSelector({ limit: 1 })
|
|
199
310
|
|