@morscherlab/mint-sdk 1.4.0-beta.11 → 1.4.0-beta.13
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-B2Bgr3ei.js → AlertBox-BdoZqANv.js} +2 -2
- package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-BdoZqANv.js.map} +1 -1
- package/dist/{AppSidebar-mvm--GxN.js → AppSidebar-CIXJBzDv.js} +237 -70
- package/dist/{AppSidebar-mvm--GxN.js.map → AppSidebar-CIXJBzDv.js.map} +1 -1
- package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-CbD7BR_s.js} +2 -2
- package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-CbD7BR_s.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-BbFtr1Fr.js} +112 -96
- package/dist/ExperimentSelectorModal-BbFtr1Fr.js.map +1 -0
- package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-BkwYOKpG.js} +2 -2
- package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-BkwYOKpG.js.map} +1 -1
- package/dist/{SettingsModal-DO3IZ7qz.js → SettingsModal-CdhFMi8j.js} +25 -24
- package/dist/SettingsModal-CdhFMi8j.js.map +1 -0
- package/dist/__stories__/experiment-helpers.d.ts +10 -0
- package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
- package/dist/components/AppTopBar.vue.d.ts +13 -5
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
- package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-BF5g4wRC.js} +2 -2
- package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-BF5g4wRC.js.map} +1 -1
- package/dist/{data-CgILxDLW.js → data-DUt0TIp5.js} +10 -10
- package/dist/{data-CgILxDLW.js.map → data-DUt0TIp5.js.map} +1 -1
- package/dist/data.js +7 -7
- package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
- package/dist/experiment-DyO54FQB.js.map +1 -0
- package/dist/index.js +18 -18
- package/dist/{install-mB8-IyvO.js → install-Czoe1rA8.js} +13 -13
- package/dist/{install-mB8-IyvO.js.map → install-Czoe1rA8.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-D5bIX426.js → lab-B-pLwc5U.js} +10 -10
- package/dist/{lab-D5bIX426.js.map → lab-B-pLwc5U.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B6gpM_3e.js → records-DnK_1Q_S.js} +2 -2
- package/dist/{records-B6gpM_3e.js.map → records-DnK_1Q_S.js.map} +1 -1
- package/dist/{runtime-D7vKpfbF.js → runtime-fPb_FEZr.js} +9 -9
- package/dist/{runtime-D7vKpfbF.js.map → runtime-fPb_FEZr.js.map} +1 -1
- package/dist/runtime.js +7 -7
- package/dist/{shell-CywENwBT.js → shell-lDf_EY-x.js} +4 -4
- package/dist/{shell-CywENwBT.js.map → shell-lDf_EY-x.js.map} +1 -1
- package/dist/shell.js +4 -4
- 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 +409 -60
- 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/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-BGN1dmcR.js} +4 -4
- package/dist/{useExperimentSamples-DHnxKFAF.js.map → useExperimentSamples-BGN1dmcR.js.map} +1 -1
- package/dist/{useListReorder-BitdUhxn.js → useListReorder-DPynGrCj.js} +3 -3
- package/dist/{useListReorder-BitdUhxn.js.map → useListReorder-DPynGrCj.js.map} +1 -1
- package/dist/{useRackEditor-Q8xO-cCy.js → useRackEditor-B2FrQA0p.js} +7 -7
- package/dist/{useRackEditor-Q8xO-cCy.js.map → useRackEditor-B2FrQA0p.js.map} +1 -1
- package/dist/{useRealtime-C3iy9QkU.js → useRealtime-yWjTTnc9.js} +2 -2
- package/dist/{useRealtime-C3iy9QkU.js.map → useRealtime-yWjTTnc9.js.map} +1 -1
- package/dist/{useRequest-D506eHi8.js → useRequest-Dqw12k2s.js} +3 -3
- package/dist/{useRequest-D506eHi8.js.map → useRequest-Dqw12k2s.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/ExperimentSelectorModal.test.ts +34 -1
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- package/src/__tests__/stores/settings.test.ts +21 -21
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/components/AppTopBar.story.vue +59 -4
- package/src/components/AppTopBar.vue +107 -8
- package/src/components/ExperimentSelectorModal.story.vue +38 -0
- package/src/components/ExperimentSelectorModal.vue +20 -1
- package/src/components/SettingsModal.vue +7 -12
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/stores/index.ts +0 -2
- package/src/stores/settings.ts +20 -50
- package/src/styles/components/app-page-selector.css +120 -45
- package/src/styles/components/app-top-bar.css +155 -0
- package/src/styles/components/experiment-selector-modal.css +13 -0
- package/src/styles/variables.css +121 -15
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/src/types/platform.ts +1 -1
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +0 -1
- package/dist/SettingsModal-DO3IZ7qz.js.map +0 -1
- package/dist/experiment-43OJtH7R.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
|
})
|
|
@@ -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'
|
|
@@ -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 () => {
|
|
@@ -2,7 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
import { createPinia, setActivePinia } from 'pinia'
|
|
3
3
|
import { nextTick } from 'vue'
|
|
4
4
|
import {
|
|
5
|
-
paletteCssVariables,
|
|
6
5
|
tableDensityToSize,
|
|
7
6
|
tryUseSettingsStore,
|
|
8
7
|
useSettingsStore,
|
|
@@ -54,6 +53,7 @@ describe('useSettingsStore', () => {
|
|
|
54
53
|
document.documentElement.classList.remove('dark')
|
|
55
54
|
document.documentElement.removeAttribute('style')
|
|
56
55
|
delete document.documentElement.dataset.density
|
|
56
|
+
delete document.documentElement.dataset.palette
|
|
57
57
|
mockMatchMedia(false)
|
|
58
58
|
})
|
|
59
59
|
|
|
@@ -72,51 +72,51 @@ describe('useSettingsStore', () => {
|
|
|
72
72
|
expect(settings.serverHost).toBe('127.0.0.1')
|
|
73
73
|
expect(settings.serverPort).toBe(8001)
|
|
74
74
|
expect(settings.theme).toBe('dark')
|
|
75
|
-
expect(settings.colorPalette).toBe('
|
|
75
|
+
expect(settings.colorPalette).toBe('purple')
|
|
76
76
|
expect(settings.tableDensity).toBe('compact')
|
|
77
77
|
expect(localStorage.getItem('mld-settings')).toBeNull()
|
|
78
78
|
expect(JSON.parse(localStorage.getItem('mint-settings') || '{}')).toEqual(expect.objectContaining({
|
|
79
79
|
serverHost: '127.0.0.1',
|
|
80
80
|
serverPort: 8001,
|
|
81
81
|
theme: 'dark',
|
|
82
|
-
colorPalette: '
|
|
82
|
+
colorPalette: 'purple',
|
|
83
83
|
tableDensity: 'compact',
|
|
84
84
|
}))
|
|
85
85
|
})
|
|
86
86
|
|
|
87
87
|
describe('palette and density DOM sync', () => {
|
|
88
|
-
it('
|
|
88
|
+
it('mirrors the stored palette onto html[data-palette] without inline overrides', () => {
|
|
89
|
+
localStorage.setItem('mint-settings', JSON.stringify({ colorPalette: 'ocean' }))
|
|
90
|
+
|
|
89
91
|
const settings = useSettingsStore()
|
|
90
92
|
settings.initialize()
|
|
91
93
|
|
|
92
|
-
expect(
|
|
94
|
+
expect(document.documentElement.dataset.palette).toBe('ocean')
|
|
93
95
|
expect(document.documentElement.style.getPropertyValue('--color-primary')).toBe('')
|
|
94
96
|
})
|
|
95
97
|
|
|
96
|
-
it('
|
|
97
|
-
localStorage.setItem('mint-settings', JSON.stringify({ colorPalette: 'viridis' }))
|
|
98
|
-
|
|
98
|
+
it('updates html[data-palette] when the palette changes', async () => {
|
|
99
99
|
const settings = useSettingsStore()
|
|
100
100
|
settings.initialize()
|
|
101
|
+
expect(document.documentElement.dataset.palette).toBe('default')
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
expect(
|
|
105
|
-
expect(style.getPropertyValue('--color-cta')).toBe('hsl(80 95% 53%)')
|
|
103
|
+
settings.colorPalette = 'forest'
|
|
104
|
+
await nextTick()
|
|
105
|
+
expect(document.documentElement.dataset.palette).toBe('forest')
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
-
it(
|
|
108
|
+
it.each([
|
|
109
|
+
['viridis', 'purple'],
|
|
110
|
+
['pastel', 'default'],
|
|
111
|
+
['not-a-palette', 'default'],
|
|
112
|
+
])('maps the stored palette %s onto %s', (stored, expected) => {
|
|
113
|
+
localStorage.setItem('mint-settings', JSON.stringify({ colorPalette: stored }))
|
|
114
|
+
|
|
109
115
|
const settings = useSettingsStore()
|
|
110
116
|
settings.initialize()
|
|
111
117
|
|
|
112
|
-
settings.colorPalette
|
|
113
|
-
|
|
114
|
-
expect(document.documentElement.style.getPropertyValue('--color-primary')).toBe('hsl(200 84% 67%)')
|
|
115
|
-
|
|
116
|
-
settings.colorPalette = 'default'
|
|
117
|
-
await nextTick()
|
|
118
|
-
expect(document.documentElement.style.getPropertyValue('--color-primary')).toBe('')
|
|
119
|
-
expect(document.documentElement.style.getPropertyValue('--color-primary-soft')).toBe('')
|
|
118
|
+
expect(settings.colorPalette).toBe(expected)
|
|
119
|
+
expect(document.documentElement.dataset.palette).toBe(expected)
|
|
120
120
|
})
|
|
121
121
|
|
|
122
122
|
it('mirrors the table density onto html[data-density]', async () => {
|
|
@@ -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.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
import { colorPalettes } from '../../stores/settings'
|
|
6
|
+
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const variablesCss = readFileSync(join(resolve(here, '../..'), 'styles/variables.css'), 'utf-8')
|
|
9
|
+
|
|
10
|
+
const ACCENT_TOKENS = [
|
|
11
|
+
'--color-primary',
|
|
12
|
+
'--color-primary-hover',
|
|
13
|
+
'--color-primary-pressed',
|
|
14
|
+
'--color-primary-soft',
|
|
15
|
+
'--color-primary-border',
|
|
16
|
+
]
|
|
17
|
+
const STATUS_TOKENS = ['--mint-success', '--mint-error', '--mint-warning', '--mint-warning-text']
|
|
18
|
+
|
|
19
|
+
/** Declarations of every rule whose selector list has a selector that ends in `[data-palette="<key>"]`. */
|
|
20
|
+
function paletteDeclarations(key: string, dark: boolean): string {
|
|
21
|
+
const attr = `[data-palette="${key}"]`
|
|
22
|
+
return [...variablesCss.matchAll(/([^{}]+)\{([^}]*)\}/g)]
|
|
23
|
+
.filter(([, selector]) => selector.split(',').some((s) => {
|
|
24
|
+
const one = s.trim()
|
|
25
|
+
return one.endsWith(attr) && one.startsWith('html.dark') === dark
|
|
26
|
+
}))
|
|
27
|
+
.map(([, , body]) => body)
|
|
28
|
+
.join('\n')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('accent palettes', () => {
|
|
32
|
+
it.each(Object.keys(colorPalettes).flatMap((key) => [[key, 'light'], [key, 'dark']]))(
|
|
33
|
+
'should define every accent token for the %s palette in %s mode',
|
|
34
|
+
(key, theme) => {
|
|
35
|
+
const body = paletteDeclarations(key, theme === 'dark')
|
|
36
|
+
for (const token of ACCENT_TOKENS) expect(body, `${key} ${theme} ${token}`).toMatch(new RegExp(`${token}\\s*:`))
|
|
37
|
+
},
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
it('should give the colour-blind palette Okabe-Ito status colours', () => {
|
|
41
|
+
const body = paletteDeclarations('colorblind', false)
|
|
42
|
+
for (const token of STATUS_TOKENS) expect(body, token).toMatch(new RegExp(`${token}\\s*:`))
|
|
43
|
+
})
|
|
44
|
+
})
|