@morscherlab/mint-sdk 1.0.0-beta.7 → 1.0.0-rc.2
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 +9 -1
- package/dist/__tests__/components/LcmsSequenceTable.test.d.ts +1 -0
- package/dist/__tests__/components/ProgressBar.test.d.ts +1 -0
- package/dist/__tests__/components/RackEditor.test.d.ts +1 -0
- package/dist/__tests__/components/SequenceProgressBar.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentSamples.test.d.ts +1 -0
- package/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
- package/dist/__tests__/stores/settings.test.d.ts +1 -0
- package/dist/__tests__/utils/instrument.test.d.ts +1 -0
- package/dist/__tests__/utils/lcms.test.d.ts +1 -0
- package/dist/__tests__/utils/permissions.test.d.ts +1 -0
- package/dist/__tests__/utils/rack.test.d.ts +1 -0
- package/dist/{auth-QQj2kkze.js → auth-B7g4J4ZF.js} +148 -24
- package/dist/auth-B7g4J4ZF.js.map +1 -0
- package/dist/components/AutoGroupModal.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -2
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +1 -1
- package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/InstrumentAlertLog.vue.d.ts +22 -0
- package/dist/components/InstrumentStateBadge.vue.d.ts +11 -0
- package/dist/components/InstrumentStatusCard.vue.d.ts +13 -0
- package/dist/components/LcmsSequenceTable.vue.d.ts +26 -0
- package/dist/components/ProgressBar.vue.d.ts +1 -0
- package/dist/components/RackEditor.vue.d.ts +41 -3
- package/dist/components/ReagentList.vue.d.ts +1 -1
- package/dist/components/SampleSelector.vue.d.ts +5 -2
- package/dist/components/SegmentedControl.vue.d.ts +2 -0
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/SequenceProgressBar.vue.d.ts +15 -0
- package/dist/components/SettingsModal.vue.d.ts +8 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +42 -3
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +3 -3
- package/dist/{components-DihbSJjU.js → components-BhK-dW99.js} +2135 -1075
- package/dist/components-BhK-dW99.js.map +1 -0
- package/dist/composables/experimentDesignData.d.ts +17 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +4 -4
- package/dist/composables/useControlSchema.d.ts +11 -0
- package/dist/composables/useExperimentData.d.ts +11 -3
- package/dist/composables/useExperimentSamples.d.ts +42 -0
- package/dist/composables/usePlatformContext.d.ts +54 -0
- package/dist/{composables-BcgZ6diz.js → composables-Bg7CFuNz.js} +5 -3
- package/dist/composables-Bg7CFuNz.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -0
- package/dist/install.js +2 -2
- package/dist/instrument.d.ts +7 -0
- package/dist/lcms.d.ts +27 -0
- package/dist/permissions.d.ts +46 -0
- package/dist/stores/auth.d.ts +74 -2
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +3186 -1070
- package/dist/templates/builders.d.ts +7 -3
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.js +2 -2
- package/dist/templates/presets.d.ts +12 -0
- package/dist/templates/types.d.ts +16 -1
- package/dist/{templates-Cyt0Suwf.js → templates-BorLR_7p.js} +324 -10
- package/dist/templates-BorLR_7p.js.map +1 -0
- package/dist/types/auth.d.ts +2 -0
- package/dist/types/components.d.ts +32 -3
- package/dist/types/form-builder.d.ts +2 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrument.d.ts +56 -0
- package/dist/types/platform.d.ts +3 -0
- package/dist/{useExperimentData-CM6Y0u5L.js → useProtocolTemplates-n6AJqSqv.js} +627 -380
- package/dist/useProtocolTemplates-n6AJqSqv.js.map +1 -0
- package/dist/utils/rack.d.ts +47 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +15 -0
- package/src/__tests__/components/BaseTabs.test.ts +15 -0
- package/src/__tests__/components/GroupAssigner.test.ts +18 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +57 -0
- package/src/__tests__/components/ProgressBar.test.ts +18 -0
- package/src/__tests__/components/RackEditor.test.ts +125 -0
- package/src/__tests__/components/SampleSelector.test.ts +25 -0
- package/src/__tests__/components/SegmentedControl.test.ts +45 -0
- package/src/__tests__/components/SequenceProgressBar.test.ts +39 -0
- package/src/__tests__/components/SettingsModal.test.ts +83 -2
- package/src/__tests__/composables/useApi.test.ts +45 -0
- package/src/__tests__/composables/useAuth.test.ts +20 -0
- package/src/__tests__/composables/useControlSchema.test.ts +4 -0
- package/src/__tests__/composables/useExperimentData.test.ts +23 -0
- package/src/__tests__/composables/useExperimentSamples.test.ts +91 -0
- package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
- package/src/__tests__/stores/settings.test.ts +78 -0
- package/src/__tests__/templates/templates.test.ts +86 -0
- package/src/__tests__/utils/instrument.test.ts +47 -0
- package/src/__tests__/utils/lcms.test.ts +73 -0
- package/src/__tests__/utils/permissions.test.ts +50 -0
- package/src/__tests__/utils/rack.test.ts +120 -0
- package/src/components/AppAvatarMenu.vue +6 -3
- package/src/components/AppTopBar.vue +16 -10
- package/src/components/AuditTrail.vue +1 -1
- package/src/components/BaseTabs.vue +22 -1
- package/src/components/Calendar.vue +6 -2
- package/src/components/ConcentrationInput.vue +3 -2
- package/src/components/GroupAssigner.vue +8 -3
- package/src/components/InstrumentAlertLog.vue +191 -0
- package/src/components/InstrumentStateBadge.vue +50 -0
- package/src/components/InstrumentStatusCard.vue +188 -0
- package/src/components/LcmsSequenceTable.vue +191 -0
- package/src/components/NumberInput.vue +5 -3
- package/src/components/ProgressBar.vue +3 -0
- package/src/components/RackEditor.vue +73 -2
- package/src/components/SampleHierarchyTree.vue +3 -2
- package/src/components/SampleSelector.vue +28 -9
- package/src/components/SegmentedControl.story.vue +17 -0
- package/src/components/SegmentedControl.vue +14 -3
- package/src/components/SequenceProgressBar.vue +71 -0
- package/src/components/SettingsModal.vue +49 -2
- package/src/components/UnitInput.vue +6 -2
- package/src/components/WellPlate.vue +145 -24
- package/src/components/index.ts +5 -0
- package/src/components/internal/WellEditPopupInternal.vue +1 -0
- package/src/composables/experimentDesignData.ts +182 -0
- package/src/composables/index.ts +14 -0
- package/src/composables/useApi.ts +113 -16
- package/src/composables/useAuth.ts +4 -0
- package/src/composables/useAutoGroup.ts +18 -9
- package/src/composables/useControlSchema.ts +21 -0
- package/src/composables/useExperimentData.ts +57 -16
- package/src/composables/useExperimentSamples.ts +142 -0
- package/src/composables/useProtocolTemplates.ts +13 -1
- package/src/composables/useRackEditor.ts +3 -2
- package/src/index.ts +27 -0
- package/src/instrument.ts +90 -0
- package/src/lcms.ts +108 -0
- package/src/permissions.ts +143 -0
- package/src/stores/auth.ts +79 -26
- package/src/stores/settings.ts +10 -0
- package/src/styles/components/instrument-monitor.css +478 -0
- package/src/styles/components/lcms-sequence-table.css +189 -0
- package/src/styles/components/sequence-progress-bar.css +63 -0
- package/src/styles/components/settings-modal.css +9 -0
- package/src/styles/components/tabs.css +9 -0
- package/src/styles/components/well-edit-popup.css +7 -1
- package/src/styles/components/well-plate.css +5 -0
- package/src/styles/index.css +3 -0
- package/src/templates/builders.ts +201 -0
- package/src/templates/controlSchemas.ts +68 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/presets.ts +23 -0
- package/src/templates/types.ts +17 -0
- package/src/types/auth.ts +3 -0
- package/src/types/components.ts +45 -3
- package/src/types/form-builder.ts +2 -1
- package/src/types/index.ts +35 -0
- package/src/types/instrument.ts +61 -0
- package/src/types/platform.ts +4 -0
- package/src/utils/rack.ts +209 -0
- package/dist/auth-QQj2kkze.js.map +0 -1
- package/dist/components-DihbSJjU.js.map +0 -1
- package/dist/composables-BcgZ6diz.js.map +0 -1
- package/dist/templates-Cyt0Suwf.js.map +0 -1
- package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Rack, SlotPosition, WellEditData, WellPlateFormat } from '../types';
|
|
2
|
+
export type LcmsPlateType = '96well' | '54vial';
|
|
3
|
+
export type LcmsControlSampleType = 'blank' | 'qc' | 'iqc';
|
|
4
|
+
export interface LcmsPlateCell {
|
|
5
|
+
row: string;
|
|
6
|
+
column: number;
|
|
7
|
+
sample_name: string;
|
|
8
|
+
sample_type?: string | null;
|
|
9
|
+
injection_volume?: number | null;
|
|
10
|
+
injection_count?: number | null;
|
|
11
|
+
custom_method?: string | null;
|
|
12
|
+
slot?: SlotPosition | null;
|
|
13
|
+
}
|
|
14
|
+
export interface LcmsPlateCellsToRackOptions {
|
|
15
|
+
rackId?: string;
|
|
16
|
+
rackName?: string;
|
|
17
|
+
format?: WellPlateFormat;
|
|
18
|
+
plateType?: LcmsPlateType;
|
|
19
|
+
slot?: SlotPosition;
|
|
20
|
+
injectionVolume?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface LcmsPlateCellsToRacksOptions extends Omit<LcmsPlateCellsToRackOptions, 'rackId' | 'rackName' | 'slot'> {
|
|
23
|
+
defaultSlot?: SlotPosition;
|
|
24
|
+
rackId?: (slot: SlotPosition, index: number) => string;
|
|
25
|
+
rackName?: (slot: SlotPosition, index: number) => string;
|
|
26
|
+
}
|
|
27
|
+
export declare const LCMS_DEFAULT_CONTROL_POSITIONS: Record<LcmsPlateType, Record<LcmsControlSampleType, {
|
|
28
|
+
row: string;
|
|
29
|
+
column: number;
|
|
30
|
+
}>>;
|
|
31
|
+
export declare function lcmsPlateTypeToRackFormat(plateType: LcmsPlateType): WellPlateFormat;
|
|
32
|
+
export declare function rackFormatToLcmsPlateType(format: WellPlateFormat): LcmsPlateType;
|
|
33
|
+
export declare function lcmsWellId(row: string, column: number): string;
|
|
34
|
+
export declare function parseLcmsWellId(wellId: string): {
|
|
35
|
+
row: string;
|
|
36
|
+
column: number;
|
|
37
|
+
};
|
|
38
|
+
export declare function lcmsPlateCellsToRack(cells: LcmsPlateCell[], options?: LcmsPlateCellsToRackOptions): Rack;
|
|
39
|
+
export declare function lcmsPlateCellsToRacks(cells: LcmsPlateCell[], options?: LcmsPlateCellsToRacksOptions): Rack[];
|
|
40
|
+
export declare function rackToLcmsPlateCells(rack: Rack): LcmsPlateCell[];
|
|
41
|
+
export declare function racksToLcmsPlateCells(racks: Rack[]): LcmsPlateCell[];
|
|
42
|
+
export declare function getLcmsDefaultControlWellId(plateType: LcmsPlateType, sampleType: LcmsControlSampleType): string;
|
|
43
|
+
export declare function createLcmsControlWellEditData(sampleType: LcmsControlSampleType, options?: {
|
|
44
|
+
wellId?: string;
|
|
45
|
+
plateType?: LcmsPlateType;
|
|
46
|
+
injectionVolume?: number;
|
|
47
|
+
}): WellEditData;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc.2",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -367,6 +367,21 @@ describe('AppTopBar', () => {
|
|
|
367
367
|
expect(settingsModal.props('showAppearance')).toBe(false)
|
|
368
368
|
})
|
|
369
369
|
|
|
370
|
+
it('should pass settings userType to SettingsModal', () => {
|
|
371
|
+
const settingsConfig: TopBarSettingsConfig = {
|
|
372
|
+
userType: 'admin',
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const wrapper = createWrapper({
|
|
376
|
+
title: 'Test App',
|
|
377
|
+
showSettings: true,
|
|
378
|
+
settingsConfig,
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
const settingsModal = wrapper.findComponent(SettingsModal)
|
|
382
|
+
expect(settingsModal.props('userType')).toBe('admin')
|
|
383
|
+
})
|
|
384
|
+
|
|
370
385
|
it('should default showAppearance to true when not specified', () => {
|
|
371
386
|
const wrapper = createWrapper({
|
|
372
387
|
title: 'Test App',
|
|
@@ -22,4 +22,19 @@ describe('BaseTabs', () => {
|
|
|
22
22
|
|
|
23
23
|
expect(wrapper.emitted('update:modelValue')).toEqual([['Results']])
|
|
24
24
|
})
|
|
25
|
+
|
|
26
|
+
it('renders svg path icons', () => {
|
|
27
|
+
const wrapper = mount(BaseTabs, {
|
|
28
|
+
props: {
|
|
29
|
+
modelValue: 'users',
|
|
30
|
+
tabs: [
|
|
31
|
+
{ id: 'users', label: 'Users', icon: 'M12 4h8M12 12h8M12 20h8' },
|
|
32
|
+
{ id: 'logs', label: 'Logs', icon: ['M4 4h16v16H4z', 'M8 8h8'] },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(wrapper.findAll('svg.mint-tab__icon--svg')).toHaveLength(2)
|
|
38
|
+
expect(wrapper.findAll('path')).toHaveLength(3)
|
|
39
|
+
})
|
|
25
40
|
})
|
|
@@ -27,4 +27,22 @@ describe('GroupAssigner', () => {
|
|
|
27
27
|
expect(wrapper.emitted('update:group1')?.[0]).toEqual([[]])
|
|
28
28
|
expect(wrapper.emitted('update:group2')).toBeUndefined()
|
|
29
29
|
})
|
|
30
|
+
|
|
31
|
+
it('does not throw when drag events omit dataTransfer', async () => {
|
|
32
|
+
const wrapper = mount(GroupAssigner, {
|
|
33
|
+
props: {
|
|
34
|
+
groups,
|
|
35
|
+
group1: [],
|
|
36
|
+
group2: [],
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
await expect(
|
|
41
|
+
wrapper.find('.mint-group-assigner__pill--unassigned').trigger('dragstart', { dataTransfer: null }),
|
|
42
|
+
).resolves.toBeUndefined()
|
|
43
|
+
|
|
44
|
+
await expect(
|
|
45
|
+
wrapper.find('.mint-group-assigner__zone').trigger('dragover', { dataTransfer: null }),
|
|
46
|
+
).resolves.toBeUndefined()
|
|
47
|
+
})
|
|
30
48
|
})
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import LcmsSequenceTable from '../../components/LcmsSequenceTable.vue'
|
|
4
|
+
import type { LcmsSequenceItem } from '../../lcms'
|
|
5
|
+
|
|
6
|
+
function makeItems(): LcmsSequenceItem[] {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
sample_type: 'Unknown',
|
|
10
|
+
file_name: 'Batch_NEG_S1_001',
|
|
11
|
+
sample_id: 'R:A1',
|
|
12
|
+
path: '',
|
|
13
|
+
instrument_method: 'C:\\Methods\\neg.meth',
|
|
14
|
+
position: 'R:A1',
|
|
15
|
+
injection_volume: 5,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
sample_type: 'Blank',
|
|
19
|
+
file_name: 'Batch_NEG_Blank_002',
|
|
20
|
+
sample_id: 'R:F9',
|
|
21
|
+
path: '',
|
|
22
|
+
instrument_method: 'C:\\Methods\\blank.meth',
|
|
23
|
+
position: 'R:F9',
|
|
24
|
+
injection_volume: 5,
|
|
25
|
+
},
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('LcmsSequenceTable', () => {
|
|
30
|
+
it('renders Xcalibur sequence rows with method basenames', () => {
|
|
31
|
+
const wrapper = mount(LcmsSequenceTable, {
|
|
32
|
+
props: {
|
|
33
|
+
items: makeItems(),
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
expect(wrapper.text()).toContain('Batch_NEG_S1_001')
|
|
38
|
+
expect(wrapper.text()).toContain('neg.meth')
|
|
39
|
+
expect(wrapper.text()).toContain('Blank')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('emits editable row actions', async () => {
|
|
43
|
+
const wrapper = mount(LcmsSequenceTable, {
|
|
44
|
+
props: {
|
|
45
|
+
items: makeItems(),
|
|
46
|
+
editable: true,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const actions = wrapper.findAll('.mint-lcms-sequence-table__action')
|
|
51
|
+
await actions[0].trigger('click')
|
|
52
|
+
await actions[1].trigger('click')
|
|
53
|
+
|
|
54
|
+
expect(wrapper.emitted('duplicate')?.[0]).toEqual([0])
|
|
55
|
+
expect(wrapper.emitted('remove')?.[0]).toEqual([0])
|
|
56
|
+
})
|
|
57
|
+
})
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import ProgressBar from '../../components/ProgressBar.vue'
|
|
4
|
+
|
|
5
|
+
describe('ProgressBar', () => {
|
|
6
|
+
it('clamps rail values and exposes an accessible label', () => {
|
|
7
|
+
const wrapper = mount(ProgressBar, {
|
|
8
|
+
props: {
|
|
9
|
+
value: 130,
|
|
10
|
+
ariaLabel: 'Connection pool usage',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const progress = wrapper.get('[role="progressbar"]')
|
|
15
|
+
expect(progress.attributes('aria-valuenow')).toBe('100')
|
|
16
|
+
expect(progress.attributes('aria-label')).toBe('Connection pool usage')
|
|
17
|
+
})
|
|
18
|
+
})
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { h, nextTick } from 'vue'
|
|
4
|
+
import RackEditor from '../../components/RackEditor.vue'
|
|
5
|
+
import type { Rack, WellEditData } from '../../types'
|
|
6
|
+
|
|
7
|
+
function makeRacks(): Rack[] {
|
|
8
|
+
return [
|
|
9
|
+
{
|
|
10
|
+
id: 'rack-1',
|
|
11
|
+
name: 'Rack 1',
|
|
12
|
+
format: 54,
|
|
13
|
+
slot: 'R',
|
|
14
|
+
injectionVolume: 5,
|
|
15
|
+
wells: {
|
|
16
|
+
A1: {
|
|
17
|
+
id: 'A1',
|
|
18
|
+
row: 0,
|
|
19
|
+
col: 0,
|
|
20
|
+
state: 'filled',
|
|
21
|
+
sampleType: 'sample',
|
|
22
|
+
metadata: {
|
|
23
|
+
label: 'Original',
|
|
24
|
+
injectionVolume: 5,
|
|
25
|
+
injectionCount: 1,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function jsonDataTransfer(data: unknown): DataTransfer {
|
|
34
|
+
return {
|
|
35
|
+
dropEffect: 'copy',
|
|
36
|
+
effectAllowed: 'copy',
|
|
37
|
+
getData: (type: string) => type === 'application/json' ? JSON.stringify(data) : '',
|
|
38
|
+
setData: vi.fn(),
|
|
39
|
+
} as unknown as DataTransfer
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('RackEditor', () => {
|
|
43
|
+
it('passes rack-aware well editor slot props through to WellPlate', async () => {
|
|
44
|
+
const wrapper = mount(RackEditor, {
|
|
45
|
+
props: {
|
|
46
|
+
modelValue: makeRacks(),
|
|
47
|
+
},
|
|
48
|
+
slots: {
|
|
49
|
+
'well-editor': (slotProps) => h(
|
|
50
|
+
'button',
|
|
51
|
+
{
|
|
52
|
+
'data-test': 'custom-well-editor',
|
|
53
|
+
onClick: () => slotProps.save({
|
|
54
|
+
wellId: slotProps.wellId,
|
|
55
|
+
label: 'Custom',
|
|
56
|
+
sampleType: 'iqc',
|
|
57
|
+
injectionVolume: 7,
|
|
58
|
+
injectionCount: 3,
|
|
59
|
+
customMethod: 'C:\\Methods\\custom.meth',
|
|
60
|
+
} satisfies WellEditData),
|
|
61
|
+
},
|
|
62
|
+
`${slotProps.rackId}:${slotProps.rack?.slot}:${slotProps.wellId}:${slotProps.wellData?.metadata?.label}`,
|
|
63
|
+
),
|
|
64
|
+
},
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await wrapper.find('.mint-well-plate__well').trigger('click')
|
|
68
|
+
|
|
69
|
+
const customEditor = wrapper.get('[data-test="custom-well-editor"]')
|
|
70
|
+
expect(customEditor.text()).toBe('rack-1:R:A1:Original')
|
|
71
|
+
|
|
72
|
+
await customEditor.trigger('click')
|
|
73
|
+
await nextTick()
|
|
74
|
+
|
|
75
|
+
const emittedRacks = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as Rack[]
|
|
76
|
+
expect(emittedRacks[0].wells.A1).toMatchObject({
|
|
77
|
+
sampleType: 'iqc',
|
|
78
|
+
metadata: {
|
|
79
|
+
label: 'Custom',
|
|
80
|
+
injectionVolume: 7,
|
|
81
|
+
injectionCount: 3,
|
|
82
|
+
customMethod: 'C:\\Methods\\custom.meth',
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('assigns dropped external samples when sample drop is enabled', async () => {
|
|
88
|
+
const wrapper = mount(RackEditor, {
|
|
89
|
+
props: {
|
|
90
|
+
modelValue: makeRacks(),
|
|
91
|
+
allowSampleDrop: true,
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
await wrapper.findAll('.mint-well-plate__well')[1].trigger('drop', {
|
|
96
|
+
dataTransfer: jsonDataTransfer({
|
|
97
|
+
type: 'sample',
|
|
98
|
+
id: 'sample-2',
|
|
99
|
+
sampleName: 'Dragged Sample',
|
|
100
|
+
}),
|
|
101
|
+
preventDefault: vi.fn(),
|
|
102
|
+
})
|
|
103
|
+
await nextTick()
|
|
104
|
+
|
|
105
|
+
const emittedRacks = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as Rack[]
|
|
106
|
+
expect(emittedRacks[0].wells.A2).toMatchObject({
|
|
107
|
+
sampleType: 'sample',
|
|
108
|
+
metadata: {
|
|
109
|
+
label: 'Dragged Sample',
|
|
110
|
+
injectionVolume: 5,
|
|
111
|
+
injectionCount: 1,
|
|
112
|
+
customMethod: null,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(wrapper.emitted('sample-drop')?.[0]?.slice(0, 3)).toEqual([
|
|
117
|
+
'rack-1',
|
|
118
|
+
'A2',
|
|
119
|
+
expect.objectContaining({
|
|
120
|
+
id: 'sample-2',
|
|
121
|
+
sampleName: 'Dragged Sample',
|
|
122
|
+
}),
|
|
123
|
+
])
|
|
124
|
+
})
|
|
125
|
+
})
|
|
@@ -57,4 +57,29 @@ describe('SampleSelector', () => {
|
|
|
57
57
|
|
|
58
58
|
expect(wrapper.find('.mint-sample-selector__sample-name').exists()).toBe(false)
|
|
59
59
|
})
|
|
60
|
+
|
|
61
|
+
it('can derive samples directly from experiment design data', () => {
|
|
62
|
+
const wrapper = mount(SampleSelector, {
|
|
63
|
+
props: {
|
|
64
|
+
modelValue: [],
|
|
65
|
+
designData: {
|
|
66
|
+
samples: [
|
|
67
|
+
{ sample_name: 'S1', sample_type: 'sample' },
|
|
68
|
+
{ sample_name: 'Blank', sample_type: 'blank' },
|
|
69
|
+
{ sample_name: 'S2', sample_type: 'sample' },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
enableGrouping: false,
|
|
73
|
+
enableSmartGroup: false,
|
|
74
|
+
},
|
|
75
|
+
global: {
|
|
76
|
+
stubs: {
|
|
77
|
+
AutoGroupModal: true,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
expect(wrapper.find('.mint-sample-selector__select-all-count').text()).toBe('2 samples')
|
|
83
|
+
expect(wrapper.findAll('.mint-sample-selector__flat-name').map(item => item.text())).toEqual(['S1', 'S2'])
|
|
84
|
+
})
|
|
60
85
|
})
|
|
@@ -21,4 +21,49 @@ describe('SegmentedControl', () => {
|
|
|
21
21
|
|
|
22
22
|
expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([96])
|
|
23
23
|
})
|
|
24
|
+
|
|
25
|
+
it('renders option-level disabled tabs without allowing selection', async () => {
|
|
26
|
+
const wrapper = mount(SegmentedControl, {
|
|
27
|
+
props: {
|
|
28
|
+
modelValue: 'active',
|
|
29
|
+
options: [
|
|
30
|
+
{ value: 'active', label: 'Active' },
|
|
31
|
+
{ value: 'archived', label: 'Archived', disabled: true },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const buttons = wrapper.findAll('button')
|
|
37
|
+
|
|
38
|
+
expect(buttons).toHaveLength(2)
|
|
39
|
+
expect(buttons[1].attributes('disabled')).toBeDefined()
|
|
40
|
+
expect(buttons[1].classes()).toContain('mint-segmented-control__option--disabled')
|
|
41
|
+
|
|
42
|
+
await buttons[1].trigger('click')
|
|
43
|
+
|
|
44
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('can disable tabs by value while keeping them visible', async () => {
|
|
48
|
+
const wrapper = mount(SegmentedControl, {
|
|
49
|
+
props: {
|
|
50
|
+
modelValue: 'table',
|
|
51
|
+
options: [
|
|
52
|
+
{ value: 'table', label: 'Table' },
|
|
53
|
+
{ value: 'chart', label: 'Chart' },
|
|
54
|
+
],
|
|
55
|
+
disabledValues: ['chart'],
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const buttons = wrapper.findAll('button')
|
|
60
|
+
|
|
61
|
+
expect(buttons.map(button => button.text())).toEqual(['Table', 'Chart'])
|
|
62
|
+
expect(buttons[1].attributes('disabled')).toBeDefined()
|
|
63
|
+
|
|
64
|
+
await buttons[1].trigger('keydown.enter')
|
|
65
|
+
await buttons[1].trigger('click')
|
|
66
|
+
|
|
67
|
+
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
|
68
|
+
})
|
|
24
69
|
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import SequenceProgressBar from '../../components/SequenceProgressBar.vue'
|
|
4
|
+
|
|
5
|
+
describe('SequenceProgressBar', () => {
|
|
6
|
+
it('renders sequence progress and remaining time', () => {
|
|
7
|
+
const wrapper = mount(SequenceProgressBar, {
|
|
8
|
+
props: {
|
|
9
|
+
progress: {
|
|
10
|
+
current_sample: 3,
|
|
11
|
+
total_samples: 6,
|
|
12
|
+
elapsed_seconds: 180,
|
|
13
|
+
sample_durations: [50, 70, 60],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(wrapper.get('.mint-sequence-progress__label').text()).toBe('3 / 6 samples')
|
|
19
|
+
expect(wrapper.get('.mint-sequence-progress__percent').text()).toBe('50%')
|
|
20
|
+
expect(wrapper.text()).toContain('~3m left')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('supports compact rendering without footer labels', () => {
|
|
24
|
+
const wrapper = mount(SequenceProgressBar, {
|
|
25
|
+
props: {
|
|
26
|
+
compact: true,
|
|
27
|
+
label: 'injections',
|
|
28
|
+
progress: {
|
|
29
|
+
current_sample: 2,
|
|
30
|
+
total_samples: 4,
|
|
31
|
+
estimated_remaining_seconds: 120,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(wrapper.get('.mint-sequence-progress__label').text()).toBe('2 / 4 injections')
|
|
37
|
+
expect(wrapper.find('.mint-sequence-progress__footer').exists()).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -3,10 +3,11 @@ import { createPinia } from 'pinia'
|
|
|
3
3
|
import { describe, expect, it } from 'vitest'
|
|
4
4
|
import SettingsModal from '../../components/SettingsModal.vue'
|
|
5
5
|
import { defineControlModel, type ControlSchema } from '../../composables/useControlSchema'
|
|
6
|
+
import { useAuthStore } from '../../stores/auth'
|
|
6
7
|
import type { SettingsModalSchema } from '../../types'
|
|
7
8
|
|
|
8
9
|
describe('SettingsModal', () => {
|
|
9
|
-
function mountSettingsModal(props = {}, slots = {}) {
|
|
10
|
+
function mountSettingsModal(props = {}, slots = {}, pinia = createPinia()) {
|
|
10
11
|
return mount(SettingsModal, {
|
|
11
12
|
props: {
|
|
12
13
|
modelValue: true,
|
|
@@ -15,7 +16,7 @@ describe('SettingsModal', () => {
|
|
|
15
16
|
},
|
|
16
17
|
slots,
|
|
17
18
|
global: {
|
|
18
|
-
plugins: [
|
|
19
|
+
plugins: [pinia],
|
|
19
20
|
stubs: {
|
|
20
21
|
teleport: true,
|
|
21
22
|
},
|
|
@@ -293,4 +294,84 @@ describe('SettingsModal', () => {
|
|
|
293
294
|
threshold: 0.25,
|
|
294
295
|
})
|
|
295
296
|
})
|
|
297
|
+
|
|
298
|
+
it('filters schema groups and fields by explicit user type', async () => {
|
|
299
|
+
const schema: SettingsModalSchema = {
|
|
300
|
+
groups: [
|
|
301
|
+
{
|
|
302
|
+
id: 'general',
|
|
303
|
+
label: 'General',
|
|
304
|
+
fields: [
|
|
305
|
+
{ name: 'common', label: 'Common', type: 'text', defaultValue: 'shared' },
|
|
306
|
+
{ name: 'adminSecret', label: 'Admin Secret', type: 'text', visibleFor: 'admin' },
|
|
307
|
+
{ name: 'userNote', label: 'User Note', type: 'text', visibleFor: 'user' },
|
|
308
|
+
],
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
id: 'admin',
|
|
312
|
+
label: 'Admin',
|
|
313
|
+
visibleFor: 'admin',
|
|
314
|
+
fields: [
|
|
315
|
+
{ name: 'adminOnly', label: 'Admin Only', type: 'text' },
|
|
316
|
+
],
|
|
317
|
+
},
|
|
318
|
+
],
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const userWrapper = mountSettingsModal({ schema, userType: 'user' })
|
|
322
|
+
|
|
323
|
+
expect(userWrapper.text()).toContain('Common')
|
|
324
|
+
expect(userWrapper.text()).toContain('User Note')
|
|
325
|
+
expect(userWrapper.text()).not.toContain('Admin Secret')
|
|
326
|
+
expect(userWrapper.text()).not.toContain('Admin Only')
|
|
327
|
+
|
|
328
|
+
const adminWrapper = mountSettingsModal({ schema, userType: 'admin' })
|
|
329
|
+
|
|
330
|
+
expect(adminWrapper.findAll('.mint-settings-modal__tab').map(tab => tab.text())).toEqual([
|
|
331
|
+
'General',
|
|
332
|
+
'Admin',
|
|
333
|
+
])
|
|
334
|
+
expect(adminWrapper.text()).toContain('Common')
|
|
335
|
+
expect(adminWrapper.text()).toContain('Admin Secret')
|
|
336
|
+
expect(adminWrapper.text()).not.toContain('User Note')
|
|
337
|
+
|
|
338
|
+
await adminWrapper.findAll('.mint-settings-modal__tab')[1].trigger('click')
|
|
339
|
+
|
|
340
|
+
expect(adminWrapper.text()).toContain('Admin Only')
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
it('uses SDK auth permissions when no user type override is passed', () => {
|
|
344
|
+
const pinia = createPinia()
|
|
345
|
+
const auth = useAuthStore(pinia)
|
|
346
|
+
auth.setUserInfo({
|
|
347
|
+
id: '1',
|
|
348
|
+
username: 'admin',
|
|
349
|
+
shortname: null,
|
|
350
|
+
email: null,
|
|
351
|
+
role: 'member',
|
|
352
|
+
roleObj: {
|
|
353
|
+
slug: 'member',
|
|
354
|
+
permissions: ['plugins.configure'],
|
|
355
|
+
},
|
|
356
|
+
isActive: true,
|
|
357
|
+
})
|
|
358
|
+
const schema: SettingsModalSchema = {
|
|
359
|
+
groups: [
|
|
360
|
+
{
|
|
361
|
+
id: 'plugins',
|
|
362
|
+
label: 'Plugins',
|
|
363
|
+
permissions: ['plugins.configure'],
|
|
364
|
+
fields: [
|
|
365
|
+
{ name: 'registry', label: 'Registry', type: 'text' },
|
|
366
|
+
{ name: 'danger', label: 'Danger Zone', type: 'text', requiresAdmin: true },
|
|
367
|
+
],
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const wrapper = mountSettingsModal({ schema }, {}, pinia)
|
|
373
|
+
|
|
374
|
+
expect(wrapper.text()).toContain('Registry')
|
|
375
|
+
expect(wrapper.text()).not.toContain('Danger Zone')
|
|
376
|
+
})
|
|
296
377
|
})
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
import { createPinia, setActivePinia } from 'pinia'
|
|
3
|
+
import type { InternalAxiosRequestConfig } from 'axios'
|
|
3
4
|
|
|
4
5
|
import { useApi } from '../../composables/useApi'
|
|
6
|
+
import { useAuthStore } from '../../stores/auth'
|
|
7
|
+
|
|
8
|
+
function readAuthorizationHeader(headers: InternalAxiosRequestConfig['headers']): unknown {
|
|
9
|
+
const bag = headers as Record<string, unknown> & { get?: (header: string) => unknown }
|
|
10
|
+
return (typeof bag.get === 'function' ? bag.get('Authorization') : undefined)
|
|
11
|
+
?? bag.Authorization
|
|
12
|
+
?? bag.authorization
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
describe('useApi URL helpers', () => {
|
|
7
16
|
beforeEach(() => {
|
|
@@ -9,6 +18,7 @@ describe('useApi URL helpers', () => {
|
|
|
9
18
|
})
|
|
10
19
|
|
|
11
20
|
afterEach(() => {
|
|
21
|
+
vi.restoreAllMocks()
|
|
12
22
|
vi.unstubAllEnvs()
|
|
13
23
|
})
|
|
14
24
|
|
|
@@ -27,4 +37,39 @@ describe('useApi URL helpers', () => {
|
|
|
27
37
|
expect(api.buildWsUrl('/events')).toMatch(/\/api\/events$/)
|
|
28
38
|
expect(api.buildWsUrl('/events')).not.toContain('/api//events')
|
|
29
39
|
})
|
|
40
|
+
|
|
41
|
+
it('normalizes paths that already include the configured API prefix', async () => {
|
|
42
|
+
const api = useApi({ baseUrl: '/api' })
|
|
43
|
+
const getSpy = vi.spyOn(api.client, 'get').mockResolvedValue({ data: { ok: true } })
|
|
44
|
+
|
|
45
|
+
expect(api.buildUrl('/api/projects')).toBe('/api/projects')
|
|
46
|
+
expect(api.buildUrl('/api')).toBe('/api')
|
|
47
|
+
expect(api.buildUrl('/api?active=true')).toBe('/api?active=true')
|
|
48
|
+
|
|
49
|
+
await expect(api.get<{ ok: boolean }>('/api/projects')).resolves.toEqual({ ok: true })
|
|
50
|
+
expect(getSpy).toHaveBeenCalledWith('/projects', expect.objectContaining({ baseURL: '/api' }))
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('honors withAuth=false even when the SDK auth store has a token', async () => {
|
|
54
|
+
const authStore = useAuthStore()
|
|
55
|
+
authStore.setToken('secret-token', 3600)
|
|
56
|
+
const api = useApi({ baseUrl: '/api', withAuth: false })
|
|
57
|
+
|
|
58
|
+
let authorization: unknown
|
|
59
|
+
const result = await api.get<{ ok: boolean }>('/public', {
|
|
60
|
+
adapter: async (config) => {
|
|
61
|
+
authorization = readAuthorizationHeader(config.headers)
|
|
62
|
+
return {
|
|
63
|
+
data: { ok: true },
|
|
64
|
+
status: 200,
|
|
65
|
+
statusText: 'OK',
|
|
66
|
+
headers: {},
|
|
67
|
+
config,
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
expect(result).toEqual({ ok: true })
|
|
73
|
+
expect(authorization).toBeUndefined()
|
|
74
|
+
})
|
|
30
75
|
})
|
|
@@ -94,6 +94,26 @@ describe('useAuth', () => {
|
|
|
94
94
|
})
|
|
95
95
|
})
|
|
96
96
|
|
|
97
|
+
describe('storage fallback', () => {
|
|
98
|
+
it('keeps auth usable when localStorage operations throw', () => {
|
|
99
|
+
vi.stubGlobal('localStorage', {
|
|
100
|
+
getItem: vi.fn(() => { throw new Error('blocked') }),
|
|
101
|
+
setItem: vi.fn(() => { throw new Error('blocked') }),
|
|
102
|
+
removeItem: vi.fn(() => { throw new Error('blocked') }),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const authStore = useAuthStore()
|
|
106
|
+
|
|
107
|
+
expect(() => authStore.initialize()).not.toThrow()
|
|
108
|
+
|
|
109
|
+
authStore.setToken('memory-token', 3600)
|
|
110
|
+
expect(authStore.token).toBe('memory-token')
|
|
111
|
+
|
|
112
|
+
authStore.clearToken()
|
|
113
|
+
expect(authStore.token).toBeNull()
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
97
117
|
describe('refreshToken - race condition', () => {
|
|
98
118
|
it('should deduplicate concurrent refresh calls', async () => {
|
|
99
119
|
const authStore = useAuthStore()
|
|
@@ -478,6 +478,8 @@ describe('useControlSchema', () => {
|
|
|
478
478
|
icon: '<svg viewBox="0 0 24 24"></svg>',
|
|
479
479
|
columns: 2,
|
|
480
480
|
condition,
|
|
481
|
+
visibleFor: 'admin',
|
|
482
|
+
permissions: ['plugins.configure'],
|
|
481
483
|
},
|
|
482
484
|
},
|
|
483
485
|
})
|
|
@@ -490,6 +492,8 @@ describe('useControlSchema', () => {
|
|
|
490
492
|
icon: '<svg viewBox="0 0 24 24"></svg>',
|
|
491
493
|
columns: 2,
|
|
492
494
|
condition,
|
|
495
|
+
visibleFor: 'admin',
|
|
496
|
+
permissions: ['plugins.configure'],
|
|
493
497
|
})
|
|
494
498
|
expect(schema.groups[0].fields.map(field => field.name)).toEqual(['threshold', 'method'])
|
|
495
499
|
expect(schema.groups[1].label).toBe('Output')
|