@morscherlab/mint-sdk 1.4.0-beta.12 → 1.4.0-beta.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{AlertBox-BdoZqANv.js → AlertBox-SnsbVow-.js} +3 -3
- package/dist/{AlertBox-BdoZqANv.js.map → AlertBox-SnsbVow-.js.map} +1 -1
- package/dist/{AppSidebar-DGK5i-hJ.js → AppSidebar-Bi8g9mRt.js} +237 -70
- package/dist/{AppSidebar-DGK5i-hJ.js.map → AppSidebar-Bi8g9mRt.js.map} +1 -1
- package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CSls2Nuv.js} +2 -2
- package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CSls2Nuv.js.map} +1 -1
- package/dist/{ExperimentPopover-CbD7BR_s.js → ExperimentPopover-ChKCrklH.js} +5 -5
- package/dist/{ExperimentPopover-CbD7BR_s.js.map → ExperimentPopover-ChKCrklH.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-BoofZZxF.js → ExperimentSelectorModal-BqifjNaN.js} +113 -97
- package/dist/ExperimentSelectorModal-BqifjNaN.js.map +1 -0
- package/dist/{SegmentedControl-BkwYOKpG.js → SegmentedControl-hOgBVdju.js} +2 -2
- package/dist/{SegmentedControl-BkwYOKpG.js.map → SegmentedControl-hOgBVdju.js.map} +1 -1
- package/dist/{SettingsModal-BXsjIVnm.js → SettingsModal-jra_2u0o.js} +7 -7
- package/dist/{SettingsModal-BXsjIVnm.js.map → SettingsModal-jra_2u0o.js.map} +1 -1
- package/dist/__stories__/experiment-helpers.d.ts +10 -0
- package/dist/components/AppTopBar.vue.d.ts +13 -5
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
- package/dist/{controlSchemaModel-BF5g4wRC.js → controlSchemaModel-CIH5Hqpw.js} +2 -2
- package/dist/{controlSchemaModel-BF5g4wRC.js.map → controlSchemaModel-CIH5Hqpw.js.map} +1 -1
- package/dist/{data-ihsBd9bs.js → data-OZ94DhVC.js} +11 -11
- package/dist/{data-ihsBd9bs.js.map → data-OZ94DhVC.js.map} +1 -1
- package/dist/data.js +8 -8
- package/dist/{experimentStatus-Cqp6qNrR.js → experimentStatus-BFrKpHAH.js} +2 -2
- package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-BFrKpHAH.js.map} +1 -1
- package/dist/index.js +18 -18
- package/dist/{install-CFgfRO64.js → install-Bx_6KwJA.js} +15 -15
- package/dist/{install-CFgfRO64.js.map → install-Bx_6KwJA.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-B-pLwc5U.js → lab-ZDKlx1Oo.js} +10 -10
- package/dist/{lab-B-pLwc5U.js.map → lab-ZDKlx1Oo.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-DnK_1Q_S.js → records-B4yl2oIu.js} +2 -2
- package/dist/{records-DnK_1Q_S.js.map → records-B4yl2oIu.js.map} +1 -1
- package/dist/{runtime-pKfF_iql.js → runtime-BJvGms6h.js} +9 -9
- package/dist/{runtime-pKfF_iql.js.map → runtime-BJvGms6h.js.map} +1 -1
- package/dist/runtime.js +6 -6
- package/dist/{shell-BIH8Rc21.js → shell-Czyrzk7l.js} +5 -5
- package/dist/{shell-BIH8Rc21.js.map → shell-Czyrzk7l.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +296 -45
- package/dist/types/index.d.ts +1 -1
- package/dist/types/jobs.d.ts +9 -0
- package/dist/{useDropdownState-8yWMu-7_.js → useDropdownState-Bb2N4m40.js} +15 -3
- package/dist/useDropdownState-Bb2N4m40.js.map +1 -0
- package/dist/{useExperimentSamples-BGN1dmcR.js → useExperimentSamples-BLx29CkM.js} +3 -3
- package/dist/{useExperimentSamples-BGN1dmcR.js.map → useExperimentSamples-BLx29CkM.js.map} +1 -1
- package/dist/{useListReorder-DPynGrCj.js → useListReorder-C_8RYeVP.js} +3 -3
- package/dist/{useListReorder-DPynGrCj.js.map → useListReorder-C_8RYeVP.js.map} +1 -1
- package/dist/{useRackEditor-B2FrQA0p.js → useRackEditor-BhYlXKFM.js} +8 -8
- package/dist/{useRackEditor-B2FrQA0p.js.map → useRackEditor-BhYlXKFM.js.map} +1 -1
- package/dist/{useRealtime-yWjTTnc9.js → useRealtime-CUMbW_ln.js} +2 -2
- package/dist/{useRealtime-yWjTTnc9.js.map → useRealtime-CUMbW_ln.js.map} +1 -1
- package/dist/{useRequest-Dqw12k2s.js → useRequest-C9uwM79q.js} +3 -3
- package/dist/{useRequest-Dqw12k2s.js.map → useRequest-C9uwM79q.js.map} +1 -1
- package/package.json +1 -1
- package/src/__stories__/experiment-helpers.ts +3 -3
- package/src/__tests__/components/AppTopBar.test.ts +175 -1
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/components/AppTopBar.story.vue +59 -4
- package/src/components/AppTopBar.vue +107 -8
- package/src/components/BaseButton.vue +16 -2
- package/src/components/ExperimentSelectorModal.story.vue +38 -0
- package/src/components/ExperimentSelectorModal.vue +20 -1
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/styles/components/app-page-selector.css +120 -45
- package/src/styles/components/app-top-bar.css +155 -0
- package/src/styles/components/button.css +8 -0
- package/src/styles/components/experiment-selector-modal.css +13 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/dist/ExperimentSelectorModal-BoofZZxF.js.map +0 -1
- package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
|
@@ -12,6 +12,7 @@ import { defineControlModel, defineControls } from '../../composables/useControl
|
|
|
12
12
|
import ThemeToggle from '../../components/ThemeToggle.vue'
|
|
13
13
|
import SettingsModal from '../../components/SettingsModal.vue'
|
|
14
14
|
import type { TopBarSettingsConfig } from '../../types/components'
|
|
15
|
+
import type { TopBarJob } from '../../types/jobs'
|
|
15
16
|
|
|
16
17
|
// Mock the usePlatform composable
|
|
17
18
|
vi.mock('../../composables/usePlatform', () => ({
|
|
@@ -908,7 +909,7 @@ describe('AppTopBar', () => {
|
|
|
908
909
|
const imageIcons = wrapper.findAll('.mint-plugin-icon__img')
|
|
909
910
|
expect(imageIcons).toHaveLength(1)
|
|
910
911
|
expect(imageIcons[0].attributes('src')).toBe(pngIcon)
|
|
911
|
-
expect(wrapper.
|
|
912
|
+
expect(wrapper.get('.mint-page-selector__item-hint').text()).toBe('Overview')
|
|
912
913
|
})
|
|
913
914
|
})
|
|
914
915
|
|
|
@@ -1237,6 +1238,26 @@ describe('AppTopBar', () => {
|
|
|
1237
1238
|
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1238
1239
|
})
|
|
1239
1240
|
|
|
1241
|
+
it.each([
|
|
1242
|
+
{ case: 'integrated with display_name', integrated: true, plugin: { ...integratedPlugin, display_name: 'LC-MS Peak Review' }, props: { title: 'Ignored' }, seed: false, expected: 'LC-MS Peak Review' },
|
|
1243
|
+
{ case: 'integrated without display_name', integrated: true, plugin: integratedPlugin, props: {}, seed: false, expected: 'LC-MS Analysis' },
|
|
1244
|
+
{ case: 'standalone with a title', integrated: false, plugin: undefined, props: { title: 'Planner Dev' }, seed: true, expected: 'Planner Dev' },
|
|
1245
|
+
{ case: 'standalone seeded', integrated: false, plugin: undefined, props: {}, seed: true, expected: 'MS Planner' },
|
|
1246
|
+
{ case: 'standalone without a name', integrated: false, plugin: undefined, props: {}, seed: false, expected: null },
|
|
1247
|
+
])('should put the plugin name in the page selector crumb when $case', ({ integrated, plugin, props, seed, expected }) => {
|
|
1248
|
+
mockPlatformCtx({ isIntegrated: integrated, plugin })
|
|
1249
|
+
if (seed) providePluginIdentity({ plugin: { name: 'ms-planner', displayName: 'MS Planner' } })
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
const wrapper = createWrapper({ ...props, pageSelector: pages, currentPageSelectorId: 'peaks' })
|
|
1253
|
+
|
|
1254
|
+
const name = wrapper.find('.mint-page-selector__plugin')
|
|
1255
|
+
expect(name.exists() ? name.text() : null).toBe(expected)
|
|
1256
|
+
} finally {
|
|
1257
|
+
providePluginIdentity({ plugin: { name: '' } })
|
|
1258
|
+
}
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1240
1261
|
it('should hide the plugin name when a page selector is shown', () => {
|
|
1241
1262
|
mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
|
|
1242
1263
|
|
|
@@ -1483,4 +1504,157 @@ describe('AppTopBar', () => {
|
|
|
1483
1504
|
expect(removeListener).toHaveBeenCalledWith('scroll', expect.any(Function), expect.objectContaining({ capture: true }))
|
|
1484
1505
|
})
|
|
1485
1506
|
})
|
|
1507
|
+
describe('run slot', () => {
|
|
1508
|
+
function mountRunSlot(job: TopBarJob | null | undefined, { experiment }: { experiment: boolean }) {
|
|
1509
|
+
vi.mocked(usePlatform).mockReturnValueOnce({
|
|
1510
|
+
integrated: computed(() => experiment),
|
|
1511
|
+
isAdmin: computed(() => true),
|
|
1512
|
+
can: () => true,
|
|
1513
|
+
plugin: computed(() => undefined),
|
|
1514
|
+
user: computed(() => undefined),
|
|
1515
|
+
theme: computed(() => 'system' as const),
|
|
1516
|
+
features: computed(() => undefined),
|
|
1517
|
+
})
|
|
1518
|
+
const appExperiment = {
|
|
1519
|
+
popover: computed(() => ({ experimentName: 'Serine tracing', experimentCode: 'EXP-0142', showSave: true })),
|
|
1520
|
+
selectorModal: computed(() => ({ open: false })),
|
|
1521
|
+
openModal: vi.fn(),
|
|
1522
|
+
closeModal: vi.fn(),
|
|
1523
|
+
handleSave: vi.fn(),
|
|
1524
|
+
handleDetach: vi.fn(),
|
|
1525
|
+
} as unknown as AppExperimentState
|
|
1526
|
+
return mount(AppTopBar, {
|
|
1527
|
+
props: job === undefined ? { title: 'LEAF' } : { title: 'LEAF', job },
|
|
1528
|
+
global: {
|
|
1529
|
+
plugins: [createPinia()],
|
|
1530
|
+
provide: { [APP_EXPERIMENT_KEY as symbol]: appExperiment },
|
|
1531
|
+
stubs: { 'router-link': { template: '<a><slot /></a>' }, BaseModal: { template: '<div><slot /></div>' }, transition: false },
|
|
1532
|
+
},
|
|
1533
|
+
})
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const running: TopBarJob = { status: 'running', label: 'Extracting', percent: 40 }
|
|
1537
|
+
|
|
1538
|
+
it.each([
|
|
1539
|
+
['no job prop, experiment', undefined, true, { popover: true, chip: false, idle: false, joined: false }],
|
|
1540
|
+
['idle, experiment', null, true, { popover: true, chip: true, idle: true, joined: false }],
|
|
1541
|
+
['running, experiment', running, true, { popover: true, chip: true, idle: false, joined: true }],
|
|
1542
|
+
['idle, standalone', null, false, { popover: false, chip: true, idle: true, joined: false }],
|
|
1543
|
+
['running, standalone', running, false, { popover: false, chip: true, idle: false, joined: true }],
|
|
1544
|
+
] as const)('should render the run slot for %s', async (_case, job, experiment, expected) => {
|
|
1545
|
+
const wrapper = mountRunSlot(job, { experiment })
|
|
1546
|
+
await waitForAsyncComponents(wrapper)
|
|
1547
|
+
|
|
1548
|
+
const chip = wrapper.find('.mint-topbar__job')
|
|
1549
|
+
expect({
|
|
1550
|
+
popover: wrapper.findComponent(ExperimentPopover).exists(),
|
|
1551
|
+
chip: chip.exists(),
|
|
1552
|
+
idle: chip.exists() && chip.classes('mint-topbar__job--idle'),
|
|
1553
|
+
joined: wrapper.find('.mint-topbar__run-slot--joined').exists(),
|
|
1554
|
+
}).toEqual(expected)
|
|
1555
|
+
})
|
|
1556
|
+
|
|
1557
|
+
it.each([
|
|
1558
|
+
[undefined, true],
|
|
1559
|
+
[null, true],
|
|
1560
|
+
[running, false],
|
|
1561
|
+
])('should pass showSave to the experiment chip when the job is %s', async (job, showSave) => {
|
|
1562
|
+
const wrapper = mountRunSlot(job, { experiment: true })
|
|
1563
|
+
await waitForAsyncComponents(wrapper)
|
|
1564
|
+
|
|
1565
|
+
expect(wrapper.findComponent(ExperimentPopover).props('showSave')).toBe(showSave)
|
|
1566
|
+
})
|
|
1567
|
+
|
|
1568
|
+
it('should keep the slot joined until the job has left, then show the idle button', async () => {
|
|
1569
|
+
const wrapper = mountRunSlot(running, { experiment: true })
|
|
1570
|
+
await waitForAsyncComponents(wrapper)
|
|
1571
|
+
const joined = () => wrapper.find('.mint-topbar__run-slot--joined').exists()
|
|
1572
|
+
|
|
1573
|
+
await wrapper.setProps({ job: null })
|
|
1574
|
+
expect(joined()).toBe(true)
|
|
1575
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job--idle').exists()).toBe(true))
|
|
1576
|
+
expect(joined()).toBe(false)
|
|
1577
|
+
|
|
1578
|
+
await wrapper.setProps({ job: running })
|
|
1579
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job--primary').exists()).toBe(true))
|
|
1580
|
+
expect(joined()).toBe(true)
|
|
1581
|
+
|
|
1582
|
+
await wrapper.setProps({ job: undefined })
|
|
1583
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job').exists()).toBe(false))
|
|
1584
|
+
expect(joined()).toBe(false)
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
it('should join the slot when the job and the slot mount together', async () => {
|
|
1588
|
+
const wrapper = mountRunSlot(undefined, { experiment: false })
|
|
1589
|
+
await waitForAsyncComponents(wrapper)
|
|
1590
|
+
|
|
1591
|
+
await wrapper.setProps({ job: running })
|
|
1592
|
+
|
|
1593
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__run-slot--joined').exists()).toBe(true))
|
|
1594
|
+
})
|
|
1595
|
+
|
|
1596
|
+
it.each([
|
|
1597
|
+
[120, '100%', 'Extracting 100%'],
|
|
1598
|
+
[-5, '0%', 'Extracting 0%'],
|
|
1599
|
+
])('should clamp percent %s to %s', async (percent, width, name) => {
|
|
1600
|
+
const wrapper = mountRunSlot({ ...running, percent }, { experiment: false })
|
|
1601
|
+
await waitForAsyncComponents(wrapper)
|
|
1602
|
+
|
|
1603
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1604
|
+
expect(chip.get('.mint-topbar__job-bar > span').attributes('style')).toContain(`width: ${width}`)
|
|
1605
|
+
expect(chip.attributes('aria-label')).toBe(name)
|
|
1606
|
+
})
|
|
1607
|
+
|
|
1608
|
+
it.each([
|
|
1609
|
+
[running, 'Job running'],
|
|
1610
|
+
[{ status: 'awaiting_input', label: 'Decoding' } as TopBarJob, 'Job needs input'],
|
|
1611
|
+
[{ status: 'failed', label: '1 failed' } as TopBarJob, 'Job failed'],
|
|
1612
|
+
[null, ''],
|
|
1613
|
+
])('should announce the status of %o as %s', async (job, text) => {
|
|
1614
|
+
const wrapper = mountRunSlot(job, { experiment: false })
|
|
1615
|
+
await waitForAsyncComponents(wrapper)
|
|
1616
|
+
|
|
1617
|
+
const live = wrapper.get('.mint-topbar__job-status')
|
|
1618
|
+
expect(live.attributes('aria-live')).toBe('polite')
|
|
1619
|
+
expect(live.text()).toBe(text)
|
|
1620
|
+
})
|
|
1621
|
+
|
|
1622
|
+
it.each([
|
|
1623
|
+
['queued', 'primary'],
|
|
1624
|
+
['running', 'primary'],
|
|
1625
|
+
['awaiting_input', 'warning'],
|
|
1626
|
+
['completed', 'success'],
|
|
1627
|
+
['failed', 'error'],
|
|
1628
|
+
['cancelled', 'neutral'],
|
|
1629
|
+
] as const)('should tone a %s job as %s', async (status, tone) => {
|
|
1630
|
+
const wrapper = mountRunSlot({ status, label: 'Job' }, { experiment: false })
|
|
1631
|
+
await waitForAsyncComponents(wrapper)
|
|
1632
|
+
|
|
1633
|
+
expect(wrapper.get('.mint-topbar__job').classes()).toContain(`mint-topbar__job--${tone}`)
|
|
1634
|
+
})
|
|
1635
|
+
|
|
1636
|
+
it.each([
|
|
1637
|
+
[running, 'Extracting 40%', true],
|
|
1638
|
+
[{ status: 'completed', label: 'Saved to EXP-0142' } as TopBarJob, 'Saved to EXP-0142', false],
|
|
1639
|
+
])('should show only the status for %o and name it %s', async (job, name, bar) => {
|
|
1640
|
+
const wrapper = mountRunSlot(job, { experiment: false })
|
|
1641
|
+
await waitForAsyncComponents(wrapper)
|
|
1642
|
+
|
|
1643
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1644
|
+
expect(chip.text()).toBe('')
|
|
1645
|
+
expect(chip.attributes('aria-label')).toBe(name)
|
|
1646
|
+
expect(chip.attributes('title')).toBe(name)
|
|
1647
|
+
expect(chip.find('.mint-topbar__job-bar').exists()).toBe(bar)
|
|
1648
|
+
if (bar) expect(chip.get('.mint-topbar__job-bar > span').attributes('style')).toContain('width: 40%')
|
|
1649
|
+
})
|
|
1650
|
+
|
|
1651
|
+
it.each([null, running])('should emit job-click when the job control is %s', async (job) => {
|
|
1652
|
+
const wrapper = mountRunSlot(job, { experiment: false })
|
|
1653
|
+
await waitForAsyncComponents(wrapper)
|
|
1654
|
+
|
|
1655
|
+
await wrapper.get('.mint-topbar__job').trigger('click')
|
|
1656
|
+
|
|
1657
|
+
expect(wrapper.emitted('job-click')).toEqual([[]])
|
|
1658
|
+
})
|
|
1659
|
+
})
|
|
1486
1660
|
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { nextTick } from 'vue'
|
|
2
3
|
import { describe, expect, it } from 'vitest'
|
|
3
4
|
import AppTopBarPageSelectorInternal from '../../components/internal/AppTopBarPageSelectorInternal.vue'
|
|
4
5
|
|
|
@@ -24,26 +25,139 @@ describe('AppTopBarPageSelectorInternal', () => {
|
|
|
24
25
|
expect(wrapper.emitted('select')).toEqual([[{ id: 'Results', label: 'Results' }]])
|
|
25
26
|
})
|
|
26
27
|
|
|
27
|
-
it('
|
|
28
|
+
it('should show each page hint under its label when the page has one', async () => {
|
|
28
29
|
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
29
30
|
props: {
|
|
30
31
|
currentPageId: 'downloads',
|
|
31
32
|
pages: [
|
|
32
33
|
{ id: 'downloads', label: 'Downloads', hint: 'Paste links and queue downloads' },
|
|
33
|
-
{ id: 'settings', label: 'Settings'
|
|
34
|
+
{ id: 'settings', label: 'Settings' },
|
|
34
35
|
],
|
|
35
36
|
},
|
|
36
37
|
})
|
|
37
38
|
|
|
38
39
|
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
'
|
|
41
|
+
const rows = wrapper.findAll('.mint-page-selector__item')
|
|
42
|
+
expect(rows.map(row => row.find('.mint-page-selector__item-hint').exists() ? row.get('.mint-page-selector__item-hint').text() : null)).toEqual([
|
|
43
|
+
'Paste links and queue downloads',
|
|
44
|
+
null,
|
|
43
45
|
])
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it.each([
|
|
49
|
+
{ pluginName: 'LEAF', crumb: ['LEAF', '/'], title: 'LEAF pages' },
|
|
50
|
+
{ pluginName: undefined, crumb: [], title: 'Pages' },
|
|
51
|
+
])('should name the menu after the plugin when pluginName is $pluginName', async ({ pluginName, crumb, title }) => {
|
|
52
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
53
|
+
props: { pluginName, currentPageId: 'Extract', pages: ['Extract', 'Analysis'] },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
expect(wrapper.findAll('.mint-page-selector__plugin, .mint-page-selector__slash').map(el => el.text())).toEqual(crumb)
|
|
57
|
+
|
|
58
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
59
|
+
|
|
60
|
+
expect(wrapper.get('.mint-page-selector__menu-title').text()).toBe(title)
|
|
61
|
+
expect(wrapper.get('[role="menu"]').attributes('aria-label')).toBe(title)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should show number keys for the first nine pages only', async () => {
|
|
65
|
+
const pages = Array.from({ length: 10 }, (_, i) => `Page ${i + 1}`)
|
|
66
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, { props: { pages } })
|
|
67
|
+
|
|
68
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
69
|
+
|
|
70
|
+
expect(wrapper.findAll('.mint-page-selector__item').map(row => row.find('.mint-page-selector__item-key').exists() ? row.get('.mint-page-selector__item-key').text() : null)).toEqual(
|
|
71
|
+
['1', '2', '3', '4', '5', '6', '7', '8', '9', null],
|
|
72
|
+
)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('keyboard', () => {
|
|
76
|
+
const pages = [
|
|
77
|
+
{ id: 'check', label: 'Quick check' },
|
|
78
|
+
{ id: 'extract', label: 'Extract' },
|
|
79
|
+
{ id: 'locked', label: 'Locked', disabled: true },
|
|
80
|
+
{ id: 'analysis', label: 'Analysis' },
|
|
81
|
+
]
|
|
82
|
+
const focusedLabel = () => document.activeElement?.querySelector('.mint-page-selector__item-label')?.textContent
|
|
83
|
+
|
|
84
|
+
function mountAttached() {
|
|
85
|
+
return mount(AppTopBarPageSelectorInternal, {
|
|
86
|
+
props: { pages, currentPageId: 'extract' },
|
|
87
|
+
attachTo: document.body,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
it.each(['ArrowDown', 'Enter', ' '])('should open the menu on the current row when %j is pressed on the trigger', async (key) => {
|
|
92
|
+
const wrapper = mountAttached()
|
|
93
|
+
|
|
94
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key })
|
|
95
|
+
await nextTick()
|
|
96
|
+
|
|
97
|
+
expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(true)
|
|
98
|
+
expect(focusedLabel()).toBe('Extract')
|
|
99
|
+
wrapper.unmount()
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('should move between enabled rows with arrows, Home and End', async () => {
|
|
103
|
+
const wrapper = mountAttached()
|
|
104
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
|
|
105
|
+
await nextTick()
|
|
106
|
+
const menu = wrapper.get('.mint-page-selector__menu')
|
|
107
|
+
|
|
108
|
+
const visited: Array<string | null | undefined> = []
|
|
109
|
+
for (const key of ['ArrowDown', 'ArrowDown', 'ArrowUp', 'Home', 'ArrowUp', 'End']) {
|
|
110
|
+
await menu.trigger('keydown', { key })
|
|
111
|
+
visited.push(focusedLabel())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
expect(visited).toEqual(['Analysis', 'Quick check', 'Analysis', 'Quick check', 'Analysis', 'Analysis'])
|
|
115
|
+
wrapper.unmount()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it.each(['Enter', ' '])('should activate the focused row and refocus the trigger when %j is pressed', async (key) => {
|
|
119
|
+
const wrapper = mountAttached()
|
|
120
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
|
|
121
|
+
await nextTick()
|
|
122
|
+
const menu = wrapper.get('.mint-page-selector__menu')
|
|
123
|
+
|
|
124
|
+
await menu.trigger('keydown', { key: 'ArrowDown' })
|
|
125
|
+
await menu.trigger('keydown', { key })
|
|
126
|
+
|
|
127
|
+
expect(wrapper.emitted('select')).toEqual([[pages[3]]])
|
|
128
|
+
expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(false)
|
|
129
|
+
expect(document.activeElement).toBe(wrapper.get('.mint-page-selector__trigger').element)
|
|
130
|
+
wrapper.unmount()
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it.each([
|
|
134
|
+
{ key: '4', emitted: [[pages[3]]], open: false },
|
|
135
|
+
{ key: '3', emitted: undefined, open: true },
|
|
136
|
+
{ key: '9', emitted: undefined, open: true },
|
|
137
|
+
])('should handle digit $key as a jump to that page', async ({ key, emitted, open }) => {
|
|
138
|
+
const wrapper = mountAttached()
|
|
139
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
|
|
140
|
+
await nextTick()
|
|
141
|
+
|
|
142
|
+
await wrapper.get('.mint-page-selector__menu').trigger('keydown', { key })
|
|
143
|
+
|
|
144
|
+
expect(wrapper.emitted('select')).toEqual(emitted)
|
|
145
|
+
expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(open)
|
|
146
|
+
wrapper.unmount()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should close on Escape and return focus to the trigger', async () => {
|
|
150
|
+
const wrapper = mountAttached()
|
|
151
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
|
|
152
|
+
await nextTick()
|
|
153
|
+
|
|
154
|
+
await wrapper.get('.mint-page-selector__menu').trigger('keydown', { key: 'Escape' })
|
|
155
|
+
await nextTick()
|
|
156
|
+
|
|
157
|
+
expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(false)
|
|
158
|
+
expect(document.activeElement).toBe(wrapper.get('.mint-page-selector__trigger').element)
|
|
159
|
+
wrapper.unmount()
|
|
160
|
+
})
|
|
47
161
|
})
|
|
48
162
|
|
|
49
163
|
it('closes linked page actions without emitting select', async () => {
|
|
@@ -171,6 +285,8 @@ describe('AppTopBarPageSelectorInternal', () => {
|
|
|
171
285
|
|
|
172
286
|
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
173
287
|
|
|
174
|
-
|
|
288
|
+
const rows = wrapper.findAll('.mint-page-selector__item')
|
|
289
|
+
expect(rows[0].classes()).toContain('mint-page-selector__item--active')
|
|
290
|
+
expect(rows.map(row => row.attributes('aria-current'))).toEqual(['page', undefined])
|
|
175
291
|
})
|
|
176
292
|
})
|
|
@@ -35,6 +35,25 @@ describe('BaseButton icon', () => {
|
|
|
35
35
|
})
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
+
// Text-only labels get a span so the stylesheet can trim the text box to the cap
|
|
39
|
+
// height (Geist sits ~1.6px high in a flex-centred button otherwise). Element
|
|
40
|
+
// children keep their flex layout, so they are not wrapped.
|
|
41
|
+
describe('BaseButton label', () => {
|
|
42
|
+
it.each([
|
|
43
|
+
['plain text', 'Cancel', 'Cancel'],
|
|
44
|
+
['element children', '<svg /><span>Export</span>', null],
|
|
45
|
+
])('should wrap the label in a span only for a %s slot', (_case, slot, label) => {
|
|
46
|
+
const wrapper = mount(BaseButton, { slots: { default: slot } })
|
|
47
|
+
const span = wrapper.find('.mint-button__label')
|
|
48
|
+
expect(span.exists() ? span.text() : null).toBe(label)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('should trim the label text box to the cap height', () => {
|
|
52
|
+
const css = readFileSync(resolve(__dirname, '../../styles/components/button.css'), 'utf8')
|
|
53
|
+
expect(css).toMatch(/\.mint-button__label \{\s*text-box: trim-both cap alphabetic;\s*\}/)
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
38
57
|
// jsdom has no layout, so this pins the stylesheet contract instead: a bare
|
|
39
58
|
// <svg> rendered by an icon-only BaseButton must be sized by the icon box, otherwise it
|
|
40
59
|
// collapses to zero width as a flex item (FilePicker source toggles, beta.14).
|
|
@@ -64,9 +64,10 @@ interface ModalProps {
|
|
|
64
64
|
currentExperimentId?: number | null
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function mountModal(props: ModalProps) {
|
|
67
|
+
function mountModal(props: ModalProps, slots: Record<string, string> = {}) {
|
|
68
68
|
return mount(ExperimentSelectorModal, {
|
|
69
69
|
props,
|
|
70
|
+
slots,
|
|
70
71
|
global: {
|
|
71
72
|
stubs: {
|
|
72
73
|
BaseModal: {
|
|
@@ -202,6 +203,38 @@ describe('ExperimentSelectorModal', () => {
|
|
|
202
203
|
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
|
203
204
|
})
|
|
204
205
|
|
|
206
|
+
it('should show the description slot above the experiment list', () => {
|
|
207
|
+
const wrapper = mountModal({ open: true }, { description: '<p class="why">Keep the result with its samples.</p>' })
|
|
208
|
+
|
|
209
|
+
const description = wrapper.get('.mint-experiment-selector__description')
|
|
210
|
+
expect(description.find('.why').text()).toBe('Keep the result with its samples.')
|
|
211
|
+
expect(description.element.compareDocumentPosition(wrapper.get('.mint-experiment-selector').element))
|
|
212
|
+
.toBe(Node.DOCUMENT_POSITION_FOLLOWING)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it.each([
|
|
216
|
+
[null, false],
|
|
217
|
+
[7, true],
|
|
218
|
+
])('should render the footer slot when the current experiment is %s, with the clear button %s', (currentExperimentId, hasClear) => {
|
|
219
|
+
const wrapper = mountModal({ open: true, currentExperimentId }, { footer: '<span class="extra">Skip</span>' })
|
|
220
|
+
|
|
221
|
+
const footer = wrapper.get('.mint-experiment-selector__footer')
|
|
222
|
+
expect(footer.find('.extra').exists()).toBe(true)
|
|
223
|
+
expect(footer.find('.mint-experiment-selector__clear-btn').exists()).toBe(hasClear)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should close without touching the store from the footer slot close()', async () => {
|
|
227
|
+
const store = useExperimentStore()
|
|
228
|
+
const wrapper = mountModal({ open: true }, {
|
|
229
|
+
footer: '<template #footer="{ close }"><button class="skip" @click="close">Skip</button></template>',
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
await wrapper.get('.skip').trigger('click')
|
|
233
|
+
|
|
234
|
+
expect(wrapper.emitted('update:open')).toEqual([[false]])
|
|
235
|
+
expect(store.current).toBeNull()
|
|
236
|
+
})
|
|
237
|
+
|
|
205
238
|
it('should list each status with its count and check the current one', async () => {
|
|
206
239
|
statusCounts.value = { ongoing: 3, planned: 1, completed: 0, cancelled: 2 }
|
|
207
240
|
filters.status = 'ongoing'
|
|
@@ -30,8 +30,14 @@ describe('menus and app shell styles', () => {
|
|
|
30
30
|
expect(css('app-pill-nav.css')).toMatch(/\.mint-pill-nav__item\s*\{[^}]*height:\s*1\.625rem;/)
|
|
31
31
|
})
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Design v2 option C: the page switcher sits in the crumb at the 32px top-bar control height.
|
|
34
|
+
it('should size the page selector trigger to the 32px top-bar control height', () => {
|
|
35
|
+
expect(css('app-page-selector.css')).toMatch(/\.mint-page-selector__trigger\s*\{[^}]*height:\s*2rem;/)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// A content-box menu measured 310px, and the platform's <a> rows (100% + padding) overflowed it.
|
|
39
|
+
it.each(['menu', 'item'])('should size the page selector %s as border-box', (part) => {
|
|
40
|
+
expect(css('app-page-selector.css')).toMatch(new RegExp(`\\.mint-page-selector__${part}\\s*\\{[^}]*box-sizing:\\s*border-box;`))
|
|
35
41
|
})
|
|
36
42
|
|
|
37
43
|
// Shrinking the tile beside the identity also shrank structured icons inside their solid chip, drawing a ring.
|
|
@@ -4,14 +4,26 @@ import Avatar from './Avatar.vue'
|
|
|
4
4
|
import PluginIcon from './PluginIcon.vue'
|
|
5
5
|
import { ExperimentProvider } from '../__stories__/experiment-helpers'
|
|
6
6
|
import { providePluginIdentity } from '../composables/pluginIdentity'
|
|
7
|
-
import type
|
|
7
|
+
import { ref, type App } from 'vue'
|
|
8
8
|
import type {
|
|
9
9
|
TopBarVariant,
|
|
10
10
|
PillNavItem,
|
|
11
11
|
PageSelectorItem,
|
|
12
12
|
PluginInfo,
|
|
13
|
+
TopBarJob,
|
|
13
14
|
} from '../types'
|
|
14
15
|
|
|
16
|
+
const runSlotJobs: { label: string; job: TopBarJob | null }[] = [
|
|
17
|
+
{ label: 'Idle', job: null },
|
|
18
|
+
{ label: 'Running', job: { status: 'running', label: 'Extracting', percent: 40 } },
|
|
19
|
+
{ label: 'Needs input', job: { status: 'awaiting_input', label: 'Decoding', percent: 62 } },
|
|
20
|
+
{ label: 'Saved', job: { status: 'completed', label: 'Saved to EXP-0142' } },
|
|
21
|
+
{ label: 'Failed', job: { status: 'failed', label: '1 failed' } },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
// An index, not the job object: Histoire merges object state across updates.
|
|
25
|
+
const liveRow = ref(0)
|
|
26
|
+
|
|
15
27
|
const variants: TopBarVariant[] = ['card', 'default']
|
|
16
28
|
|
|
17
29
|
const samplePillNav: PillNavItem[] = [
|
|
@@ -43,10 +55,12 @@ const samplePageSelector: PageSelectorItem[] = [
|
|
|
43
55
|
{ id: 'admin', label: 'Admin' },
|
|
44
56
|
]
|
|
45
57
|
|
|
58
|
+
// The menu shows each hint as a second line; a page without one stays a single line.
|
|
46
59
|
const samplePluginPages: PageSelectorItem[] = [
|
|
47
|
-
{ id: 'peaks', label: 'Peak integration' },
|
|
48
|
-
{ id: 'calibration', label: 'Calibration' },
|
|
49
|
-
{ id: 'report', label: 'Report' },
|
|
60
|
+
{ id: 'peaks', label: 'Peak integration', hint: 'Integrate peaks and review the fits per sample' },
|
|
61
|
+
{ id: 'calibration', label: 'Calibration', hint: 'Fit standard curves and check their residuals' },
|
|
62
|
+
{ id: 'report', label: 'Report', hint: 'Export concentrations for the experiment' },
|
|
63
|
+
{ id: 'settings', label: 'Settings' },
|
|
50
64
|
]
|
|
51
65
|
|
|
52
66
|
const samplePlugin: PluginInfo = {
|
|
@@ -147,6 +161,47 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
147
161
|
</div>
|
|
148
162
|
</Variant>
|
|
149
163
|
|
|
164
|
+
<Variant title="Run slot · Experiment and job">
|
|
165
|
+
<!-- The plugin picks the viewer's own job; the bar joins it to the experiment chip. Like LEAF Extract: no Save. -->
|
|
166
|
+
<div style="padding: 2rem; background: var(--bg-primary); display: grid; gap: 0.75rem;">
|
|
167
|
+
<ExperimentProvider :plugin="samplePlugin" :save="false">
|
|
168
|
+
<AppTopBar
|
|
169
|
+
v-for="row in runSlotJobs"
|
|
170
|
+
:key="row.label"
|
|
171
|
+
:spec="{ themeToggle: true, homePath: '' }"
|
|
172
|
+
:page-selector="samplePluginPages"
|
|
173
|
+
current-page-selector-id="peaks"
|
|
174
|
+
:job="row.job"
|
|
175
|
+
/>
|
|
176
|
+
</ExperimentProvider>
|
|
177
|
+
</div>
|
|
178
|
+
</Variant>
|
|
179
|
+
|
|
180
|
+
<Variant title="Run slot · Animation">
|
|
181
|
+
<div style="padding: 2rem; background: var(--bg-primary); display: grid; gap: 0.75rem;">
|
|
182
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
183
|
+
<button
|
|
184
|
+
v-for="(row, index) in runSlotJobs"
|
|
185
|
+
:key="row.label"
|
|
186
|
+
type="button"
|
|
187
|
+
data-run-slot-state
|
|
188
|
+
style="padding: 0.25rem 0.75rem; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-card); color: var(--text-primary); cursor: pointer;"
|
|
189
|
+
@click="liveRow = index"
|
|
190
|
+
>
|
|
191
|
+
{{ row.label }}
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
<ExperimentProvider :plugin="samplePlugin" :save="false">
|
|
195
|
+
<AppTopBar
|
|
196
|
+
:spec="{ themeToggle: true, homePath: '' }"
|
|
197
|
+
:page-selector="samplePluginPages"
|
|
198
|
+
current-page-selector-id="peaks"
|
|
199
|
+
:job="runSlotJobs[liveRow].job"
|
|
200
|
+
/>
|
|
201
|
+
</ExperimentProvider>
|
|
202
|
+
</div>
|
|
203
|
+
</Variant>
|
|
204
|
+
|
|
150
205
|
<Variant title="Refresh · Admin">
|
|
151
206
|
<div style="padding: 2rem; background: var(--bg-primary); min-height: 12rem;">
|
|
152
207
|
<AppTopBar
|