@morscherlab/mint-sdk 1.4.0-beta.9 → 1.4.1
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
|
@@ -728,10 +728,17 @@ describe('SettingsModal', () => {
|
|
|
728
728
|
expect(useSettingsStore(pinia).theme).toBe('dark')
|
|
729
729
|
})
|
|
730
730
|
|
|
731
|
-
it('should
|
|
731
|
+
it('should preview each palette with its own tokens when appearance is shown', () => {
|
|
732
732
|
const wrapper = mountSettingsModal({ showAppearance: true })
|
|
733
733
|
|
|
734
|
-
|
|
734
|
+
const strips = wrapper.findAll('.mint-settings-modal__swatches')
|
|
735
|
+
expect(strips.map((strip) => strip.attributes('data-palette'))).toEqual(Object.keys(colorPalettes))
|
|
736
|
+
expect(strips[0].findAll('.mint-settings-modal__swatch').map((swatch) => swatch.attributes('style'))).toEqual([
|
|
737
|
+
'background-color: var(--color-primary);',
|
|
738
|
+
'background-color: var(--color-primary-hover);',
|
|
739
|
+
'background-color: var(--color-primary-soft);',
|
|
740
|
+
'background-color: var(--color-primary-border);',
|
|
741
|
+
])
|
|
735
742
|
})
|
|
736
743
|
|
|
737
744
|
it('should resize the density preview when the density changes', async () => {
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
1
3
|
import { describe, expect, it } from 'vitest'
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
detectDelimiter,
|
|
5
7
|
parseDelimitedText,
|
|
8
|
+
readTableFileAsCsv,
|
|
6
9
|
useFileImport,
|
|
7
10
|
validateImportFile,
|
|
8
11
|
} from '../../composables/useFileImport'
|
|
9
12
|
|
|
13
|
+
const SAMPLES_CSV = [
|
|
14
|
+
'Sample Name,Treatment,Inj Vol,Date',
|
|
15
|
+
'S1,Vehicle,5,2026-10-01',
|
|
16
|
+
'S2,"Drug ""A"", 10 uM",2.5,2026-10-02',
|
|
17
|
+
].join('\n')
|
|
18
|
+
|
|
10
19
|
describe('file import helpers', () => {
|
|
11
20
|
it('parses quoted CSV fields', () => {
|
|
12
21
|
const parsed = parseDelimitedText('name,notes\nSample A,"hello, world"\n')
|
|
@@ -41,4 +50,31 @@ describe('file import helpers', () => {
|
|
|
41
50
|
expect(result.rows).toEqual([{ name: 'A' }])
|
|
42
51
|
expect(importer.loading.value).toBe(false)
|
|
43
52
|
})
|
|
53
|
+
|
|
54
|
+
it.each([
|
|
55
|
+
['samples.csv', SAMPLES_CSV],
|
|
56
|
+
['samples.tsv', 'Sample Name\tTreatment\tInj Vol\tDate\nS1\tVehicle\t5\t2026-10-01\nS2\t"Drug ""A"", 10 uM"\t2.5\t2026-10-02'],
|
|
57
|
+
['samples.xlsx', readFileSync(resolve(__dirname, '../fixtures/table-import/samples.xlsx'))],
|
|
58
|
+
])('should read the first table as CSV text when the upload is %s', async (name, content) => {
|
|
59
|
+
const file = new File([content], name)
|
|
60
|
+
|
|
61
|
+
expect(await readTableFileAsCsv(file)).toBe(SAMPLES_CSV)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should keep a quote inside a field as text when the quote does not open the field', () => {
|
|
65
|
+
expect(parseDelimitedText('S1\t5" vial\nS2\tok', { hasHeaderRow: false }).rawRows)
|
|
66
|
+
.toEqual([['S1', '5" vial'], ['S2', 'ok']])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should keep the stored cell text when the upload is .xlsx', async () => {
|
|
70
|
+
const file = new File([readFileSync(resolve(__dirname, '../fixtures/table-import/cells.xlsx'))], 'cells.xlsx')
|
|
71
|
+
|
|
72
|
+
expect(await readTableFileAsCsv(file)).toBe('2026-10-01T13:45:00,08:30:00,TRUE\n S1 ,9007199254740993,0.1')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should reject a file that is not a workbook when the name ends in .xlsx', async () => {
|
|
76
|
+
const file = new File(['Sample Name\nS1\n'], 'renamed.xlsx')
|
|
77
|
+
|
|
78
|
+
await expect(readTableFileAsCsv(file)).rejects.toThrow('not a valid .xlsx workbook')
|
|
79
|
+
})
|
|
44
80
|
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { defineComponent, nextTick, ref } from 'vue'
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { computePosition } from '@floating-ui/dom'
|
|
5
|
+
import { useFloatingPanel } from '../../composables/useFloatingPanel'
|
|
6
|
+
|
|
7
|
+
const cleanup = vi.fn()
|
|
8
|
+
vi.mock('@floating-ui/dom', async (importOriginal) => ({
|
|
9
|
+
...(await importOriginal<typeof import('@floating-ui/dom')>()),
|
|
10
|
+
autoUpdate: vi.fn((_reference: unknown, _floating: unknown, update: () => void) => {
|
|
11
|
+
update()
|
|
12
|
+
return cleanup
|
|
13
|
+
}),
|
|
14
|
+
computePosition: vi.fn(async () => ({ x: 10, y: 20, placement: 'bottom-start', strategy: 'fixed', middlewareData: {} })),
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
/** A trigger and the panel placed next to it. */
|
|
18
|
+
const Panel = defineComponent({
|
|
19
|
+
setup() {
|
|
20
|
+
const trigger = ref<HTMLElement | null>(null)
|
|
21
|
+
const panel = ref<HTMLElement | null>(null)
|
|
22
|
+
const isOpen = ref(true)
|
|
23
|
+
const { style } = useFloatingPanel(trigger, panel, isOpen)
|
|
24
|
+
return { trigger, panel, isOpen, style }
|
|
25
|
+
},
|
|
26
|
+
template: '<div><button ref="trigger">Open</button><div v-if="isOpen" ref="panel" class="panel" :style="style" /></div>',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
async function settle() {
|
|
30
|
+
for (let i = 0; i < 3; i++) await nextTick()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('useFloatingPanel', () => {
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
cleanup.mockClear()
|
|
36
|
+
document.body.innerHTML = ''
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it.each([
|
|
40
|
+
['closes', (wrapper: ReturnType<typeof mount>) => { (wrapper.vm as unknown as { isOpen: boolean }).isOpen = false }],
|
|
41
|
+
['unmounts', (wrapper: ReturnType<typeof mount>) => wrapper.unmount()],
|
|
42
|
+
])('should stop autoUpdate when the panel %s', async (_, end) => {
|
|
43
|
+
const wrapper = mount(Panel, { attachTo: document.body })
|
|
44
|
+
await settle()
|
|
45
|
+
expect(cleanup).not.toHaveBeenCalled()
|
|
46
|
+
|
|
47
|
+
end(wrapper)
|
|
48
|
+
await settle()
|
|
49
|
+
|
|
50
|
+
expect(cleanup).toHaveBeenCalledTimes(1)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it.each([
|
|
54
|
+
[false, ''],
|
|
55
|
+
[true, 'hidden'],
|
|
56
|
+
])('should set the panel visibility to %j when referenceHidden is %s', async (referenceHidden, visibility) => {
|
|
57
|
+
vi.mocked(computePosition).mockResolvedValueOnce({
|
|
58
|
+
x: 10, y: 20, placement: 'bottom-start', strategy: 'fixed', middlewareData: { hide: { referenceHidden } },
|
|
59
|
+
})
|
|
60
|
+
mount(Panel, { attachTo: document.body })
|
|
61
|
+
await settle()
|
|
62
|
+
|
|
63
|
+
const panel = document.querySelector<HTMLElement>('.panel')!
|
|
64
|
+
expect(panel.style.position).toBe('fixed')
|
|
65
|
+
expect(panel.style.top).toBe('20px')
|
|
66
|
+
expect(panel.style.visibility).toBe(visibility)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { mount } from '@vue/test-utils'
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
2
|
import { defineComponent, nextTick, shallowRef } from 'vue'
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
combinePluginJobSources,
|
|
7
7
|
useJobCenter,
|
|
8
8
|
type UseJobCenterOptions,
|
|
9
9
|
} from '../../composables/useJobCenter'
|
|
10
|
-
import type { JobStateInput, JobStatus } from '../../types/jobs'
|
|
10
|
+
import type { JobStateInput, JobStatus, TopBarJob } from '../../types/jobs'
|
|
11
11
|
|
|
12
12
|
function job(status: JobStatus, id: string = status): JobStateInput {
|
|
13
13
|
return {
|
|
@@ -281,3 +281,262 @@ describe('useJobCenter', () => {
|
|
|
281
281
|
expect(tray.deletableJobs.value).toEqual([])
|
|
282
282
|
})
|
|
283
283
|
})
|
|
284
|
+
|
|
285
|
+
describe('useJobCenter top-bar chip', () => {
|
|
286
|
+
const platformWindow = window as unknown as { __MINT_PLATFORM__?: unknown }
|
|
287
|
+
function integrateAs(userId: number, pluginId = 'peak-qc') {
|
|
288
|
+
platformWindow.__MINT_PLATFORM__ = { user: { id: userId }, plugin: { id: pluginId } }
|
|
289
|
+
}
|
|
290
|
+
const owned = (status: JobStatus, id: string, owner: string | null, user = owner): JobStateInput => ({
|
|
291
|
+
...job(status, id), owner_user_id: owner, user_id: user,
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
beforeEach(() => localStorage.clear())
|
|
295
|
+
afterEach(() => {
|
|
296
|
+
delete platformWindow.__MINT_PLATFORM__
|
|
297
|
+
vi.useRealTimers()
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it.each<[string, JobStateInput[], TopBarJob | null]>([
|
|
301
|
+
['an awaiting-input job first', [job('running', 'r'), job('awaiting_input', 'a')],
|
|
302
|
+
{ status: 'awaiting_input', label: 'a waiting for input', percent: null }],
|
|
303
|
+
['the first active job', [job('queued', 'q'), job('running', 'r')],
|
|
304
|
+
{ status: 'queued', label: 'q queued', percent: null }],
|
|
305
|
+
['the progress message and percent of a running job',
|
|
306
|
+
[{ ...job('running', 'r'), progress: { ...job('running', 'r').progress, message: 'Integrating' } }],
|
|
307
|
+
{ status: 'running', label: 'Integrating', percent: 42 }],
|
|
308
|
+
['nothing without active, held or failed jobs', [job('completed', 'c'), job('cancelled', 'x')], null],
|
|
309
|
+
])('should show %s when the viewer runs jobs', (_case, jobs, expected) => {
|
|
310
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
311
|
+
expect(tray.topBarJob.value).toEqual(expected)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should show only the viewer own jobs when the page is integrated', () => {
|
|
315
|
+
integrateAs(1)
|
|
316
|
+
const { tray } = mountComposable({
|
|
317
|
+
source: { jobs: [owned('running', 'theirs', '2'), owned('queued', 'mine', null, '1')] },
|
|
318
|
+
loadOnMount: false,
|
|
319
|
+
})
|
|
320
|
+
expect(tray.topBarJob.value).toMatchObject({ status: 'queued', label: 'mine queued' })
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it.each([
|
|
324
|
+
['auth is off', false, 'running'],
|
|
325
|
+
['auth is required', true, undefined],
|
|
326
|
+
])('should treat jobs of an integrated page without a signed-in user as own only when %s', (_case, authRequired, status) => {
|
|
327
|
+
platformWindow.__MINT_PLATFORM__ = { user: null, plugin: { id: 'peak-qc' }, authRequired }
|
|
328
|
+
const { tray } = mountComposable({ source: { jobs: [owned('running', 'theirs', '2')] }, loadOnMount: false })
|
|
329
|
+
expect(tray.topBarJob.value?.status).toBe(status)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('should hold a completed job when several jobs leave the active list in one update', async () => {
|
|
333
|
+
const jobs = shallowRef([job('running', 'a'), job('running', 'b')])
|
|
334
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
335
|
+
|
|
336
|
+
jobs.value = [job('failed', 'a'), job('completed', 'b')]
|
|
337
|
+
await nextTick()
|
|
338
|
+
|
|
339
|
+
expect(tray.topBarJob.value).toEqual({ status: 'completed', label: 'b completed' })
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('should close the open tray instead of selecting the held job when the chip is clicked', async () => {
|
|
343
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
344
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
345
|
+
const onSelect = vi.fn()
|
|
346
|
+
jobs.value = [job('completed', 'r')]
|
|
347
|
+
await nextTick()
|
|
348
|
+
tray.open()
|
|
349
|
+
|
|
350
|
+
tray.handleTopBarClick(onSelect)
|
|
351
|
+
|
|
352
|
+
expect([tray.isOpen.value, onSelect.mock.calls.length]).toEqual([false, 0])
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
it('should drop the held job when it leaves the job list', async () => {
|
|
356
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
357
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
358
|
+
const onSelect = vi.fn()
|
|
359
|
+
jobs.value = [job('completed', 'r')]
|
|
360
|
+
await nextTick()
|
|
361
|
+
jobs.value = []
|
|
362
|
+
await nextTick()
|
|
363
|
+
|
|
364
|
+
tray.handleTopBarClick(onSelect)
|
|
365
|
+
|
|
366
|
+
expect([tray.topBarJob.value, onSelect.mock.calls.length, tray.isOpen.value]).toEqual([null, 0, true])
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('should mark a failure seen when the job fails while the tray is open', async () => {
|
|
370
|
+
localStorage.setItem('mint.jobs.standalone.seen-failed', '[]')
|
|
371
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
372
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
373
|
+
tray.open()
|
|
374
|
+
await nextTick()
|
|
375
|
+
|
|
376
|
+
jobs.value = [job('failed', 'r')]
|
|
377
|
+
await nextTick()
|
|
378
|
+
tray.close()
|
|
379
|
+
await nextTick()
|
|
380
|
+
|
|
381
|
+
expect(tray.topBarJob.value).toBeNull()
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it.each([
|
|
385
|
+
['an empty list', [] as JobStateInput[], { status: 'failed', label: '1 failed' }],
|
|
386
|
+
['an old failure', [job('failed', 'old')], { status: 'failed', label: '1 failed' }],
|
|
387
|
+
])('should take the baseline when the initial load returns %s', async (_case, loaded, later) => {
|
|
388
|
+
const jobs = shallowRef<JobStateInput[]>([])
|
|
389
|
+
const { tray } = mountComposable({ source: { jobs, refresh: async () => { jobs.value = loaded } } })
|
|
390
|
+
await flushPromises()
|
|
391
|
+
const baseline = tray.topBarJob.value
|
|
392
|
+
|
|
393
|
+
jobs.value = [...loaded, job('failed', 'new')]
|
|
394
|
+
await nextTick()
|
|
395
|
+
|
|
396
|
+
expect([baseline, tray.topBarJob.value]).toEqual([null, later])
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
it('should not hold a job when the source is replaced by one where it already completed', async () => {
|
|
400
|
+
const source = shallowRef({ jobs: [job('running', 'x')] })
|
|
401
|
+
const { tray } = mountComposable({ source, loadOnMount: false })
|
|
402
|
+
|
|
403
|
+
source.value = { jobs: [job('completed', 'x')] }
|
|
404
|
+
await nextTick()
|
|
405
|
+
|
|
406
|
+
expect(tray.topBarJob.value).toBeNull()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('should alert only failures after the first job list when nothing was stored', async () => {
|
|
410
|
+
const jobs = shallowRef<JobStateInput[]>([])
|
|
411
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
412
|
+
|
|
413
|
+
jobs.value = [job('failed', 'old')]
|
|
414
|
+
await nextTick()
|
|
415
|
+
const baseline = tray.topBarJob.value
|
|
416
|
+
jobs.value = [job('failed', 'old'), job('failed', 'new')]
|
|
417
|
+
await nextTick()
|
|
418
|
+
|
|
419
|
+
expect({
|
|
420
|
+
baseline,
|
|
421
|
+
later: tray.topBarJob.value,
|
|
422
|
+
stored: JSON.parse(localStorage.getItem('mint.jobs.standalone.seen-failed') ?? 'null'),
|
|
423
|
+
}).toEqual({ baseline: null, later: { status: 'failed', label: '1 failed' }, stored: ['old'] })
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it.each<[string, TopBarJob | null, TopBarJob]>([
|
|
427
|
+
['use the override', { status: 'completed', label: 'Saved to the experiment' },
|
|
428
|
+
{ status: 'completed', label: 'Saved to the experiment' }],
|
|
429
|
+
['keep the default', null, { status: 'completed', label: 'r completed' }],
|
|
430
|
+
])('should %s for the held job when the topBarJob option returns that value', async (_case, override, expected) => {
|
|
431
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
432
|
+
const topBarJob = vi.fn((shown: { status: JobStatus }) => (shown.status === 'completed' ? override : null))
|
|
433
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false, topBarJob })
|
|
434
|
+
|
|
435
|
+
jobs.value = [job('completed', 'r')]
|
|
436
|
+
await nextTick()
|
|
437
|
+
|
|
438
|
+
expect(tray.topBarJob.value).toEqual(expected)
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it('should hold a just-completed job for 6 s and select it on click when nothing else runs', async () => {
|
|
442
|
+
vi.useFakeTimers()
|
|
443
|
+
const jobs = shallowRef([job('running', 'r')])
|
|
444
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
445
|
+
const onSelect = vi.fn()
|
|
446
|
+
|
|
447
|
+
jobs.value = [{ ...job('completed', 'r') }]
|
|
448
|
+
await nextTick()
|
|
449
|
+
const held = tray.topBarJob.value
|
|
450
|
+
tray.handleTopBarClick(onSelect)
|
|
451
|
+
vi.advanceTimersByTime(5999)
|
|
452
|
+
const stillHeld = tray.topBarJob.value
|
|
453
|
+
vi.advanceTimersByTime(1)
|
|
454
|
+
tray.handleTopBarClick(onSelect)
|
|
455
|
+
|
|
456
|
+
expect({
|
|
457
|
+
held,
|
|
458
|
+
stillHeld: stillHeld?.status,
|
|
459
|
+
after: tray.topBarJob.value,
|
|
460
|
+
selected: onSelect.mock.calls.map(([selected]) => selected.job_id),
|
|
461
|
+
open: tray.isOpen.value,
|
|
462
|
+
}).toEqual({
|
|
463
|
+
held: { status: 'completed', label: 'r completed' },
|
|
464
|
+
stillHeld: 'completed',
|
|
465
|
+
after: null,
|
|
466
|
+
selected: ['r'],
|
|
467
|
+
open: true,
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it.each([
|
|
472
|
+
['standalone', () => undefined, 'mint.jobs.standalone.seen-failed'],
|
|
473
|
+
['integrated', () => integrateAs(1), 'mint.jobs.peak-qc.seen-failed'],
|
|
474
|
+
])('should count unseen failures until the tray opens when %s', async (_mode, setup, key) => {
|
|
475
|
+
setup()
|
|
476
|
+
localStorage.setItem(key, '[]')
|
|
477
|
+
const jobs = [owned('failed', 'f1', '1'), owned('failed', 'f2', '1'), owned('failed', 'other', '2')]
|
|
478
|
+
const { tray } = mountComposable({ source: { jobs }, loadOnMount: false })
|
|
479
|
+
const before = tray.topBarJob.value
|
|
480
|
+
|
|
481
|
+
tray.open()
|
|
482
|
+
await nextTick()
|
|
483
|
+
const reopened = mountComposable({ source: { jobs }, loadOnMount: false }).tray
|
|
484
|
+
|
|
485
|
+
expect({
|
|
486
|
+
before,
|
|
487
|
+
after: tray.topBarJob.value,
|
|
488
|
+
reopened: reopened.topBarJob.value,
|
|
489
|
+
stored: JSON.parse(localStorage.getItem(key) ?? 'null'),
|
|
490
|
+
}).toEqual({
|
|
491
|
+
before: { status: 'failed', label: _mode === 'standalone' ? '3 failed' : '2 failed' },
|
|
492
|
+
after: null,
|
|
493
|
+
reopened: null,
|
|
494
|
+
stored: _mode === 'standalone' ? ['f1', 'f2', 'other'] : ['f1', 'f2'],
|
|
495
|
+
})
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
it.each<[string, TopBarJob | null, TopBarJob]>([
|
|
499
|
+
['replace the default mapping', { status: 'running', label: 'Decoding in the browser', percent: 10 },
|
|
500
|
+
{ status: 'running', label: 'Decoding in the browser', percent: 10 }],
|
|
501
|
+
['keep the default mapping', null, { status: 'awaiting_input', label: 'a waiting for input', percent: null }],
|
|
502
|
+
])('should %s when the topBarJob option returns that value', (_case, override, expected) => {
|
|
503
|
+
const { tray } = mountComposable({
|
|
504
|
+
source: { jobs: [job('awaiting_input', 'a')] },
|
|
505
|
+
loadOnMount: false,
|
|
506
|
+
topBarJob: () => override,
|
|
507
|
+
})
|
|
508
|
+
expect(tray.topBarJob.value).toEqual(expected)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
it('should count only completed jobs when the list holds finished and active jobs', () => {
|
|
512
|
+
const { tray } = mountComposable({
|
|
513
|
+
source: { jobs: [job('completed', 'c1'), job('completed', 'c2'), job('failed', 'f'), job('running', 'r')] },
|
|
514
|
+
loadOnMount: false,
|
|
515
|
+
})
|
|
516
|
+
expect(tray.completedCount.value).toBe(2)
|
|
517
|
+
})
|
|
518
|
+
})
|
|
519
|
+
|
|
520
|
+
describe('useJobCenter row view', () => {
|
|
521
|
+
const awaiting = job('awaiting_input', 'a')
|
|
522
|
+
const running = job('running', 'r')
|
|
523
|
+
|
|
524
|
+
it.each<[string, JobStateInput, UseJobCenterOptions['rowJob'], ReturnType<ReturnType<typeof useJobCenter>['rowView']>]>([
|
|
525
|
+
['absent', running, undefined, { status: 'running', percent: 42, message: 'running' }],
|
|
526
|
+
['null', awaiting, () => null,
|
|
527
|
+
{ status: 'awaiting_input', percent: null, message: 'Waiting for external input' }],
|
|
528
|
+
['a status and percent', awaiting, () => ({ status: 'running', percent: 40 }),
|
|
529
|
+
{ status: 'running', percent: 40, message: 'Waiting for external input' }],
|
|
530
|
+
['a message only', running, () => ({ message: 'Decoding 3/8' }),
|
|
531
|
+
{ status: 'running', percent: 42, message: 'Decoding 3/8' }],
|
|
532
|
+
['undefined and null fields', running, () => ({ status: null, percent: undefined, message: null }),
|
|
533
|
+
{ status: 'running', percent: 42, message: 'running' }],
|
|
534
|
+
['a status only', running, () => ({ status: 'awaiting_input' }),
|
|
535
|
+
{ status: 'awaiting_input', percent: 42, message: 'running' }],
|
|
536
|
+
['a null percent', running, () => ({ percent: null }),
|
|
537
|
+
{ status: 'running', percent: null, message: 'running' }],
|
|
538
|
+
])('should resolve the row view from the job when the row override is %s', (_case, input, rowJob, expected) => {
|
|
539
|
+
const { tray } = mountComposable({ source: { jobs: [input] }, loadOnMount: false, rowJob })
|
|
540
|
+
expect(tray.rowView(tray.jobs.value[0]!)).toEqual(expected)
|
|
541
|
+
})
|
|
542
|
+
})
|