@morscherlab/mint-sdk 1.4.0-beta.17 → 1.4.0-beta.18
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-SnsbVow-.js → AlertBox-CzLg0UaL.js} +2 -2
- package/dist/{AlertBox-SnsbVow-.js.map → AlertBox-CzLg0UaL.js.map} +1 -1
- package/dist/{AppSidebar-CB0-RQ4m.js → AppSidebar-6oTT-gPO.js} +43 -13
- package/dist/AppSidebar-6oTT-gPO.js.map +1 -0
- package/dist/{ExperimentPopover-ChKCrklH.js → ExperimentPopover-9l4QZlvQ.js} +2 -2
- package/dist/{ExperimentPopover-ChKCrklH.js.map → ExperimentPopover-9l4QZlvQ.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-BqifjNaN.js → ExperimentSelectorModal-pR5fK3pA.js} +3 -3
- package/dist/{ExperimentSelectorModal-BqifjNaN.js.map → ExperimentSelectorModal-pR5fK3pA.js.map} +1 -1
- package/dist/{SegmentedControl-hOgBVdju.js → SegmentedControl-CJ7EAmOq.js} +2 -2
- package/dist/{SegmentedControl-hOgBVdju.js.map → SegmentedControl-CJ7EAmOq.js.map} +1 -1
- package/dist/{SettingsModal-CoZK3EDN.js → SettingsModal-Dyitgzfp.js} +6 -6
- package/dist/{SettingsModal-CoZK3EDN.js.map → SettingsModal-Dyitgzfp.js.map} +1 -1
- package/dist/components/AppTopBar.vue.d.ts +7 -0
- package/dist/components/JobsStatusTray.vue.d.ts +11 -4
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
- package/dist/composables/useJobCenter.d.ts +11 -3
- package/dist/{controlSchemaModel-CIH5Hqpw.js → controlSchemaModel-DCUOx8ll.js} +2 -2
- package/dist/{controlSchemaModel-CIH5Hqpw.js.map → controlSchemaModel-DCUOx8ll.js.map} +1 -1
- package/dist/{data-OZ94DhVC.js → data-BIiV4kKw.js} +9 -9
- package/dist/{data-OZ94DhVC.js.map → data-BIiV4kKw.js.map} +1 -1
- package/dist/data.js +7 -7
- package/dist/index.js +16 -16
- package/dist/{install-BKgb5qZw.js → install-Dg-Z6ze5.js} +13 -13
- package/dist/{install-BKgb5qZw.js.map → install-Dg-Z6ze5.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-ZDKlx1Oo.js → lab-BHVT1qN2.js} +9 -9
- package/dist/{lab-ZDKlx1Oo.js.map → lab-BHVT1qN2.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B4yl2oIu.js → records-CR_pYzIP.js} +62 -62
- package/dist/{records-B4yl2oIu.js.map → records-CR_pYzIP.js.map} +1 -1
- package/dist/{runtime-Cz1GJa_O.js → runtime-cHv1phcc.js} +379 -31
- package/dist/runtime-cHv1phcc.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +6 -6
- package/dist/{shell-kq9p4VsX.js → shell-BDuZf2zb.js} +4 -4
- package/dist/{shell-kq9p4VsX.js.map → shell-BDuZf2zb.js.map} +1 -1
- package/dist/shell.js +4 -4
- package/dist/styles.css +147 -307
- package/dist/types/components.d.ts +2 -0
- package/dist/{useExperimentSamples-BLx29CkM.js → useExperimentSamples-Dno1fFMq.js} +3 -3
- package/dist/{useExperimentSamples-BLx29CkM.js.map → useExperimentSamples-Dno1fFMq.js.map} +1 -1
- package/dist/{useListReorder-C_8RYeVP.js → useListReorder-CQ9OCScc.js} +76 -76
- package/dist/useListReorder-CQ9OCScc.js.map +1 -0
- package/dist/{useRackEditor-BhYlXKFM.js → useRackEditor-wL3kPrRT.js} +6 -6
- package/dist/{useRackEditor-BhYlXKFM.js.map → useRackEditor-wL3kPrRT.js.map} +1 -1
- package/dist/useRealtime-BW02xy4Z.js +398 -0
- package/dist/useRealtime-BW02xy4Z.js.map +1 -0
- package/dist/{useRequest-C9uwM79q.js → useRequest-CDkrXNU7.js} +2 -2
- package/dist/{useRequest-C9uwM79q.js.map → useRequest-CDkrXNU7.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +33 -2
- package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
- package/src/__tests__/components/JobsStatusTray.test.ts +137 -71
- package/src/__tests__/components/PluginWorkspaceView.test.ts +56 -1
- package/src/__tests__/composables/useJobCenter.test.ts +223 -3
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +3 -3
- package/src/components/AppTopBar.vue +14 -0
- package/src/components/GeneratedPluginView.vue +4 -9
- package/src/components/JobsStatusTray.story.vue +78 -43
- package/src/components/JobsStatusTray.vue +171 -279
- package/src/components/PluginWorkspaceView.props.ts +3 -0
- package/src/components/PluginWorkspaceView.vue +17 -0
- package/src/components/internal/WorkspaceShellInternal.vue +12 -0
- package/src/composables/useJobCenter.ts +127 -6
- package/src/runtime.ts +0 -1
- package/src/styles/components/app-top-bar.css +7 -0
- package/src/styles/components/jobs-status-tray.css +129 -296
- package/src/types/components.ts +2 -0
- package/dist/AppSidebar-CB0-RQ4m.js.map +0 -1
- package/dist/runtime-Cz1GJa_O.js.map +0 -1
- package/dist/useListReorder-C_8RYeVP.js.map +0 -1
- package/dist/useRealtime-CUMbW_ln.js +0 -699
- package/dist/useRealtime-CUMbW_ln.js.map +0 -1
|
@@ -1505,7 +1505,10 @@ describe('AppTopBar', () => {
|
|
|
1505
1505
|
})
|
|
1506
1506
|
})
|
|
1507
1507
|
describe('run slot', () => {
|
|
1508
|
-
function mountRunSlot(
|
|
1508
|
+
function mountRunSlot(
|
|
1509
|
+
job: TopBarJob | null | undefined,
|
|
1510
|
+
{ experiment, jobsDone, jobsOpen, jobsPanelId }: { experiment: boolean; jobsDone?: number; jobsOpen?: boolean; jobsPanelId?: string },
|
|
1511
|
+
) {
|
|
1509
1512
|
vi.mocked(usePlatform).mockReturnValueOnce({
|
|
1510
1513
|
integrated: computed(() => experiment),
|
|
1511
1514
|
isAdmin: computed(() => true),
|
|
@@ -1524,7 +1527,9 @@ describe('AppTopBar', () => {
|
|
|
1524
1527
|
handleDetach: vi.fn(),
|
|
1525
1528
|
} as unknown as AppExperimentState
|
|
1526
1529
|
return mount(AppTopBar, {
|
|
1527
|
-
props: job === undefined
|
|
1530
|
+
props: job === undefined
|
|
1531
|
+
? { title: 'LEAF', jobsDone, jobsOpen, jobsPanelId }
|
|
1532
|
+
: { title: 'LEAF', job, jobsDone, jobsOpen, jobsPanelId },
|
|
1528
1533
|
global: {
|
|
1529
1534
|
plugins: [createPinia()],
|
|
1530
1535
|
provide: { [APP_EXPERIMENT_KEY as symbol]: appExperiment },
|
|
@@ -1648,6 +1653,32 @@ describe('AppTopBar', () => {
|
|
|
1648
1653
|
if (bar) expect(chip.get('.mint-topbar__job-bar > span').attributes('style')).toContain('width: 40%')
|
|
1649
1654
|
})
|
|
1650
1655
|
|
|
1656
|
+
it.each([
|
|
1657
|
+
[null, undefined, null, undefined],
|
|
1658
|
+
[null, 0, null, undefined],
|
|
1659
|
+
[null, 2, '2', 'Jobs, 2 completed'],
|
|
1660
|
+
[running, 2, null, 'Extracting 40%'],
|
|
1661
|
+
])('should show the completed count only on the idle button when the job is %o and jobsDone %s', async (job, jobsDone, count, name) => {
|
|
1662
|
+
const wrapper = mountRunSlot(job, { experiment: false, jobsDone })
|
|
1663
|
+
await waitForAsyncComponents(wrapper)
|
|
1664
|
+
|
|
1665
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1666
|
+
expect([chip.find('.mint-topbar__job-count').exists() ? chip.get('.mint-topbar__job-count').text() : null, chip.attributes('aria-label')])
|
|
1667
|
+
.toEqual([count, name])
|
|
1668
|
+
})
|
|
1669
|
+
|
|
1670
|
+
it.each([
|
|
1671
|
+
[null, undefined, undefined, [undefined, undefined]],
|
|
1672
|
+
[null, true, 'jobs-panel', ['true', 'jobs-panel']],
|
|
1673
|
+
[running, false, 'jobs-panel', ['false', 'jobs-panel']],
|
|
1674
|
+
])('should expose the tray state on the job control when it is %o and open is %s', async (job, jobsOpen, jobsPanelId, expected) => {
|
|
1675
|
+
const wrapper = mountRunSlot(job, { experiment: false, jobsOpen, jobsPanelId })
|
|
1676
|
+
await waitForAsyncComponents(wrapper)
|
|
1677
|
+
|
|
1678
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1679
|
+
expect([chip.attributes('aria-expanded'), chip.attributes('aria-controls')]).toEqual(expected)
|
|
1680
|
+
})
|
|
1681
|
+
|
|
1651
1682
|
it.each([null, running])('should emit job-click when the job control is %s', async (job) => {
|
|
1652
1683
|
const wrapper = mountRunSlot(job, { experiment: false })
|
|
1653
1684
|
await waitForAsyncComponents(wrapper)
|
|
@@ -147,7 +147,7 @@ describe('GeneratedPluginView', () => {
|
|
|
147
147
|
workspace: wrapper.findComponent(PluginWorkspaceView).props(),
|
|
148
148
|
result: wrapper.findComponent(GeneratedResultRenderer).text(),
|
|
149
149
|
jobs: wrapper.findComponent(JobsStatusTray).exists(),
|
|
150
|
-
jobSource: wrapper.findComponent(
|
|
150
|
+
jobSource: wrapper.findComponent(PluginWorkspaceView).props('spec')?.jobs,
|
|
151
151
|
}).toMatchObject({
|
|
152
152
|
workspace: {
|
|
153
153
|
spec: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils'
|
|
2
2
|
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { h } from 'vue'
|
|
3
|
+
import { defineComponent, h, nextTick } from 'vue'
|
|
4
4
|
|
|
5
5
|
import JobsStatusTray from '../../components/JobsStatusTray.vue'
|
|
6
|
-
import type
|
|
6
|
+
import { useJobCenter, type PluginJobCenterSource, type UseJobCenterReturn } from '../../composables/useJobCenter'
|
|
7
7
|
import type { JobState, JobStateInput, JobStatus } from '../../types/jobs'
|
|
8
8
|
|
|
9
9
|
function job(status: JobStatus, id: string = status): JobStateInput {
|
|
@@ -44,32 +44,35 @@ async function openTray(options: {
|
|
|
44
44
|
source: PluginJobCenterSource
|
|
45
45
|
slots?: Record<string, (props: { job: JobState }) => unknown>
|
|
46
46
|
}) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
const Host = defineComponent({
|
|
48
|
+
setup() {
|
|
49
|
+
const center = useJobCenter({ source: options.source, loadOnMount: false })
|
|
50
|
+
return () => [
|
|
51
|
+
h('button', { class: 'opener', onClick: center.toggle }, 'Jobs'),
|
|
52
|
+
h(JobsStatusTray, { center, teleportTo: false }, options.slots),
|
|
53
|
+
]
|
|
54
54
|
},
|
|
55
|
-
slots,
|
|
56
55
|
})
|
|
57
|
-
|
|
56
|
+
const wrapper = mount(Host, { attachTo: document.body })
|
|
57
|
+
const opener = wrapper.get('.opener')
|
|
58
|
+
;(opener.element as HTMLElement).focus()
|
|
59
|
+
await opener.trigger('click')
|
|
58
60
|
return wrapper
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
describe('JobsStatusTray', () => {
|
|
62
|
-
it('
|
|
64
|
+
it('should show awaiting input as active without default actions when the job has no capability', async () => {
|
|
63
65
|
const wrapper = await openTray({ source: { jobs: [job('awaiting_input')] } })
|
|
64
66
|
|
|
65
67
|
expect([
|
|
66
|
-
wrapper.
|
|
67
|
-
wrapper.find('[aria-label="
|
|
68
|
-
wrapper.find('[aria-label="
|
|
69
|
-
|
|
68
|
+
wrapper.find('[data-section="active"]').exists(),
|
|
69
|
+
wrapper.find('[role="img"][aria-label="Waiting for input"]').exists(),
|
|
70
|
+
wrapper.find('[aria-label="Cancel Job awaiting_input"]').exists(),
|
|
71
|
+
wrapper.find('[aria-label="Delete Job awaiting_input"]').exists(),
|
|
72
|
+
]).toEqual([true, true, false, false])
|
|
70
73
|
})
|
|
71
74
|
|
|
72
|
-
it('
|
|
75
|
+
it('should show only actions supported by both capability and source when rows render', async () => {
|
|
73
76
|
const wrapper = await openTray({
|
|
74
77
|
source: {
|
|
75
78
|
jobs: [
|
|
@@ -82,24 +85,9 @@ describe('JobsStatusTray', () => {
|
|
|
82
85
|
})
|
|
83
86
|
|
|
84
87
|
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
|
-
])
|
|
88
|
+
wrapper.find('[data-status="awaiting_input"] [aria-label="Cancel Job input"]').exists(),
|
|
89
|
+
wrapper.find('[data-status="completed"] [aria-label="Delete Job done"]').exists(),
|
|
90
|
+
]).toEqual([true, true])
|
|
103
91
|
})
|
|
104
92
|
|
|
105
93
|
it('renders an individual delete action for an active capable job', async () => {
|
|
@@ -110,83 +98,161 @@ describe('JobsStatusTray', () => {
|
|
|
110
98
|
},
|
|
111
99
|
})
|
|
112
100
|
|
|
113
|
-
expect(wrapper.find('[aria-label="Delete
|
|
101
|
+
expect(wrapper.find('[aria-label="Delete Job awaiting_input"]').exists()).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should render a non-modal panel that does not trap Tab when open', async () => {
|
|
105
|
+
const wrapper = await openTray({ source: { jobs: [job('running')] } })
|
|
106
|
+
const panel = wrapper.get('.mint-jobs-tray__panel')
|
|
107
|
+
const tab = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true })
|
|
108
|
+
panel.element.dispatchEvent(tab)
|
|
109
|
+
|
|
110
|
+
expect([
|
|
111
|
+
panel.attributes('role'),
|
|
112
|
+
panel.attributes('aria-modal'),
|
|
113
|
+
tab.defaultPrevented,
|
|
114
|
+
]).toEqual([undefined, undefined, false])
|
|
114
115
|
})
|
|
115
116
|
|
|
116
|
-
it('
|
|
117
|
+
it('should close and return focus to the opener when Escape is pressed', async () => {
|
|
117
118
|
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()
|
|
119
|
+
expect(document.activeElement).toBe(wrapper.get('.mint-jobs-tray__panel').element)
|
|
122
120
|
|
|
123
|
-
await wrapper.get('
|
|
121
|
+
await wrapper.get('.mint-jobs-tray__panel').trigger('keydown', { key: 'Escape' })
|
|
124
122
|
|
|
125
|
-
expect(
|
|
123
|
+
expect([
|
|
124
|
+
wrapper.find('.mint-jobs-tray__panel').exists(),
|
|
125
|
+
document.activeElement,
|
|
126
|
+
]).toEqual([false, wrapper.get('.opener').element])
|
|
126
127
|
})
|
|
127
128
|
|
|
128
|
-
it('
|
|
129
|
+
it('should close when the page outside the panel is clicked', async () => {
|
|
129
130
|
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
131
|
|
|
135
|
-
await
|
|
132
|
+
await wrapper.get('.mint-jobs-tray__scrim').trigger('click')
|
|
136
133
|
|
|
137
|
-
expect(
|
|
134
|
+
expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(false)
|
|
138
135
|
})
|
|
139
136
|
|
|
140
|
-
it('
|
|
137
|
+
it('should emit select from a keyboard-accessible title button when a row is chosen', async () => {
|
|
141
138
|
const wrapper = await openTray({ source: { jobs: [job('running')] } })
|
|
142
139
|
|
|
143
|
-
await wrapper.get('.mint-jobs-
|
|
140
|
+
await wrapper.get('.mint-jobs-tray__row-title').trigger('click')
|
|
144
141
|
|
|
145
|
-
expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ job_id: 'running' })
|
|
142
|
+
expect(wrapper.findComponent(JobsStatusTray).emitted('select')?.[0]?.[0]).toMatchObject({ job_id: 'running' })
|
|
146
143
|
})
|
|
147
144
|
|
|
148
|
-
it('
|
|
145
|
+
it('should render each row slot once per row with the row job when slots are given', async () => {
|
|
149
146
|
const wrapper = await openTray({
|
|
150
147
|
source: { jobs: [job('running', 'R-1'), job('completed', 'C-1')] },
|
|
151
148
|
slots: {
|
|
152
|
-
'job-actions': ({ job: row }) =>
|
|
153
|
-
|
|
149
|
+
'job-actions': ({ job: row }) => h('button', { 'data-testid': 'open', 'data-job': row.job_id }, 'Open'),
|
|
150
|
+
'job-title': ({ job: row }) => h('span', { 'data-testid': 'title' }, `Title ${row.job_id}`),
|
|
151
|
+
'job-detail': ({ job: row }) => h('span', { 'data-testid': 'detail' }, `Detail ${row.job_id}`),
|
|
154
152
|
},
|
|
155
153
|
})
|
|
156
154
|
|
|
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'])
|
|
155
|
+
expect(['open', 'title', 'detail'].map(id => wrapper.findAll(`[data-testid="${id}"]`).map(el => el.attributes('data-job') ?? el.text())))
|
|
156
|
+
.toEqual([['R-1', 'C-1'], ['Title R-1', 'Title C-1'], ['Detail R-1', 'Detail C-1']])
|
|
164
157
|
})
|
|
165
158
|
|
|
166
159
|
it('should pin awaiting-input jobs to the top when they arrive after other active jobs', async () => {
|
|
167
160
|
const wrapper = await openTray({
|
|
168
161
|
source: { jobs: [job('running', 'R-1'), job('queued', 'Q-1'), job('awaiting_input', 'A-1')] },
|
|
169
162
|
})
|
|
170
|
-
expect(wrapper.findAll('.mint-jobs-
|
|
163
|
+
expect(wrapper.findAll('.mint-jobs-tray__row-title').map((b) => b.text()))
|
|
171
164
|
.toEqual(['Job A-1', 'Job R-1', 'Job Q-1'])
|
|
172
165
|
})
|
|
173
166
|
|
|
174
167
|
it('should tint the awaiting-input row when a job waits for input', async () => {
|
|
175
168
|
const wrapper = await openTray({ source: { jobs: [job('awaiting_input')] } })
|
|
176
|
-
expect(wrapper.get('.mint-jobs-
|
|
169
|
+
expect(wrapper.get('.mint-jobs-tray__row').classes()).toContain('mint-jobs-tray__row--awaiting-input')
|
|
177
170
|
})
|
|
178
171
|
|
|
179
|
-
it('should show a mono percent
|
|
172
|
+
it('should show a mono percent as the row value when a job is running', async () => {
|
|
180
173
|
const running = job('running')
|
|
181
174
|
running.progress = { ...running.progress, percent: 62 }
|
|
182
175
|
const wrapper = await openTray({ source: { jobs: [running] } })
|
|
183
|
-
expect(wrapper.get('.mint-jobs-
|
|
176
|
+
expect([wrapper.get('.mint-jobs-tray__value').text(), wrapper.get('[role="progressbar"]').attributes('aria-label')])
|
|
177
|
+
.toEqual(['62%', 'Job running progress'])
|
|
184
178
|
})
|
|
185
179
|
|
|
186
|
-
it('should
|
|
187
|
-
const wrapper = await openTray({
|
|
188
|
-
|
|
180
|
+
it('should show the error in error ink when a job failed', async () => {
|
|
181
|
+
const wrapper = await openTray({ source: { jobs: [{ ...job('failed'), error: 'Peaks not found' }] } })
|
|
182
|
+
const detail = wrapper.get('.mint-jobs-tray__detail')
|
|
183
|
+
expect([detail.text(), detail.classes()]).toEqual(['Peaks not found', expect.arrayContaining(['mint-jobs-tray__detail--error'])])
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it.each([
|
|
187
|
+
[[job('running', 'R-1'), job('queued', 'Q-1'), job('completed', 'C-1')], '2 active · 1 done', ['Active · 2', 'Finished · 1']],
|
|
188
|
+
[[job('failed', 'F-1'), job('cancelled', 'X-1')], '1 failed', ['Finished · 2']],
|
|
189
|
+
[[], 'No jobs', []],
|
|
190
|
+
])('should count jobs in the header and sections for %#', async (jobs, count, sections) => {
|
|
191
|
+
const wrapper = await openTray({ source: { jobs } })
|
|
192
|
+
expect({
|
|
193
|
+
count: wrapper.get('.mint-jobs-tray__head-count').text(),
|
|
194
|
+
sections: wrapper.findAll('.mint-jobs-tray__section').map(el => el.text().replace(/\s+/g, ' ')),
|
|
195
|
+
empty: wrapper.find('.mint-jobs-tray__empty').exists(),
|
|
196
|
+
}).toEqual({ count, sections, empty: jobs.length === 0 })
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('should clear finished jobs from the Finished section when the source can clear', async () => {
|
|
200
|
+
const clear = vi.fn(async () => 1)
|
|
201
|
+
const wrapper = await openTray({ source: { jobs: [job('running', 'R-1'), job('completed', 'C-1')], clear } })
|
|
202
|
+
|
|
203
|
+
await wrapper.get('[data-section="finished"] .mint-jobs-tray__section-action').trigger('click')
|
|
204
|
+
|
|
205
|
+
expect([wrapper.find('[data-section="active"] .mint-jobs-tray__section-action').exists(), clear.mock.calls.length])
|
|
206
|
+
.toEqual([false, 1])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
describe('focus and Escape', () => {
|
|
210
|
+
function mountWithChip() {
|
|
211
|
+
let center!: UseJobCenterReturn
|
|
212
|
+
const Host = defineComponent({
|
|
213
|
+
setup() {
|
|
214
|
+
center = useJobCenter({ source: { jobs: [job('running')] }, loadOnMount: false })
|
|
215
|
+
return () => [
|
|
216
|
+
h('button', { class: 'mint-topbar__job' }, 'Jobs'),
|
|
217
|
+
h('button', { class: 'outside' }, 'Elsewhere'),
|
|
218
|
+
h(JobsStatusTray, { center, teleportTo: false }),
|
|
219
|
+
]
|
|
220
|
+
},
|
|
221
|
+
})
|
|
222
|
+
return { wrapper: mount(Host, { attachTo: document.body }), center: () => center }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
it.each([
|
|
226
|
+
['the page body', () => (document.activeElement as HTMLElement | null)?.blur()],
|
|
227
|
+
['a detached element', () => {
|
|
228
|
+
const gone = document.body.appendChild(document.createElement('button'))
|
|
229
|
+
gone.focus()
|
|
230
|
+
return () => gone.remove()
|
|
231
|
+
}],
|
|
232
|
+
])('should focus the top-bar jobs control on Escape when the opener was %s', async (_case, focusOpener) => {
|
|
233
|
+
const { wrapper, center } = mountWithChip()
|
|
234
|
+
const detach = focusOpener()
|
|
235
|
+
center().open()
|
|
236
|
+
await nextTick()
|
|
237
|
+
detach?.()
|
|
238
|
+
|
|
239
|
+
await wrapper.get('.mint-jobs-tray__panel').trigger('keydown', { key: 'Escape' })
|
|
240
|
+
|
|
241
|
+
expect(document.activeElement).toBe(wrapper.get('.mint-topbar__job').element)
|
|
242
|
+
wrapper.unmount()
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it('should close on Escape when focus has left the open panel', async () => {
|
|
246
|
+
const { wrapper, center } = mountWithChip()
|
|
247
|
+
center().open()
|
|
248
|
+
await nextTick()
|
|
249
|
+
;(wrapper.get('.outside').element as HTMLElement).focus()
|
|
250
|
+
|
|
251
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
|
|
252
|
+
await nextTick()
|
|
253
|
+
|
|
254
|
+
expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(false)
|
|
255
|
+
wrapper.unmount()
|
|
189
256
|
})
|
|
190
|
-
expect(wrapper.get('.mint-jobs-tray__subtitle').text()).toBe('1 running · 1 queued · 1 waiting')
|
|
191
257
|
})
|
|
192
258
|
})
|
|
@@ -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(() => ({
|
|
@@ -743,4 +745,57 @@ describe('PluginWorkspaceView', () => {
|
|
|
743
745
|
},
|
|
744
746
|
})
|
|
745
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
|
+
})
|
|
746
801
|
})
|