@morscherlab/mint-sdk 1.4.0-beta.9 → 1.4.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 +14 -3
- package/dist/{AlertBox-B2Bgr3ei.js → AlertBox-DwgNTEqf.js} +3 -3
- package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-DwgNTEqf.js.map} +1 -1
- package/dist/{AppSidebar-Dq9W1jE2.js → AppSidebar-CZZaxni0.js} +1103 -144
- package/dist/AppSidebar-CZZaxni0.js.map +1 -0
- package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CjWoxAD1.js} +2 -2
- package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
- package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-DujiuQIO.js} +5 -5
- package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-DujiuQIO.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-B4j4eUOJ.js} +114 -98
- package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
- package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-DyUPH3I5.js} +2 -2
- package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
- package/dist/{SettingsModal-483nyI6b.js → SettingsModal-D2MOOtuO.js} +26 -25
- package/dist/SettingsModal-D2MOOtuO.js.map +1 -0
- package/dist/__stories__/experiment-helpers.d.ts +10 -0
- package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
- package/dist/__tests__/components/FloatingPanelsInModal.test.d.ts +1 -0
- package/dist/__tests__/composables/useFloatingPanel.test.d.ts +1 -0
- package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
- package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
- package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
- package/dist/components/AppTopBar.vue.d.ts +20 -5
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
- package/dist/components/JobsStatusTray.vue.d.ts +15 -4
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
- package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
- package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +2 -2
- package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
- package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
- package/dist/composables/useFileImport.d.ts +7 -0
- package/dist/composables/useFloatingPanel.d.ts +29 -0
- package/dist/composables/useJobCenter.d.ts +41 -3
- package/dist/composables/useOnboarding.d.ts +119 -0
- package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
- package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
- package/dist/{data-CgILxDLW.js → data-Boeh4uPI.js} +13 -12
- package/dist/{data-CgILxDLW.js.map → data-Boeh4uPI.js.map} +1 -1
- package/dist/data.d.ts +1 -1
- package/dist/data.js +9 -9
- package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
- package/dist/experiment-DyO54FQB.js.map +1 -0
- package/dist/{experimentStatus-Cqp6qNrR.js → experimentStatus-DCn2QKG7.js} +2 -2
- package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
- package/dist/index.js +20 -20
- package/dist/{install-BtLWTk6H.js → install-BEHp7Mo-.js} +15 -15
- package/dist/{install-BtLWTk6H.js.map → install-BEHp7Mo-.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-MHzp-_J8.js → lab-D61eaX6f.js} +68 -61
- package/dist/{lab-MHzp-_J8.js.map → lab-D61eaX6f.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B6gpM_3e.js → records-D4aQXyZu.js} +193 -133
- package/dist/records-D4aQXyZu.js.map +1 -0
- package/dist/{runtime-7vfl_bI3.js → runtime-q-R0CNLh.js} +400 -33
- package/dist/runtime-q-R0CNLh.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +7 -7
- package/dist/{shell-Cs9OJMnm.js → shell-6CGFvUUw.js} +10 -5
- package/dist/{shell-Cs9OJMnm.js.map → shell-6CGFvUUw.js.map} +1 -1
- package/dist/shell.d.ts +1 -0
- package/dist/shell.js +6 -6
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +2 -2
- package/dist/stores/settings.d.ts +4 -12
- package/dist/styles.css +1191 -479
- package/dist/types/components.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/jobs.d.ts +9 -0
- package/dist/types/platform.d.ts +1 -1
- package/dist/{useDropdownState-8yWMu-7_.js → useDropdownState-uFIz6Yv8.js} +16 -4
- package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
- package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-QEFoq4tf.js} +48 -6
- package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
- package/dist/{useListReorder-BitdUhxn.js → useListReorder-DsuzwWGS.js} +122 -172
- package/dist/useListReorder-DsuzwWGS.js.map +1 -0
- package/dist/{useRackEditor-DiwCt77M.js → useRackEditor-KpZ4iJ6o.js} +22 -32
- package/dist/useRackEditor-KpZ4iJ6o.js.map +1 -0
- package/dist/useRealtime-B58Lo-Tm.js +508 -0
- package/dist/useRealtime-B58Lo-Tm.js.map +1 -0
- package/dist/{useRequest-D506eHi8.js → useRequest-BuDmW8Jc.js} +8 -37
- package/dist/useRequest-BuDmW8Jc.js.map +1 -0
- package/package.json +3 -2
- package/src/__stories__/experiment-helpers.ts +3 -3
- package/src/__tests__/components/ActionMenu.test.ts +2 -2
- package/src/__tests__/components/AppTopBar.test.ts +206 -1
- package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/__tests__/components/BaseModal.test.ts +13 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
- package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
- package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
- package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
- package/src/__tests__/components/SampleSelector.test.ts +20 -0
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- package/src/__tests__/composables/useFileImport.test.ts +36 -0
- package/src/__tests__/composables/useFloatingPanel.test.ts +68 -0
- package/src/__tests__/composables/useJobCenter.test.ts +262 -3
- package/src/__tests__/composables/useOnboarding.test.ts +317 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
- package/src/__tests__/fixtures/table-import/cells.xlsx +0 -0
- package/src/__tests__/fixtures/table-import/samples.xlsx +0 -0
- package/src/__tests__/stores/settings.test.ts +21 -21
- package/src/__tests__/styles/cssTokens.test.ts +4 -1
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
- package/src/components/ActionMenu.vue +6 -37
- package/src/components/AppTopBar.onboarding.story.vue +69 -0
- package/src/components/AppTopBar.story.vue +59 -4
- package/src/components/AppTopBar.vue +132 -10
- package/src/components/BaseButton.vue +16 -2
- package/src/components/BaseModal.vue +2 -1
- package/src/components/ExperimentSelectorModal.story.vue +38 -0
- package/src/components/ExperimentSelectorModal.vue +20 -1
- package/src/components/GeneratedPluginView.vue +4 -9
- package/src/components/GeneratedResultRenderer.vue +3 -4
- package/src/components/JobsStatusTray.story.vue +97 -43
- package/src/components/JobsStatusTray.vue +287 -290
- package/src/components/LayoutResizeHandle.story.vue +65 -4
- package/src/components/PluginWorkspaceView.props.ts +3 -0
- package/src/components/PluginWorkspaceView.vue +17 -0
- package/src/components/SampleSelector.story.vue +21 -6
- package/src/components/SampleSelector.vue +10 -3
- package/src/components/SectionCard.story.vue +33 -0
- package/src/components/SettingsModal.vue +7 -12
- package/src/components/Tooltip.vue +9 -32
- package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/components/internal/DatePickerDateInternal.vue +7 -45
- package/src/components/internal/DatePickerDateTimeInternal.vue +7 -1
- package/src/components/internal/DatePickerTimeInternal.vue +8 -1
- package/src/components/internal/NumberUnitInputInternal.vue +4 -18
- package/src/components/internal/OnboardingLayerInternal.vue +173 -0
- package/src/components/internal/SelectMultipleInternal.vue +6 -1
- package/src/components/internal/SelectSearchableInternal.vue +6 -1
- package/src/components/internal/WellEditPopupInternal.vue +11 -22
- package/src/components/internal/WorkspaceShellInternal.vue +12 -0
- package/src/composables/pluginWorkspaceModel.ts +2 -2
- package/src/composables/useFileImport.ts +51 -1
- package/src/composables/useFloatingPanel.ts +107 -0
- package/src/composables/useJobCenter.ts +188 -6
- package/src/composables/useOnboarding.ts +465 -0
- package/src/composables/usePluginWorkspace.ts +1 -1
- package/src/data.ts +1 -0
- package/src/runtime.ts +2 -1
- package/src/shell.ts +17 -0
- package/src/stores/index.ts +0 -2
- package/src/stores/settings.ts +20 -50
- package/src/styles/components/action-menu.css +1 -1
- package/src/styles/components/app-layout.css +6 -3
- package/src/styles/components/app-page-selector.css +120 -45
- package/src/styles/components/app-top-bar.css +165 -3
- package/src/styles/components/button.css +8 -0
- package/src/styles/components/dataframe.css +1 -1
- package/src/styles/components/date-picker.css +4 -2
- package/src/styles/components/datetime-picker.css +4 -4
- package/src/styles/components/experiment-selector-modal.css +13 -0
- package/src/styles/components/jobs-status-tray.css +134 -296
- package/src/styles/components/layout-resize-handle.css +59 -16
- package/src/styles/components/modal.css +3 -3
- package/src/styles/components/number-input.css +1 -1
- package/src/styles/components/onboarding.css +368 -0
- package/src/styles/components/progress-bar.css +3 -3
- package/src/styles/components/sample-selector.css +43 -0
- package/src/styles/components/searchable-select.css +3 -7
- package/src/styles/components/section-card.css +30 -6
- package/src/styles/components/settings-modal.css +7 -2
- package/src/styles/components/time-picker.css +3 -6
- package/src/styles/components/toast.css +1 -1
- package/src/styles/components/tooltip.css +2 -2
- package/src/styles/components/unit-input.css +1 -1
- package/src/styles/components/well-edit-popup.css +2 -2
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +153 -15
- package/src/types/components.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/src/types/platform.ts +1 -1
- package/dist/AppSidebar-Dq9W1jE2.js.map +0 -1
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +0 -1
- package/dist/SettingsModal-483nyI6b.js.map +0 -1
- package/dist/experiment-43OJtH7R.js.map +0 -1
- package/dist/records-B6gpM_3e.js.map +0 -1
- package/dist/runtime-7vfl_bI3.js.map +0 -1
- package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
- package/dist/useExperimentSamples-DHnxKFAF.js.map +0 -1
- package/dist/useListReorder-BitdUhxn.js.map +0 -1
- package/dist/useRackEditor-DiwCt77M.js.map +0 -1
- package/dist/useRealtime-C3iy9QkU.js +0 -699
- package/dist/useRealtime-C3iy9QkU.js.map +0 -1
- package/dist/useRequest-D506eHi8.js.map +0 -1
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { mount } from '@vue/test-utils'
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { h } from 'vue'
|
|
3
|
+
import { defineComponent, h, nextTick, shallowRef } from 'vue'
|
|
4
4
|
|
|
5
|
+
import BaseModal from '../../components/BaseModal.vue'
|
|
5
6
|
import JobsStatusTray from '../../components/JobsStatusTray.vue'
|
|
6
|
-
import
|
|
7
|
+
import {
|
|
8
|
+
useJobCenter,
|
|
9
|
+
type PluginJobCenterSource,
|
|
10
|
+
type UseJobCenterOptions,
|
|
11
|
+
type UseJobCenterReturn,
|
|
12
|
+
} from '../../composables/useJobCenter'
|
|
7
13
|
import type { JobState, JobStateInput, JobStatus } from '../../types/jobs'
|
|
8
14
|
|
|
9
15
|
function job(status: JobStatus, id: string = status): JobStateInput {
|
|
@@ -43,33 +49,43 @@ function job(status: JobStatus, id: string = status): JobStateInput {
|
|
|
43
49
|
async function openTray(options: {
|
|
44
50
|
source: PluginJobCenterSource
|
|
45
51
|
slots?: Record<string, (props: { job: JobState }) => unknown>
|
|
52
|
+
confirm?: InstanceType<typeof JobsStatusTray>['$props']['confirm']
|
|
53
|
+
rowJob?: UseJobCenterOptions['rowJob']
|
|
54
|
+
/** Run real transitions, so a leaving dialog stays in the DOM for a frame. */
|
|
55
|
+
transitions?: boolean
|
|
46
56
|
}) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
const Host = defineComponent({
|
|
58
|
+
setup() {
|
|
59
|
+
const center = useJobCenter({ source: options.source, loadOnMount: false, rowJob: options.rowJob })
|
|
60
|
+
return () => [
|
|
61
|
+
h('button', { class: 'opener', onClick: center.toggle }, 'Jobs'),
|
|
62
|
+
h(JobsStatusTray, { center, teleportTo: false, confirm: options.confirm }, options.slots),
|
|
63
|
+
]
|
|
54
64
|
},
|
|
55
|
-
slots,
|
|
56
65
|
})
|
|
57
|
-
|
|
66
|
+
const wrapper = mount(Host, {
|
|
67
|
+
attachTo: document.body,
|
|
68
|
+
global: options.transitions ? { stubs: { transition: false } } : {},
|
|
69
|
+
})
|
|
70
|
+
const opener = wrapper.get('.opener')
|
|
71
|
+
;(opener.element as HTMLElement).focus()
|
|
72
|
+
await opener.trigger('click')
|
|
58
73
|
return wrapper
|
|
59
74
|
}
|
|
60
75
|
|
|
61
76
|
describe('JobsStatusTray', () => {
|
|
62
|
-
it('
|
|
77
|
+
it('should show awaiting input as active without default actions when the job has no capability', async () => {
|
|
63
78
|
const wrapper = await openTray({ source: { jobs: [job('awaiting_input')] } })
|
|
64
79
|
|
|
65
80
|
expect([
|
|
66
|
-
wrapper.
|
|
67
|
-
wrapper.find('[aria-label="
|
|
68
|
-
wrapper.find('[aria-label="
|
|
69
|
-
|
|
81
|
+
wrapper.find('[data-section="active"]').exists(),
|
|
82
|
+
wrapper.find('[role="img"][aria-label="Waiting for input"]').exists(),
|
|
83
|
+
wrapper.find('[aria-label="Cancel Job awaiting_input"]').exists(),
|
|
84
|
+
wrapper.find('[aria-label="Delete Job awaiting_input"]').exists(),
|
|
85
|
+
]).toEqual([true, true, false, false])
|
|
70
86
|
})
|
|
71
87
|
|
|
72
|
-
it('
|
|
88
|
+
it('should show only actions supported by both capability and source when rows render', async () => {
|
|
73
89
|
const wrapper = await openTray({
|
|
74
90
|
source: {
|
|
75
91
|
jobs: [
|
|
@@ -82,24 +98,9 @@ describe('JobsStatusTray', () => {
|
|
|
82
98
|
})
|
|
83
99
|
|
|
84
100
|
expect([
|
|
85
|
-
wrapper.find('[aria-label="Cancel
|
|
86
|
-
wrapper.find('[aria-label="Delete
|
|
87
|
-
]).toEqual([true,
|
|
88
|
-
|
|
89
|
-
await wrapper.get('[role="tab"][data-tab="history"]').trigger('click')
|
|
90
|
-
expect(wrapper.find('[aria-label="Delete job"]').exists()).toBe(true)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('exposes dialog state and closes on Escape', async () => {
|
|
94
|
-
const wrapper = await openTray({ source: { jobs: [job('running')] } })
|
|
95
|
-
const trigger = wrapper.get('.mint-jobs-tray__trigger')
|
|
96
|
-
|
|
97
|
-
await wrapper.get('[role="dialog"]').trigger('keydown', { key: 'Escape' })
|
|
98
|
-
|
|
99
|
-
expect([trigger.attributes('aria-expanded'), wrapper.find('[role="dialog"]').exists()]).toEqual([
|
|
100
|
-
'false',
|
|
101
|
-
false,
|
|
102
|
-
])
|
|
101
|
+
wrapper.find('[data-status="awaiting_input"] [aria-label="Cancel Job input"]').exists(),
|
|
102
|
+
wrapper.find('[data-status="completed"] [aria-label="Delete Job done"]').exists(),
|
|
103
|
+
]).toEqual([true, true])
|
|
103
104
|
})
|
|
104
105
|
|
|
105
106
|
it('renders an individual delete action for an active capable job', async () => {
|
|
@@ -110,83 +111,384 @@ describe('JobsStatusTray', () => {
|
|
|
110
111
|
},
|
|
111
112
|
})
|
|
112
113
|
|
|
113
|
-
expect(wrapper.find('[aria-label="Delete
|
|
114
|
+
expect(wrapper.find('[aria-label="Delete Job awaiting_input"]').exists()).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should render a non-modal panel that does not trap Tab when open', async () => {
|
|
118
|
+
const wrapper = await openTray({ source: { jobs: [job('running')] } })
|
|
119
|
+
const panel = wrapper.get('.mint-jobs-tray__panel')
|
|
120
|
+
const tab = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true })
|
|
121
|
+
panel.element.dispatchEvent(tab)
|
|
122
|
+
|
|
123
|
+
expect([
|
|
124
|
+
panel.attributes('role'),
|
|
125
|
+
panel.attributes('aria-modal'),
|
|
126
|
+
tab.defaultPrevented,
|
|
127
|
+
]).toEqual([undefined, undefined, false])
|
|
114
128
|
})
|
|
115
129
|
|
|
116
|
-
it('
|
|
130
|
+
it('should close and return focus to the opener when Escape is pressed', async () => {
|
|
117
131
|
const wrapper = await openTray({ source: { jobs: [job('running')] } })
|
|
118
|
-
|
|
119
|
-
const firstInsidePanel = wrapper.get('[aria-label="Close jobs"]')
|
|
120
|
-
const lastInsidePanel = focusable[focusable.length - 1]!
|
|
121
|
-
lastInsidePanel.element.focus()
|
|
132
|
+
expect(document.activeElement).toBe(wrapper.get('.mint-jobs-tray__panel').element)
|
|
122
133
|
|
|
123
|
-
await wrapper.get('
|
|
134
|
+
await wrapper.get('.mint-jobs-tray__panel').trigger('keydown', { key: 'Escape' })
|
|
124
135
|
|
|
125
|
-
expect(
|
|
136
|
+
expect([
|
|
137
|
+
wrapper.find('.mint-jobs-tray__panel').exists(),
|
|
138
|
+
document.activeElement,
|
|
139
|
+
]).toEqual([false, wrapper.get('.opener').element])
|
|
126
140
|
})
|
|
127
141
|
|
|
128
|
-
it('
|
|
142
|
+
it('should close when the page outside the panel is clicked', async () => {
|
|
129
143
|
const wrapper = await openTray({ source: { jobs: [job('running')] } })
|
|
130
|
-
const panel = wrapper.get('[role="dialog"]')
|
|
131
|
-
const focusable = panel.findAll('button')
|
|
132
|
-
const panelElement = panel.element as HTMLElement
|
|
133
|
-
panelElement.focus()
|
|
134
144
|
|
|
135
|
-
await
|
|
145
|
+
await wrapper.get('.mint-jobs-tray__scrim').trigger('click')
|
|
136
146
|
|
|
137
|
-
expect(
|
|
147
|
+
expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(false)
|
|
138
148
|
})
|
|
139
149
|
|
|
140
|
-
it('
|
|
150
|
+
it('should emit select from a keyboard-accessible title button when a row is chosen', async () => {
|
|
141
151
|
const wrapper = await openTray({ source: { jobs: [job('running')] } })
|
|
142
152
|
|
|
143
|
-
await wrapper.get('.mint-jobs-
|
|
153
|
+
await wrapper.get('.mint-jobs-tray__row-title').trigger('click')
|
|
144
154
|
|
|
145
|
-
expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ job_id: 'running' })
|
|
155
|
+
expect(wrapper.findComponent(JobsStatusTray).emitted('select')?.[0]?.[0]).toMatchObject({ job_id: 'running' })
|
|
146
156
|
})
|
|
147
157
|
|
|
148
|
-
it('
|
|
158
|
+
it('should render each row slot once per row with the row job when slots are given', async () => {
|
|
149
159
|
const wrapper = await openTray({
|
|
150
160
|
source: { jobs: [job('running', 'R-1'), job('completed', 'C-1')] },
|
|
151
161
|
slots: {
|
|
152
|
-
'job-actions': ({ job: row }) =>
|
|
153
|
-
|
|
162
|
+
'job-actions': ({ job: row }) => h('button', { 'data-testid': 'open', 'data-job': row.job_id }, 'Open'),
|
|
163
|
+
'job-title': ({ job: row }) => h('span', { 'data-testid': 'title' }, `Title ${row.job_id}`),
|
|
164
|
+
'job-detail': ({ job: row }) => h('span', { 'data-testid': 'detail' }, `Detail ${row.job_id}`),
|
|
154
165
|
},
|
|
155
166
|
})
|
|
156
167
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
await wrapper.get('[role="tab"][data-tab="history"]').trigger('click')
|
|
161
|
-
|
|
162
|
-
const history = wrapper.findAll('[data-testid="open"]')
|
|
163
|
-
expect(history.map((b) => b.attributes('data-job'))).toEqual(['C-1'])
|
|
168
|
+
expect(['open', 'title', 'detail'].map(id => wrapper.findAll(`[data-testid="${id}"]`).map(el => el.attributes('data-job') ?? el.text())))
|
|
169
|
+
.toEqual([['R-1', 'C-1'], ['Title R-1', 'Title C-1'], ['Detail R-1', 'Detail C-1']])
|
|
164
170
|
})
|
|
165
171
|
|
|
166
172
|
it('should pin awaiting-input jobs to the top when they arrive after other active jobs', async () => {
|
|
167
173
|
const wrapper = await openTray({
|
|
168
174
|
source: { jobs: [job('running', 'R-1'), job('queued', 'Q-1'), job('awaiting_input', 'A-1')] },
|
|
169
175
|
})
|
|
170
|
-
expect(wrapper.findAll('.mint-jobs-
|
|
176
|
+
expect(wrapper.findAll('.mint-jobs-tray__row-title').map((b) => b.text()))
|
|
171
177
|
.toEqual(['Job A-1', 'Job R-1', 'Job Q-1'])
|
|
172
178
|
})
|
|
173
179
|
|
|
174
180
|
it('should tint the awaiting-input row when a job waits for input', async () => {
|
|
175
181
|
const wrapper = await openTray({ source: { jobs: [job('awaiting_input')] } })
|
|
176
|
-
expect(wrapper.get('.mint-jobs-
|
|
182
|
+
expect(wrapper.get('.mint-jobs-tray__row').classes()).toContain('mint-jobs-tray__row--awaiting-input')
|
|
177
183
|
})
|
|
178
184
|
|
|
179
|
-
it('should show a mono percent
|
|
185
|
+
it('should show a mono percent as the row value when a job is running', async () => {
|
|
180
186
|
const running = job('running')
|
|
181
187
|
running.progress = { ...running.progress, percent: 62 }
|
|
182
188
|
const wrapper = await openTray({ source: { jobs: [running] } })
|
|
183
|
-
expect(wrapper.get('.mint-jobs-
|
|
189
|
+
expect([wrapper.get('.mint-jobs-tray__value').text(), wrapper.get('[role="progressbar"]').attributes('aria-label')])
|
|
190
|
+
.toEqual(['62%', 'Job running progress'])
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
it('should show the error in error ink when a job failed', async () => {
|
|
194
|
+
const wrapper = await openTray({ source: { jobs: [{ ...job('failed'), error: 'Peaks not found' }] } })
|
|
195
|
+
const detail = wrapper.get('.mint-jobs-tray__detail')
|
|
196
|
+
expect([detail.text(), detail.classes()]).toEqual(['Peaks not found', expect.arrayContaining(['mint-jobs-tray__detail--error'])])
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it.each([
|
|
200
|
+
[[job('running', 'R-1'), job('queued', 'Q-1'), job('completed', 'C-1')], '2 active · 1 done', ['Active · 2', 'Finished · 1']],
|
|
201
|
+
[[job('failed', 'F-1'), job('cancelled', 'X-1')], '1 failed', ['Finished · 2']],
|
|
202
|
+
[[], 'No jobs', []],
|
|
203
|
+
])('should count jobs in the header and sections for %#', async (jobs, count, sections) => {
|
|
204
|
+
const wrapper = await openTray({ source: { jobs } })
|
|
205
|
+
expect({
|
|
206
|
+
count: wrapper.get('.mint-jobs-tray__head-count').text(),
|
|
207
|
+
sections: wrapper.findAll('.mint-jobs-tray__section').map(el => el.text().replace(/\s+/g, ' ')),
|
|
208
|
+
empty: wrapper.find('.mint-jobs-tray__empty').exists(),
|
|
209
|
+
}).toEqual({ count, sections, empty: jobs.length === 0 })
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should clear finished jobs from the Finished section when the source can clear', async () => {
|
|
213
|
+
const clear = vi.fn(async () => 1)
|
|
214
|
+
const wrapper = await openTray({ source: { jobs: [job('running', 'R-1'), job('completed', 'C-1')], clear } })
|
|
215
|
+
|
|
216
|
+
await wrapper.get('[data-section="finished"] .mint-jobs-tray__section-action').trigger('click')
|
|
217
|
+
|
|
218
|
+
expect([wrapper.find('[data-section="active"] .mint-jobs-tray__section-action').exists(), clear.mock.calls.length])
|
|
219
|
+
.toEqual([false, 1])
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('focus and Escape', () => {
|
|
223
|
+
function mountWithChip() {
|
|
224
|
+
let center!: UseJobCenterReturn
|
|
225
|
+
const Host = defineComponent({
|
|
226
|
+
setup() {
|
|
227
|
+
center = useJobCenter({ source: { jobs: [job('running')] }, loadOnMount: false })
|
|
228
|
+
return () => [
|
|
229
|
+
h('button', { class: 'mint-topbar__job' }, 'Jobs'),
|
|
230
|
+
h('button', { class: 'outside' }, 'Elsewhere'),
|
|
231
|
+
h(JobsStatusTray, { center, teleportTo: false }),
|
|
232
|
+
]
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
return { wrapper: mount(Host, { attachTo: document.body }), center: () => center }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
it.each([
|
|
239
|
+
['the page body', () => (document.activeElement as HTMLElement | null)?.blur()],
|
|
240
|
+
['a detached element', () => {
|
|
241
|
+
const gone = document.body.appendChild(document.createElement('button'))
|
|
242
|
+
gone.focus()
|
|
243
|
+
return () => gone.remove()
|
|
244
|
+
}],
|
|
245
|
+
])('should focus the top-bar jobs control on Escape when the opener was %s', async (_case, focusOpener) => {
|
|
246
|
+
const { wrapper, center } = mountWithChip()
|
|
247
|
+
const detach = focusOpener()
|
|
248
|
+
center().open()
|
|
249
|
+
await nextTick()
|
|
250
|
+
detach?.()
|
|
251
|
+
|
|
252
|
+
await wrapper.get('.mint-jobs-tray__panel').trigger('keydown', { key: 'Escape' })
|
|
253
|
+
|
|
254
|
+
expect(document.activeElement).toBe(wrapper.get('.mint-topbar__job').element)
|
|
255
|
+
wrapper.unmount()
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
it('should close on Escape when focus has left the open panel', async () => {
|
|
259
|
+
const { wrapper, center } = mountWithChip()
|
|
260
|
+
center().open()
|
|
261
|
+
await nextTick()
|
|
262
|
+
;(wrapper.get('.outside').element as HTMLElement).focus()
|
|
263
|
+
|
|
264
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
265
|
+
await nextTick()
|
|
266
|
+
|
|
267
|
+
expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(false)
|
|
268
|
+
wrapper.unmount()
|
|
269
|
+
})
|
|
184
270
|
})
|
|
185
271
|
|
|
186
|
-
it('should
|
|
272
|
+
it('should draw a row from the center row view when a rowJob option overrides it', async () => {
|
|
187
273
|
const wrapper = await openTray({
|
|
188
|
-
source: { jobs: [job('
|
|
274
|
+
source: { jobs: [{ ...job('awaiting_input', 'A-1'), can_cancel: true }], cancel: vi.fn(async () => undefined) },
|
|
275
|
+
rowJob: row => (row.status === 'awaiting_input' ? { status: 'running', percent: 40, message: 'Decoding in the browser' } : null),
|
|
276
|
+
})
|
|
277
|
+
const row = wrapper.get('.mint-jobs-tray__row')
|
|
278
|
+
|
|
279
|
+
expect({
|
|
280
|
+
section: wrapper.get('[data-section="active"]').text().replace(/\s+/g, ' '),
|
|
281
|
+
status: row.attributes('data-status'),
|
|
282
|
+
tinted: row.classes('mint-jobs-tray__row--awaiting-input'),
|
|
283
|
+
icon: row.get('[role="img"]').attributes('aria-label'),
|
|
284
|
+
value: row.get('.mint-jobs-tray__value').text(),
|
|
285
|
+
progress: row.get('[role="progressbar"]').attributes('aria-valuenow'),
|
|
286
|
+
detail: row.get('.mint-jobs-tray__detail').text(),
|
|
287
|
+
cancel: row.find('[aria-label="Cancel Job A-1"]').exists(),
|
|
288
|
+
}).toEqual({
|
|
289
|
+
section: 'Active · 1',
|
|
290
|
+
status: 'awaiting_input',
|
|
291
|
+
tinted: false,
|
|
292
|
+
icon: 'Running',
|
|
293
|
+
value: '40%',
|
|
294
|
+
progress: '40',
|
|
295
|
+
detail: 'Decoding in the browser',
|
|
296
|
+
cancel: true,
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('should mount no confirm dialog when confirm is off', async () => {
|
|
301
|
+
const wrapper = await openTray({ source: { jobs: [job('completed')], delete: vi.fn(async () => true) } })
|
|
302
|
+
expect(wrapper.findComponent(BaseModal).exists()).toBe(false)
|
|
303
|
+
wrapper.unmount()
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
it('should keep the tray open when an inner handler prevents the Escape default', async () => {
|
|
307
|
+
const wrapper = await openTray({
|
|
308
|
+
source: { jobs: [job('running', 'R-1')] },
|
|
309
|
+
slots: {
|
|
310
|
+
'job-actions': () => h('input', {
|
|
311
|
+
class: 'inner',
|
|
312
|
+
onKeydown: (event: KeyboardEvent) => { if (event.key === 'Escape') event.preventDefault() },
|
|
313
|
+
}),
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
const inner = wrapper.get('.inner').element as HTMLElement
|
|
317
|
+
inner.focus()
|
|
318
|
+
|
|
319
|
+
inner.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }))
|
|
320
|
+
await nextTick()
|
|
321
|
+
|
|
322
|
+
expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(true)
|
|
323
|
+
wrapper.unmount()
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
it('should leave focus on the page when a job update arrives while nothing in the tray is focused', async () => {
|
|
327
|
+
const jobs = shallowRef([job('running', 'R-1')])
|
|
328
|
+
const wrapper = await openTray({ source: { jobs } })
|
|
329
|
+
;(document.activeElement as HTMLElement).blur()
|
|
330
|
+
|
|
331
|
+
jobs.value = [job('completed', 'R-1')]
|
|
332
|
+
await nextTick()
|
|
333
|
+
await nextTick()
|
|
334
|
+
|
|
335
|
+
expect(document.activeElement).toBe(document.body)
|
|
336
|
+
wrapper.unmount()
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('should focus the panel when the focused row leaves the list while the tray is open', async () => {
|
|
340
|
+
const jobs = shallowRef([job('completed', 'C-1'), job('completed', 'C-2')])
|
|
341
|
+
const wrapper = await openTray({ source: { jobs, delete: vi.fn(async () => true) } })
|
|
342
|
+
;(wrapper.get('[aria-label="Delete Job C-1"]').element as HTMLElement).focus()
|
|
343
|
+
|
|
344
|
+
jobs.value = [job('completed', 'C-2')]
|
|
345
|
+
await nextTick()
|
|
346
|
+
await nextTick()
|
|
347
|
+
|
|
348
|
+
expect(document.activeElement).toBe(wrapper.get('.mint-jobs-tray__panel').element)
|
|
349
|
+
wrapper.unmount()
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
describe('confirmation', () => {
|
|
353
|
+
const dialog = () => document.body.querySelector<HTMLElement>('.mint-modal [role="dialog"], .mint-modal__container')
|
|
354
|
+
|
|
355
|
+
function confirmSource() {
|
|
356
|
+
return {
|
|
357
|
+
jobs: [{ ...job('running', 'R-1'), can_cancel: true }, job('completed', 'C-1'), job('failed', 'F-1')],
|
|
358
|
+
cancel: vi.fn(async () => undefined),
|
|
359
|
+
delete: vi.fn(async () => true),
|
|
360
|
+
clear: vi.fn(async () => 2),
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const triggers = {
|
|
364
|
+
cancel: '[aria-label="Cancel Job R-1"]',
|
|
365
|
+
delete: '[aria-label="Delete Job C-1"]',
|
|
366
|
+
clear: '[data-section="finished"] .mint-jobs-tray__section-action',
|
|
367
|
+
} as const
|
|
368
|
+
|
|
369
|
+
it.each([
|
|
370
|
+
['cancel', true, 'Job R-1'],
|
|
371
|
+
['delete', { delete: true }, 'Job C-1'],
|
|
372
|
+
['clear', { clear: true }, '2 finished jobs'],
|
|
373
|
+
] as const)('should run %s only after the confirm dialog is confirmed when confirm is %o', async (kind, confirm, named) => {
|
|
374
|
+
const source = confirmSource()
|
|
375
|
+
const wrapper = await openTray({ source, confirm })
|
|
376
|
+
|
|
377
|
+
await wrapper.get(triggers[kind]).trigger('click')
|
|
378
|
+
await nextTick()
|
|
379
|
+
const shown = dialog()
|
|
380
|
+
const before = source[kind].mock.calls.length
|
|
381
|
+
const confirmButton = [...shown!.querySelectorAll('button')].at(-1)!
|
|
382
|
+
confirmButton.click()
|
|
383
|
+
await flushPromises()
|
|
384
|
+
|
|
385
|
+
expect([shown?.textContent?.includes(named), before, source[kind].mock.calls.length]).toEqual([true, 0, 1])
|
|
386
|
+
wrapper.unmount()
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
it.each([
|
|
390
|
+
['off', undefined],
|
|
391
|
+
['set for another action', { delete: true }],
|
|
392
|
+
] as const)('should cancel at once without a dialog when confirm is %s', async (_case, confirm) => {
|
|
393
|
+
const source = confirmSource()
|
|
394
|
+
const wrapper = await openTray({ source, confirm })
|
|
395
|
+
|
|
396
|
+
await wrapper.get(triggers.cancel).trigger('click')
|
|
397
|
+
await flushPromises()
|
|
398
|
+
|
|
399
|
+
expect([dialog(), source.cancel.mock.calls.length]).toEqual([null, 1])
|
|
400
|
+
wrapper.unmount()
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it('should run the action once when Confirm is clicked twice while the dialog fades out', async () => {
|
|
404
|
+
const source = confirmSource()
|
|
405
|
+
const wrapper = await openTray({ source, confirm: true, transitions: true })
|
|
406
|
+
await wrapper.get(triggers.clear).trigger('click')
|
|
407
|
+
await nextTick()
|
|
408
|
+
const confirmButton = [...dialog()!.querySelectorAll('button')].at(-1)!
|
|
409
|
+
|
|
410
|
+
confirmButton.click()
|
|
411
|
+
confirmButton.click()
|
|
412
|
+
await flushPromises()
|
|
413
|
+
|
|
414
|
+
expect([confirmButton.isConnected, source.clear.mock.calls.length]).toEqual([true, 1])
|
|
415
|
+
await vi.waitFor(() => expect(dialog()).toBeNull())
|
|
416
|
+
wrapper.unmount()
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('should close only the dialog when Escape is pressed in it', async () => {
|
|
420
|
+
const source = confirmSource()
|
|
421
|
+
const wrapper = await openTray({ source, confirm: true })
|
|
422
|
+
await wrapper.get(triggers.delete).trigger('click')
|
|
423
|
+
await nextTick()
|
|
424
|
+
|
|
425
|
+
dialog()!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
426
|
+
await flushPromises()
|
|
427
|
+
|
|
428
|
+
expect([dialog(), wrapper.find('.mint-jobs-tray__panel').exists(), source.delete.mock.calls.length])
|
|
429
|
+
.toEqual([null, true, 0])
|
|
430
|
+
wrapper.unmount()
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
it('should ask again with the new count when the finished jobs change before Confirm', async () => {
|
|
434
|
+
const jobs = shallowRef([job('completed', 'C-1'), job('failed', 'F-1')])
|
|
435
|
+
const clear = vi.fn(async () => 3)
|
|
436
|
+
const wrapper = await openTray({ source: { jobs, clear }, confirm: true })
|
|
437
|
+
await wrapper.get(triggers.clear).trigger('click')
|
|
438
|
+
await nextTick()
|
|
439
|
+
|
|
440
|
+
jobs.value = [...jobs.value, job('cancelled', 'X-1')]
|
|
441
|
+
await nextTick()
|
|
442
|
+
;[...dialog()!.querySelectorAll('button')].at(-1)!.click()
|
|
443
|
+
await flushPromises()
|
|
444
|
+
const asked = [dialog()?.textContent?.includes('3 finished jobs'), clear.mock.calls.length]
|
|
445
|
+
;[...dialog()!.querySelectorAll('button')].at(-1)!.click()
|
|
446
|
+
await flushPromises()
|
|
447
|
+
|
|
448
|
+
expect([...asked, dialog(), clear.mock.calls.length]).toEqual([true, 0, null, 1])
|
|
449
|
+
wrapper.unmount()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it.each([
|
|
453
|
+
['delete', 'the job is gone', [] as JobStateInput[]],
|
|
454
|
+
['cancel', 'the job finished', [{ ...job('completed', 'R-1'), can_cancel: true }]],
|
|
455
|
+
['delete', 'the job is no longer deletable', [{ ...job('completed', 'C-1'), can_delete: false }]],
|
|
456
|
+
] as const)('should close the %s dialog without acting when %s before Confirm', async (kind, _case, next) => {
|
|
457
|
+
const source = { ...confirmSource(), jobs: shallowRef<JobStateInput[]>(confirmSource().jobs) }
|
|
458
|
+
const wrapper = await openTray({ source, confirm: true })
|
|
459
|
+
await wrapper.get(triggers[kind]).trigger('click')
|
|
460
|
+
await nextTick()
|
|
461
|
+
|
|
462
|
+
source.jobs.value = [...next]
|
|
463
|
+
await nextTick()
|
|
464
|
+
;[...dialog()!.querySelectorAll('button')].at(-1)!.click()
|
|
465
|
+
await flushPromises()
|
|
466
|
+
|
|
467
|
+
expect([dialog(), source[kind].mock.calls.length]).toEqual([null, 0])
|
|
468
|
+
wrapper.unmount()
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('should drop the pending action when the job source changes while the dialog is open', async () => {
|
|
472
|
+
const first = confirmSource()
|
|
473
|
+
const second = confirmSource()
|
|
474
|
+
const current = shallowRef<PluginJobCenterSource>(first)
|
|
475
|
+
const Host = defineComponent({
|
|
476
|
+
setup() {
|
|
477
|
+
const center = useJobCenter({ source: current, loadOnMount: false })
|
|
478
|
+
center.open()
|
|
479
|
+
return () => h(JobsStatusTray, { center, teleportTo: false, confirm: true })
|
|
480
|
+
},
|
|
481
|
+
})
|
|
482
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
483
|
+
await nextTick()
|
|
484
|
+
await wrapper.get(triggers.delete).trigger('click')
|
|
485
|
+
await nextTick()
|
|
486
|
+
|
|
487
|
+
current.value = second
|
|
488
|
+
await nextTick()
|
|
489
|
+
|
|
490
|
+
expect([dialog(), first.delete.mock.calls.length, second.delete.mock.calls.length]).toEqual([null, 0, 0])
|
|
491
|
+
wrapper.unmount()
|
|
189
492
|
})
|
|
190
|
-
expect(wrapper.get('.mint-jobs-tray__subtitle').text()).toBe('1 running · 1 queued · 1 waiting')
|
|
191
493
|
})
|
|
192
494
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
2
|
import type { VueWrapper } from '@vue/test-utils'
|
|
3
3
|
import { createPinia, setActivePinia } from 'pinia'
|
|
4
|
-
import { computed, h, nextTick, ref } from 'vue'
|
|
4
|
+
import { computed, h, nextTick, ref, shallowRef } from 'vue'
|
|
5
5
|
import { describe, expect, it, vi } from 'vitest'
|
|
6
6
|
import AppLayout from '../../components/AppLayout.vue'
|
|
7
7
|
import AppSidebar from '../../components/AppSidebar.vue'
|
|
@@ -10,6 +10,7 @@ import ExperimentPopover from '../../components/ExperimentPopover.vue'
|
|
|
10
10
|
import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vue'
|
|
11
11
|
import PluginWorkspaceView from '../../components/PluginWorkspaceView.vue'
|
|
12
12
|
import AppToastContainer from '../../components/AppToastContainer.vue'
|
|
13
|
+
import JobsStatusTray from '../../components/JobsStatusTray.vue'
|
|
13
14
|
import { defineControlModel, defineDoseDesignControlModel } from '../../composables/useControlSchema'
|
|
14
15
|
import { usePlatform } from '../../composables/usePlatform'
|
|
15
16
|
import { useExperimentStore } from '../../stores/experiment'
|
|
@@ -18,6 +19,7 @@ import type {
|
|
|
18
19
|
PillNavItem,
|
|
19
20
|
SidebarToolSection,
|
|
20
21
|
} from '../../types'
|
|
22
|
+
import type { JobStateInput, JobStatus } from '../../types/jobs'
|
|
21
23
|
|
|
22
24
|
vi.mock('../../composables/usePlatform', () => ({
|
|
23
25
|
usePlatform: vi.fn(() => ({
|
|
@@ -131,6 +133,13 @@ function mountIntegratedPageSelectorWorkspace(): VueWrapper {
|
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
describe('PluginWorkspaceView', () => {
|
|
136
|
+
it('should leave the top bar title unset when the spec has no title', () => {
|
|
137
|
+
// The top bar then shows the plugin identity, not a generic "Workspace".
|
|
138
|
+
const wrapper = mount(PluginWorkspaceView, { props: { spec: {} }, global: globalOptions })
|
|
139
|
+
|
|
140
|
+
expect(wrapper.findComponent(AppTopBar).props('title')).toBeUndefined()
|
|
141
|
+
})
|
|
142
|
+
|
|
134
143
|
it('should not forward the retired profile-button prop', () => {
|
|
135
144
|
const wrapper = mount(PluginWorkspaceView, {
|
|
136
145
|
global: globalOptions,
|
|
@@ -736,4 +745,57 @@ describe('PluginWorkspaceView', () => {
|
|
|
736
745
|
},
|
|
737
746
|
})
|
|
738
747
|
})
|
|
748
|
+
|
|
749
|
+
describe('jobs', () => {
|
|
750
|
+
function workspaceJob(status: JobStatus): JobStateInput {
|
|
751
|
+
return {
|
|
752
|
+
id: 'r', job_id: 'r', kind: 'analysis', name: 'Run', status,
|
|
753
|
+
progress: { current_file: 0, total_files: 0, current_filename: null, percent: 40, message: '', stage: status },
|
|
754
|
+
queue_position: 0, warnings: [], error: null, output_path: null, save_path: null, session_id: null,
|
|
755
|
+
folders: [], folder_path: null, user_id: null, owner_user_id: null, experiment_id: null,
|
|
756
|
+
extraction_result_id: null, metadata: {}, created_at: '', started_at: null, completed_at: null,
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
const jobsGlobal = { ...globalOptions, stubs: { ...globalOptions.stubs, Teleport: true } }
|
|
760
|
+
|
|
761
|
+
it('should give the top bar no jobs control when spec.jobs is unset', () => {
|
|
762
|
+
const wrapper = mount(PluginWorkspaceView, { props: { spec: { title: 'Workspace' } }, global: jobsGlobal })
|
|
763
|
+
expect([wrapper.findComponent(AppTopBar).props('job'), wrapper.findComponent(JobsStatusTray).exists()])
|
|
764
|
+
.toEqual([undefined, false])
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
it('should open the jobs tray when the top-bar chip of a running job is clicked', async () => {
|
|
768
|
+
const wrapper = mount(PluginWorkspaceView, {
|
|
769
|
+
props: { spec: { title: 'Workspace', jobs: { jobs: [workspaceJob('running')] } } },
|
|
770
|
+
global: jobsGlobal,
|
|
771
|
+
})
|
|
772
|
+
|
|
773
|
+
await wrapper.get('.mint-topbar__job').trigger('click')
|
|
774
|
+
|
|
775
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
776
|
+
expect([
|
|
777
|
+
wrapper.findComponent(AppTopBar).props('job'),
|
|
778
|
+
wrapper.find('.mint-jobs-tray__panel').exists(),
|
|
779
|
+
chip.attributes('aria-expanded'),
|
|
780
|
+
chip.attributes('aria-controls') === wrapper.get('.mint-jobs-tray__panel').attributes('id'),
|
|
781
|
+
]).toEqual([{ status: 'running', label: 'Run running', percent: 40 }, true, 'true', true])
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it('should emit job-select with the held job when the chip is clicked after the job completed', async () => {
|
|
785
|
+
const jobs = shallowRef([workspaceJob('running')])
|
|
786
|
+
const wrapper = mount(PluginWorkspaceView, {
|
|
787
|
+
props: { spec: { title: 'Workspace', jobs: { jobs } } },
|
|
788
|
+
global: jobsGlobal,
|
|
789
|
+
})
|
|
790
|
+
jobs.value = [workspaceJob('completed')]
|
|
791
|
+
await nextTick()
|
|
792
|
+
|
|
793
|
+
await wrapper.get('.mint-topbar__job').trigger('click')
|
|
794
|
+
|
|
795
|
+
expect([
|
|
796
|
+
wrapper.emitted<[JobStateInput]>('job-select')?.map(([job]) => job.job_id),
|
|
797
|
+
wrapper.find('.mint-jobs-tray__panel').exists(),
|
|
798
|
+
]).toEqual([['r'], false])
|
|
799
|
+
})
|
|
800
|
+
})
|
|
739
801
|
})
|
|
@@ -125,6 +125,26 @@ describe('SampleSelector', () => {
|
|
|
125
125
|
expect(modal.props('initialMode')).toBe('auto')
|
|
126
126
|
})
|
|
127
127
|
|
|
128
|
+
it.each([
|
|
129
|
+
[true, 'Smart group'],
|
|
130
|
+
[false, 'Group'],
|
|
131
|
+
])('should keep the grouping label as accessible name and tooltip when smart group is %s', (enableSmartGroup, label) => {
|
|
132
|
+
const wrapper = mount(SampleSelector, {
|
|
133
|
+
props: { samples: ['S1', 'S2'], modelValue: ['S1'], groups: [], enableSmartGroup },
|
|
134
|
+
global: { stubs: { AutoGroupModalInternal: true } },
|
|
135
|
+
})
|
|
136
|
+
const button = wrapper.get('.mint-sample-selector__header-actions > .mint-sample-selector__action-btn')
|
|
137
|
+
const count = wrapper.get('.mint-sample-selector__selected')
|
|
138
|
+
|
|
139
|
+
expect({
|
|
140
|
+
name: button.text(),
|
|
141
|
+
title: button.attributes('title'),
|
|
142
|
+
full: count.get('.mint-sample-selector__selected-full').text(),
|
|
143
|
+
short: count.get('.mint-sample-selector__selected-short').text(),
|
|
144
|
+
shortHidden: count.get('.mint-sample-selector__selected-short').attributes('aria-hidden'),
|
|
145
|
+
}).toEqual({ name: label, title: label, full: '1 selected', short: '1 sel.', shortHidden: 'true' })
|
|
146
|
+
})
|
|
147
|
+
|
|
128
148
|
it('opens the grouping modal in manual mode', async () => {
|
|
129
149
|
const wrapper = mount(SampleSelector, {
|
|
130
150
|
props: {
|