@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
|
@@ -70,6 +70,29 @@ describe('useExperimentData', () => {
|
|
|
70
70
|
expect(hook2.treeData.value).toEqual([{ id: '2' }])
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
it('unwraps platform design-data responses and exposes sample helpers', async () => {
|
|
74
|
+
const response = {
|
|
75
|
+
experiment_id: 42,
|
|
76
|
+
plugin_id: 'ms-designer',
|
|
77
|
+
schema_version: '1.0',
|
|
78
|
+
data: {
|
|
79
|
+
tree_data: [{ id: 'design' }],
|
|
80
|
+
samples: [
|
|
81
|
+
{ sample_name: 'S1', sample_type: 'sample', conditions: { group: 'A' } },
|
|
82
|
+
{ sample_name: 'Blank', sample_type: 'blank', conditions: { group: 'QC' } },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
fakeResponse = response
|
|
87
|
+
const hook = useExperimentData()
|
|
88
|
+
await hook.fetch(42)
|
|
89
|
+
|
|
90
|
+
expect(hook.designData.value).toEqual(response.data)
|
|
91
|
+
expect(hook.treeData.value).toEqual([{ id: 'design' }])
|
|
92
|
+
expect(hook.sampleNames.value).toEqual(['S1'])
|
|
93
|
+
expect(hook.sampleOptions.value).toEqual([{ value: 'S1', label: 'S1' }])
|
|
94
|
+
})
|
|
95
|
+
|
|
73
96
|
it('tableData falls back to empty array when absent', async () => {
|
|
74
97
|
fakeResponse = {}
|
|
75
98
|
const hook = useExperimentData()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia'
|
|
3
|
+
import axios, { type AxiosRequestConfig } from 'axios'
|
|
4
|
+
|
|
5
|
+
import { useExperimentSamples } from '../../composables/useExperimentSamples'
|
|
6
|
+
|
|
7
|
+
let fakeResponse: unknown = null
|
|
8
|
+
let fakeError: Error | null = null
|
|
9
|
+
let recordedUrls: string[] = []
|
|
10
|
+
|
|
11
|
+
describe('useExperimentSamples', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
setActivePinia(createPinia())
|
|
14
|
+
fakeResponse = {}
|
|
15
|
+
fakeError = null
|
|
16
|
+
recordedUrls = []
|
|
17
|
+
vi.spyOn(axios.Axios.prototype, 'get').mockImplementation(async function (
|
|
18
|
+
this: unknown,
|
|
19
|
+
url: string,
|
|
20
|
+
_config?: AxiosRequestConfig,
|
|
21
|
+
) {
|
|
22
|
+
recordedUrls.push(url)
|
|
23
|
+
if (fakeError) throw fakeError
|
|
24
|
+
return { data: fakeResponse }
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
vi.restoreAllMocks()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('derives samples from pre-fetched MS Designer design_data', () => {
|
|
33
|
+
const hook = useExperimentSamples({
|
|
34
|
+
syncAppExperiment: false,
|
|
35
|
+
designData: {
|
|
36
|
+
samples: [
|
|
37
|
+
{ sample_name: 'S1', sample_type: 'sample', conditions: { group: 'A' } },
|
|
38
|
+
{ sample_name: 'QC1', sample_type: 'qc', conditions: { group: 'QC' } },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
expect(hook.samples.value).toEqual(['S1'])
|
|
44
|
+
expect(hook.sampleOptions.value).toEqual([{ value: 'S1', label: 'S1' }])
|
|
45
|
+
expect(recordedUrls).toEqual([])
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('loads and unwraps platform design_data for an experiment id', async () => {
|
|
49
|
+
fakeResponse = {
|
|
50
|
+
experiment_id: 7,
|
|
51
|
+
plugin_id: 'ms-designer',
|
|
52
|
+
data: {
|
|
53
|
+
samples: [
|
|
54
|
+
{ sample_name: 'S1', sample_type: 'sample' },
|
|
55
|
+
{ sample_name: 'S2', sample_type: 'sample' },
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
schema_version: '1.0',
|
|
59
|
+
}
|
|
60
|
+
const hook = useExperimentSamples({ syncAppExperiment: false, immediate: false })
|
|
61
|
+
|
|
62
|
+
await hook.fetch(7)
|
|
63
|
+
|
|
64
|
+
expect(recordedUrls).toEqual(['/experiments/7/data'])
|
|
65
|
+
expect(hook.samples.value).toEqual(['S1', 'S2'])
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it('extracts options from template collection sample sheets', () => {
|
|
69
|
+
const hook = useExperimentSamples({
|
|
70
|
+
syncAppExperiment: false,
|
|
71
|
+
designData: {
|
|
72
|
+
templates: {
|
|
73
|
+
'sample-sheet': {
|
|
74
|
+
data: {
|
|
75
|
+
samples: [
|
|
76
|
+
{ sampleId: 'ctrl', name: 'Control', group: 'baseline' },
|
|
77
|
+
{ sampleId: 'drug', name: 'Drug', group: 'treated' },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
expect(hook.samples.value).toEqual(['ctrl', 'drug'])
|
|
86
|
+
expect(hook.sampleOptions.value).toEqual([
|
|
87
|
+
{ value: 'ctrl', label: 'Control', description: 'baseline' },
|
|
88
|
+
{ value: 'drug', label: 'Drug', description: 'treated' },
|
|
89
|
+
])
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { useProtocolTemplates } from '../../composables/useProtocolTemplates'
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'mint-custom-protocol-templates'
|
|
5
|
+
|
|
6
|
+
function installLocalStorage() {
|
|
7
|
+
const storage: Record<string, string> = {}
|
|
8
|
+
vi.stubGlobal('localStorage', {
|
|
9
|
+
get length() {
|
|
10
|
+
return Object.keys(storage).length
|
|
11
|
+
},
|
|
12
|
+
key: vi.fn((index: number) => Object.keys(storage)[index] ?? null),
|
|
13
|
+
getItem: vi.fn((key: string) => storage[key] ?? null),
|
|
14
|
+
setItem: vi.fn((key: string, value: string) => {
|
|
15
|
+
storage[key] = value
|
|
16
|
+
}),
|
|
17
|
+
removeItem: vi.fn((key: string) => {
|
|
18
|
+
delete storage[key]
|
|
19
|
+
}),
|
|
20
|
+
clear: vi.fn(() => {
|
|
21
|
+
for (const key of Object.keys(storage)) delete storage[key]
|
|
22
|
+
}),
|
|
23
|
+
} as unknown as Storage)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('useProtocolTemplates', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
vi.restoreAllMocks()
|
|
29
|
+
vi.unstubAllGlobals()
|
|
30
|
+
installLocalStorage()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('ignores corrupted custom template storage', () => {
|
|
34
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ invalid: true }))
|
|
35
|
+
|
|
36
|
+
const templates = useProtocolTemplates()
|
|
37
|
+
|
|
38
|
+
expect(templates.customTemplates.value).toEqual([])
|
|
39
|
+
expect(templates.allTemplates.value.length).toBeGreaterThan(0)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('filters invalid custom template entries', () => {
|
|
43
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify([
|
|
44
|
+
{ id: 'bad', name: 'Missing parameters', type: 'custom' },
|
|
45
|
+
{
|
|
46
|
+
id: 'custom-valid',
|
|
47
|
+
name: 'Valid custom step',
|
|
48
|
+
type: 'custom',
|
|
49
|
+
parameters: [],
|
|
50
|
+
},
|
|
51
|
+
]))
|
|
52
|
+
|
|
53
|
+
const templates = useProtocolTemplates()
|
|
54
|
+
|
|
55
|
+
expect(templates.customTemplates.value).toEqual([
|
|
56
|
+
{
|
|
57
|
+
id: 'custom-valid',
|
|
58
|
+
name: 'Valid custom step',
|
|
59
|
+
type: 'custom',
|
|
60
|
+
parameters: [],
|
|
61
|
+
},
|
|
62
|
+
])
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia'
|
|
3
|
+
import { useSettingsStore } from '../../stores/settings'
|
|
4
|
+
|
|
5
|
+
function mockMatchMedia(matches = false) {
|
|
6
|
+
Object.defineProperty(window, 'matchMedia', {
|
|
7
|
+
configurable: true,
|
|
8
|
+
writable: true,
|
|
9
|
+
value: vi.fn(() => ({
|
|
10
|
+
matches,
|
|
11
|
+
media: '(prefers-color-scheme: dark)',
|
|
12
|
+
onchange: null,
|
|
13
|
+
addEventListener: vi.fn(),
|
|
14
|
+
removeEventListener: vi.fn(),
|
|
15
|
+
addListener: vi.fn(),
|
|
16
|
+
removeListener: vi.fn(),
|
|
17
|
+
dispatchEvent: vi.fn(),
|
|
18
|
+
})),
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function installLocalStorage() {
|
|
23
|
+
const storage: Record<string, string> = {}
|
|
24
|
+
vi.stubGlobal('localStorage', {
|
|
25
|
+
get length() {
|
|
26
|
+
return Object.keys(storage).length
|
|
27
|
+
},
|
|
28
|
+
key: vi.fn((index: number) => Object.keys(storage)[index] ?? null),
|
|
29
|
+
getItem: vi.fn((key: string) => storage[key] ?? null),
|
|
30
|
+
setItem: vi.fn((key: string, value: string) => {
|
|
31
|
+
storage[key] = value
|
|
32
|
+
}),
|
|
33
|
+
removeItem: vi.fn((key: string) => {
|
|
34
|
+
delete storage[key]
|
|
35
|
+
}),
|
|
36
|
+
clear: vi.fn(() => {
|
|
37
|
+
for (const key of Object.keys(storage)) delete storage[key]
|
|
38
|
+
}),
|
|
39
|
+
} as unknown as Storage)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('useSettingsStore', () => {
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
vi.restoreAllMocks()
|
|
45
|
+
vi.unstubAllGlobals()
|
|
46
|
+
setActivePinia(createPinia())
|
|
47
|
+
installLocalStorage()
|
|
48
|
+
document.documentElement.classList.remove('dark')
|
|
49
|
+
mockMatchMedia(false)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('migrates legacy MLD settings into the MINT settings key', () => {
|
|
53
|
+
localStorage.setItem('mld-settings', JSON.stringify({
|
|
54
|
+
serverHost: '127.0.0.1',
|
|
55
|
+
serverPort: 8001,
|
|
56
|
+
theme: 'dark',
|
|
57
|
+
colorPalette: 'viridis',
|
|
58
|
+
tableDensity: 'compact',
|
|
59
|
+
}))
|
|
60
|
+
|
|
61
|
+
const settings = useSettingsStore()
|
|
62
|
+
settings.initialize()
|
|
63
|
+
|
|
64
|
+
expect(settings.serverHost).toBe('127.0.0.1')
|
|
65
|
+
expect(settings.serverPort).toBe(8001)
|
|
66
|
+
expect(settings.theme).toBe('dark')
|
|
67
|
+
expect(settings.colorPalette).toBe('viridis')
|
|
68
|
+
expect(settings.tableDensity).toBe('compact')
|
|
69
|
+
expect(localStorage.getItem('mld-settings')).toBeNull()
|
|
70
|
+
expect(JSON.parse(localStorage.getItem('mint-settings') || '{}')).toEqual(expect.objectContaining({
|
|
71
|
+
serverHost: '127.0.0.1',
|
|
72
|
+
serverPort: 8001,
|
|
73
|
+
theme: 'dark',
|
|
74
|
+
colorPalette: 'viridis',
|
|
75
|
+
tableDensity: 'compact',
|
|
76
|
+
}))
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
createReagentListTemplate,
|
|
21
21
|
createSampleSheetTemplate,
|
|
22
22
|
createSamplePrepTemplate,
|
|
23
|
+
createTargetedMetabolomicsCollection,
|
|
23
24
|
createDefaultBioTemplate,
|
|
24
25
|
createTemplateCollection,
|
|
25
26
|
createTimeCourseTemplate,
|
|
@@ -209,6 +210,7 @@ describe('bio data templates', () => {
|
|
|
209
210
|
'wellplate-screen',
|
|
210
211
|
'qpcr-expression',
|
|
211
212
|
'lcms-batch',
|
|
213
|
+
'targeted-metabolomics',
|
|
212
214
|
'elisa-assay',
|
|
213
215
|
'flow-cytometry-assay',
|
|
214
216
|
'western-blot-assay',
|
|
@@ -224,6 +226,8 @@ describe('bio data templates', () => {
|
|
|
224
226
|
expect(getBioTemplatePresetInfo('metabolomics')?.name).toBe('lcms-batch')
|
|
225
227
|
expect(getBioTemplatePresetInfo('metabolism')?.name).toBe('lcms-batch')
|
|
226
228
|
expect(getBioTemplatePresetInfo('metabolite profiling')?.name).toBe('lcms-batch')
|
|
229
|
+
expect(getBioTemplatePresetInfo('targeted metabolomics')?.name).toBe('targeted-metabolomics')
|
|
230
|
+
expect(searchBioTemplatePresets('internal standards')[0].name).toBe('targeted-metabolomics')
|
|
227
231
|
expect(getBioTemplatePresetInfo('immunoassay')?.templates).toEqual([
|
|
228
232
|
'plate-map',
|
|
229
233
|
'sample-sheet',
|
|
@@ -275,6 +279,7 @@ describe('bio data templates', () => {
|
|
|
275
279
|
const plateSchema = getBioTemplateControlSchema('wellplate')
|
|
276
280
|
const presetSchema = getBioTemplateControlSchema('wellplate-screen')
|
|
277
281
|
const defaults = getBioTemplateControlDefaults('lcms-batch')
|
|
282
|
+
const targetedDefaults = getBioTemplateControlDefaults('targeted-metabolomics')
|
|
278
283
|
const elisaDefaults = getBioTemplateControlDefaults('elisa-assay')
|
|
279
284
|
const flowDefaults = getBioTemplateControlDefaults('flow-cytometry-assay')
|
|
280
285
|
const westernDefaults = getBioTemplateControlDefaults('western-blot-assay')
|
|
@@ -284,6 +289,9 @@ describe('bio data templates', () => {
|
|
|
284
289
|
expect(presetSchema?.replicates.default).toBe(3)
|
|
285
290
|
expect(defaults.instrument).toBe('LC-MS')
|
|
286
291
|
expect(defaults.featureNames).toEqual(['Glucose', 'Lactate'])
|
|
292
|
+
expect(targetedDefaults.metaboliteNames).toEqual(['Glucose', 'Lactate', 'Pyruvate'])
|
|
293
|
+
expect(targetedDefaults.internalStandards).toEqual(['Stable isotope internal standard mix'])
|
|
294
|
+
expect(targetedDefaults.method).toBe('Targeted metabolomics')
|
|
287
295
|
expect(elisaDefaults.analyte).toBe('Analyte')
|
|
288
296
|
expect(elisaDefaults.standardConcentrations).toEqual(['1000', '100', '10', '1'])
|
|
289
297
|
expect(flowDefaults.markers).toEqual(['CD3', 'CD4', 'CD8'])
|
|
@@ -299,6 +307,7 @@ describe('bio data templates', () => {
|
|
|
299
307
|
const elisaBindings = getBioTemplateComponentBindings('elisa-assay')
|
|
300
308
|
const flowBindings = getBioTemplateComponentBindings('flow-cytometry-assay')
|
|
301
309
|
const lcmsBindings = getBioTemplateComponentBindings('lcms-batch')
|
|
310
|
+
const targetedBindings = getBioTemplateComponentBindings('targeted-metabolomics')
|
|
302
311
|
const westernBindings = getBioTemplateComponentBindings('western-blot-assay')
|
|
303
312
|
const wellplateImports = toBioTemplateComponentImports('wellplate-screen')
|
|
304
313
|
|
|
@@ -311,6 +320,9 @@ describe('bio data templates', () => {
|
|
|
311
320
|
expect(elisaBindings.map(binding => binding.component)).toContain('DataFrame')
|
|
312
321
|
expect(lcmsBindings.map(binding => binding.component)).toContain('ScheduleCalendar')
|
|
313
322
|
expect(lcmsBindings.map(binding => binding.component)).toContain('ExperimentTimeline')
|
|
323
|
+
expect(targetedBindings.map(binding => binding.component)).toContain('ReagentList')
|
|
324
|
+
expect(targetedBindings.map(binding => binding.component)).toContain('DataFrame')
|
|
325
|
+
expect(targetedBindings.map(binding => binding.component)).toContain('ScheduleCalendar')
|
|
314
326
|
expect(flowBindings.map(binding => binding.template_id)).toEqual([
|
|
315
327
|
'sample-sheet',
|
|
316
328
|
'sample-sheet',
|
|
@@ -541,6 +553,13 @@ describe('bio data templates', () => {
|
|
|
541
553
|
features: ['Glucose'],
|
|
542
554
|
instrument: 'LC-MS',
|
|
543
555
|
})
|
|
556
|
+
const targeted = createTargetedMetabolomicsCollection({
|
|
557
|
+
samples: ['S001', 'S002'],
|
|
558
|
+
metabolites: ['Glucose', 'Lactate'],
|
|
559
|
+
internalStandards: ['13C Glucose'],
|
|
560
|
+
standardConcentrations: [0.1, 1, 10],
|
|
561
|
+
instrument: 'Orbitrap',
|
|
562
|
+
})
|
|
544
563
|
const flow = createFlowCytometryAssayCollection({
|
|
545
564
|
samples: ['Control', 'Treatment'],
|
|
546
565
|
markers: ['CD3', 'CD4'],
|
|
@@ -555,6 +574,7 @@ describe('bio data templates', () => {
|
|
|
555
574
|
const wellplateTemplates = extractTemplateCollection(wellplate)
|
|
556
575
|
const qpcrTemplates = extractTemplateCollection(qpcr)
|
|
557
576
|
const lcmsTemplates = extractTemplateCollection(lcms)
|
|
577
|
+
const targetedTemplates = extractTemplateCollection(targeted)
|
|
558
578
|
const flowTemplates = extractTemplateCollection(flow)
|
|
559
579
|
const westernTemplates = extractTemplateCollection(western)
|
|
560
580
|
const wellplateComponentProps = toBioTemplateComponentProps(wellplate)
|
|
@@ -572,6 +592,28 @@ describe('bio data templates', () => {
|
|
|
572
592
|
expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[2].sampleId).toBe('s001')
|
|
573
593
|
expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('glucose')
|
|
574
594
|
expect(toAssayMatrixSampleOptions(lcmsTemplates['assay-matrix'] as AssayMatrixTemplate).map(option => option.label)).toEqual(['S001', 'S002'])
|
|
595
|
+
expect(Object.keys(targetedTemplates)).toEqual([
|
|
596
|
+
'sample-sheet',
|
|
597
|
+
'sample-prep',
|
|
598
|
+
'reagent-list',
|
|
599
|
+
'instrument-run',
|
|
600
|
+
'calibration-curve',
|
|
601
|
+
'assay-matrix',
|
|
602
|
+
])
|
|
603
|
+
expect(targeted.metadata?.preset).toBe('targeted-metabolomics')
|
|
604
|
+
expect(toReagentDataFrame(targetedTemplates['reagent-list'] as ReagentListTemplate).data[0].kind).toBe('compound')
|
|
605
|
+
expect(toTemplateDataFrame(targetedTemplates['calibration-curve']).data[0].role).toBe('blank')
|
|
606
|
+
expect(toInstrumentRunRows(targetedTemplates['instrument-run'] as InstrumentRunTemplate).map(row => row.kind)).toEqual([
|
|
607
|
+
'blank',
|
|
608
|
+
'standard',
|
|
609
|
+
'standard',
|
|
610
|
+
'standard',
|
|
611
|
+
'qc',
|
|
612
|
+
'qc',
|
|
613
|
+
'sample',
|
|
614
|
+
'sample',
|
|
615
|
+
])
|
|
616
|
+
expect(toTemplateDataFrame(targetedTemplates['assay-matrix']).columns.map(column => column.key)).toContain('lactate')
|
|
575
617
|
expect(Object.keys(flowTemplates)).toEqual(['sample-sheet', 'flow-cytometry-panel', 'assay-matrix'])
|
|
576
618
|
expect(flow.metadata?.preset).toBe('flow-cytometry-assay')
|
|
577
619
|
expect(flowTemplates['flow-cytometry-panel'].data).toMatchObject({
|
|
@@ -596,6 +638,7 @@ describe('bio data templates', () => {
|
|
|
596
638
|
expect(Object.keys(extractTemplateCollection(aliasCollection))).toEqual(['plate-map', 'sample-sheet', 'dose-response'])
|
|
597
639
|
expect(createBioTemplatePresetCollection('facs').metadata?.preset).toBe('flow-cytometry-assay')
|
|
598
640
|
expect(createBioTemplatePresetCollection('immunoblot').metadata?.preset).toBe('western-blot-assay')
|
|
641
|
+
expect(createBioTemplatePresetCollection('targeted metabolomics').metadata?.preset).toBe('targeted-metabolomics')
|
|
599
642
|
expect(() => createBioTemplatePresetCollection('unknown-preset')).toThrow("Unknown template preset 'unknown-preset'.")
|
|
600
643
|
})
|
|
601
644
|
|
|
@@ -682,6 +725,49 @@ describe('bio data templates', () => {
|
|
|
682
725
|
expect(run.data.items.map(item => item.kind)).toEqual(['blank', 'sample', 'sample'])
|
|
683
726
|
expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('pyruvate')
|
|
684
727
|
|
|
728
|
+
const targetedOptions = bioTemplatePresetControlValuesToOptions('targeted-metabolomics', {
|
|
729
|
+
sampleNames: 'QC Pool, Patient A',
|
|
730
|
+
metaboliteNames: 'Glucose, Citrate',
|
|
731
|
+
internalStandards: '13C Glucose',
|
|
732
|
+
standardConcentrations: '0.5, 5, 50',
|
|
733
|
+
concentrationUnit: 'nM',
|
|
734
|
+
responseUnit: 'area ratio',
|
|
735
|
+
instrument: 'Triple quadrupole',
|
|
736
|
+
method: 'MRM panel',
|
|
737
|
+
outputVolume: '25',
|
|
738
|
+
includeQc: false,
|
|
739
|
+
})
|
|
740
|
+
const targetedTemplates = extractTemplateCollection(
|
|
741
|
+
createBioTemplatePresetCollectionFromControls('targeted-metabolomics', {
|
|
742
|
+
sampleNames: 'QC Pool, Patient A',
|
|
743
|
+
metaboliteNames: 'Glucose, Citrate',
|
|
744
|
+
internalStandards: '13C Glucose',
|
|
745
|
+
standardConcentrations: '0.5, 5, 50',
|
|
746
|
+
concentrationUnit: 'nM',
|
|
747
|
+
responseUnit: 'area ratio',
|
|
748
|
+
instrument: 'Triple quadrupole',
|
|
749
|
+
method: 'MRM panel',
|
|
750
|
+
outputVolume: '25',
|
|
751
|
+
includeQc: false,
|
|
752
|
+
}),
|
|
753
|
+
)
|
|
754
|
+
const targetedRun = targetedTemplates['instrument-run'] as InstrumentRunTemplate
|
|
755
|
+
|
|
756
|
+
expect(targetedOptions.metabolites).toEqual(['Glucose', 'Citrate'])
|
|
757
|
+
expect(targetedOptions.standardConcentrations).toEqual([0.5, 5, 50])
|
|
758
|
+
expect(targetedOptions.outputVolume).toBe(25)
|
|
759
|
+
expect(targetedRun.data.instrument).toBe('Triple quadrupole')
|
|
760
|
+
expect(targetedRun.data.methods[0].name).toBe('MRM panel')
|
|
761
|
+
expect(targetedRun.data.items.map(item => item.kind)).toEqual([
|
|
762
|
+
'blank',
|
|
763
|
+
'standard',
|
|
764
|
+
'standard',
|
|
765
|
+
'standard',
|
|
766
|
+
'sample',
|
|
767
|
+
'sample',
|
|
768
|
+
])
|
|
769
|
+
expect(toTemplateDataFrame(targetedTemplates['assay-matrix']).columns.map(column => column.key)).toContain('citrate')
|
|
770
|
+
|
|
685
771
|
const flowOptions = bioTemplatePresetControlValuesToOptions('flow-cytometry-assay', {
|
|
686
772
|
sampleNames: 'Control, Treatment',
|
|
687
773
|
markers: 'CD3, CD19',
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
estimateSequenceFinishDate,
|
|
4
|
+
estimateSequenceRemainingSeconds,
|
|
5
|
+
formatSequenceRemaining,
|
|
6
|
+
sequenceProgressPercent,
|
|
7
|
+
} from '../../instrument'
|
|
8
|
+
import type { SequenceProgress } from '../../types'
|
|
9
|
+
|
|
10
|
+
describe('instrument sequence helpers', () => {
|
|
11
|
+
const progress: SequenceProgress = {
|
|
12
|
+
current_sample: 3,
|
|
13
|
+
total_samples: 6,
|
|
14
|
+
elapsed_seconds: 180,
|
|
15
|
+
sample_durations: [50, 70, 60],
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
it('calculates percent and remaining time from sample durations', () => {
|
|
19
|
+
expect(sequenceProgressPercent(progress)).toBe(50)
|
|
20
|
+
expect(estimateSequenceRemainingSeconds(progress)).toBe(180)
|
|
21
|
+
expect(formatSequenceRemaining(progress)).toBe('~3m left')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('falls back to elapsed average when sample durations are unavailable', () => {
|
|
25
|
+
expect(estimateSequenceRemainingSeconds({
|
|
26
|
+
current_sample: 2,
|
|
27
|
+
total_samples: 5,
|
|
28
|
+
elapsed_seconds: 120,
|
|
29
|
+
})).toBe(180)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('uses explicit finish time or builds one from remaining seconds', () => {
|
|
33
|
+
const now = new Date('2026-05-15T12:00:00.000Z')
|
|
34
|
+
|
|
35
|
+
expect(estimateSequenceFinishDate(progress, now)?.toISOString()).toBe('2026-05-15T12:03:00.000Z')
|
|
36
|
+
expect(estimateSequenceFinishDate({
|
|
37
|
+
current_sample: 1,
|
|
38
|
+
total_samples: 2,
|
|
39
|
+
estimated_finish_time: '2026-05-15T13:00:00.000Z',
|
|
40
|
+
})?.toISOString()).toBe('2026-05-15T13:00:00.000Z')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('clamps completed or over-complete sequences for display', () => {
|
|
44
|
+
expect(sequenceProgressPercent({ current_sample: 12, total_samples: 10 })).toBe(100)
|
|
45
|
+
expect(formatSequenceRemaining({ current_sample: 10, total_samples: 10 })).toBeNull()
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
basenameFromWindowsPath,
|
|
4
|
+
extractLcmsCommonPrefix,
|
|
5
|
+
extractLcmsSampleName,
|
|
6
|
+
inferLcmsPlateTypeFromWellIds,
|
|
7
|
+
lcmsWellIdFromPosition,
|
|
8
|
+
reconstructLcmsPlateCellsFromSequenceItems,
|
|
9
|
+
} from '../../lcms'
|
|
10
|
+
|
|
11
|
+
describe('lcms utilities', () => {
|
|
12
|
+
it('extracts common prefixes and sample names from Xcalibur file names', () => {
|
|
13
|
+
const prefix = extractLcmsCommonPrefix([
|
|
14
|
+
'Batch_01_NEG_S1_001',
|
|
15
|
+
'Batch_01_NEG_S2_002',
|
|
16
|
+
'Batch_01_NEG_QC_003',
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
expect(prefix).toBe('Batch_01')
|
|
20
|
+
expect(extractLcmsSampleName('Batch_01_NEG_S1_001', prefix)).toBe('S1')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('infers plate type from well geometry', () => {
|
|
24
|
+
expect(lcmsWellIdFromPosition('R:A1')).toBe('A1')
|
|
25
|
+
expect(inferLcmsPlateTypeFromWellIds(['A1', 'F9'])).toBe('54vial')
|
|
26
|
+
expect(inferLcmsPlateTypeFromWellIds(['A1', 'H12'])).toBe('96well')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('reconstructs plate cells from sequence items while skipping controls', () => {
|
|
30
|
+
const result = reconstructLcmsPlateCellsFromSequenceItems([
|
|
31
|
+
{
|
|
32
|
+
sample_type: 'Unknown',
|
|
33
|
+
file_name: 'Batch_NEG_S1_001',
|
|
34
|
+
sample_id: 'R:A1',
|
|
35
|
+
path: '',
|
|
36
|
+
instrument_method: 'C:\\Methods\\neg.meth',
|
|
37
|
+
position: 'R:A1',
|
|
38
|
+
injection_volume: 5,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
sample_type: 'Unknown',
|
|
42
|
+
file_name: 'Batch_NEG_S2_002',
|
|
43
|
+
sample_id: 'R:H12',
|
|
44
|
+
path: '',
|
|
45
|
+
instrument_method: 'C:\\Methods\\neg.meth',
|
|
46
|
+
position: 'R:H12',
|
|
47
|
+
injection_volume: 5,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
sample_type: 'Blank',
|
|
51
|
+
file_name: 'Batch_NEG_Blank_003',
|
|
52
|
+
sample_id: 'R:F9',
|
|
53
|
+
path: '',
|
|
54
|
+
instrument_method: 'C:\\Methods\\neg.meth',
|
|
55
|
+
position: 'R:F9',
|
|
56
|
+
injection_volume: 5,
|
|
57
|
+
},
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
expect(result).toEqual({
|
|
61
|
+
plateType: '96well',
|
|
62
|
+
cells: [
|
|
63
|
+
{ row: 'A', column: 1, sample_name: 'S1' },
|
|
64
|
+
{ row: 'H', column: 12, sample_name: 'S2' },
|
|
65
|
+
],
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('extracts basenames from Windows and POSIX paths', () => {
|
|
70
|
+
expect(basenameFromWindowsPath('C:\\Methods\\neg.meth')).toBe('neg.meth')
|
|
71
|
+
expect(basenameFromWindowsPath('/methods/pos.meth')).toBe('pos.meth')
|
|
72
|
+
})
|
|
73
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
canAccessAdmin,
|
|
4
|
+
canAccessByPolicy,
|
|
5
|
+
canAccessPlugin,
|
|
6
|
+
getAccessAudience,
|
|
7
|
+
hasAllPermissions,
|
|
8
|
+
hasAnyPermission,
|
|
9
|
+
isAdminUser,
|
|
10
|
+
} from '../../permissions'
|
|
11
|
+
|
|
12
|
+
describe('permission helpers', () => {
|
|
13
|
+
const member = {
|
|
14
|
+
role: 'member',
|
|
15
|
+
role_obj: {
|
|
16
|
+
slug: 'member',
|
|
17
|
+
permissions: ['experiments.view', 'plugins.configure'],
|
|
18
|
+
plugin_access: ['dose'],
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
it('treats the admin role as full access', () => {
|
|
23
|
+
const admin = { role: 'admin' }
|
|
24
|
+
|
|
25
|
+
expect(isAdminUser(admin)).toBe(true)
|
|
26
|
+
expect(getAccessAudience(admin)).toBe('admin')
|
|
27
|
+
expect(hasAllPermissions(admin, ['platform.configure'])).toBe(true)
|
|
28
|
+
expect(canAccessAdmin(admin)).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('checks role permissions and plugin access for non-admin users', () => {
|
|
32
|
+
expect(isAdminUser(member)).toBe(false)
|
|
33
|
+
expect(getAccessAudience(member)).toBe('user')
|
|
34
|
+
expect(hasAllPermissions(member, ['experiments.view'])).toBe(true)
|
|
35
|
+
expect(hasAllPermissions(member, ['platform.configure'])).toBe(false)
|
|
36
|
+
expect(hasAnyPermission(member, ['platform.configure', 'plugins.configure'])).toBe(true)
|
|
37
|
+
expect(canAccessAdmin(member)).toBe(true)
|
|
38
|
+
expect(canAccessPlugin(member, 'dose')).toBe(true)
|
|
39
|
+
expect(canAccessPlugin(member, 'reports')).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('evaluates access policies consistently', () => {
|
|
43
|
+
expect(canAccessByPolicy(member, { visibleFor: 'user' })).toBe(true)
|
|
44
|
+
expect(canAccessByPolicy(member, { visibleFor: 'admin' })).toBe(false)
|
|
45
|
+
expect(canAccessByPolicy(member, { permissions: ['experiments.view'] })).toBe(true)
|
|
46
|
+
expect(canAccessByPolicy(member, { anyPermissions: ['platform.configure', 'plugins.configure'] })).toBe(true)
|
|
47
|
+
expect(canAccessByPolicy(member, { requiresAdmin: true })).toBe(false)
|
|
48
|
+
expect(canAccessByPolicy(member, { plugin: 'reports' })).toBe(false)
|
|
49
|
+
})
|
|
50
|
+
})
|