@morscherlab/mint-sdk 1.1.0-beta.2 → 1.1.0-beta.20
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 +18 -4
- package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
- package/dist/EmptyState-DgBRoYAy.js.map +1 -0
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
- package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-Dpg-X1OM.js} +3 -3
- package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
- package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
- package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
- package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
- package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -0
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
- package/dist/components/SectionCard.vue.d.ts +47 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +6 -6
- package/dist/{components-UfkPN1bK.js → components-Ci_BTx00.js} +711 -633
- package/dist/components-Ci_BTx00.js.map +1 -0
- package/dist/composables/analysisArtifactTarget.d.ts +5 -0
- package/dist/composables/index.d.ts +3 -1
- package/dist/composables/index.js +8 -8
- package/dist/composables/platformContextHelpers.d.ts +2 -0
- package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
- package/dist/composables/useGeneratedAnalysis.d.ts +1 -1
- package/dist/composables/usePluginClient.d.ts +11 -5
- package/dist/{composables-B512bWJP.js → composables-C0VEJ73O.js} +40 -5
- package/dist/composables-C0VEJ73O.js.map +1 -0
- package/dist/index.js +11 -11
- package/dist/install.js +5 -5
- package/dist/styles.css +314 -17
- package/dist/tailwind.preset.d.ts +5 -0
- package/dist/tailwind.preset.js +7 -2
- package/dist/tailwind.preset.js.map +1 -1
- package/dist/templates/index.js +3 -3
- package/dist/{templates-Cttbjbyc.js → templates-Cdj93S9C.js} +2 -2
- package/dist/{templates-Cttbjbyc.js.map → templates-Cdj93S9C.js.map} +1 -1
- package/dist/types/analysisArtifactTypes.d.ts +8 -0
- package/dist/types/components.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +13 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/{useControlSchema-Dallso-L.js → useControlSchema-B_qgU4rW.js} +48 -19
- package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
- package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
- package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
- package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-D1G5J5Z5.js} +2 -2
- package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
- package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-DeaOmMaw.js} +4 -4
- package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-DeaOmMaw.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/BaseSelect.test.ts +52 -0
- package/src/__tests__/components/BaseToggle.test.ts +40 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +10 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
- package/src/__tests__/components/SectionCard.test.ts +181 -0
- package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +2 -0
- package/src/__tests__/composables/usePluginClient.test.ts +36 -1
- package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
- package/src/components/BaseSelect.vue +16 -2
- package/src/components/BaseToggle.vue +3 -1
- package/src/components/GeneratedPluginView.vue +1 -1
- package/src/components/PluginWorkspaceView.props.ts +1 -1
- package/src/components/PluginWorkspaceView.vue +1 -5
- package/src/components/SectionCard.story.vue +163 -0
- package/src/components/SectionCard.vue +98 -0
- package/src/components/index.ts +1 -0
- package/src/composables/analysisArtifactTarget.ts +53 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/platformContextHelpers.ts +10 -3
- package/src/composables/useAnalysisArtifacts.ts +48 -0
- package/src/composables/useGeneratedAnalysis.ts +30 -12
- package/src/composables/usePluginClient.ts +12 -5
- package/src/styles/components/button.css +1 -3
- package/src/styles/components/section-card.css +161 -0
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +25 -0
- package/src/tailwind.preset.ts +5 -0
- package/src/types/analysisArtifactTypes.ts +9 -0
- package/src/types/components.ts +12 -0
- package/src/types/generated-ui.ts +13 -0
- package/src/types/index.ts +3 -0
- package/dist/EmptyState-D1c4RdC7.js.map +0 -1
- package/dist/components-UfkPN1bK.js.map +0 -1
- package/dist/composables-B512bWJP.js.map +0 -1
- package/dist/useControlSchema-Dallso-L.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
3
|
+
"version": "1.1.0-beta.20",
|
|
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",
|
|
@@ -3,6 +3,58 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import BaseSelect from '../../components/BaseSelect.vue'
|
|
4
4
|
|
|
5
5
|
describe('BaseSelect', () => {
|
|
6
|
+
it('forwards native attributes to the select instead of the wrapper', () => {
|
|
7
|
+
const wrapper = mount(BaseSelect, {
|
|
8
|
+
props: {
|
|
9
|
+
options: ['linear'],
|
|
10
|
+
},
|
|
11
|
+
attrs: {
|
|
12
|
+
id: 'curve-model',
|
|
13
|
+
name: 'curve-model',
|
|
14
|
+
'aria-label': 'Curve model',
|
|
15
|
+
'data-testid': 'curve-model-select',
|
|
16
|
+
class: 'curve-model-layout',
|
|
17
|
+
style: 'width: 12rem;',
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const select = wrapper.get('select')
|
|
22
|
+
|
|
23
|
+
expect({
|
|
24
|
+
select: {
|
|
25
|
+
id: select.attributes('id'),
|
|
26
|
+
name: select.attributes('name'),
|
|
27
|
+
ariaLabel: select.attributes('aria-label'),
|
|
28
|
+
testId: select.attributes('data-testid'),
|
|
29
|
+
layoutClass: select.classes().includes('curve-model-layout'),
|
|
30
|
+
},
|
|
31
|
+
wrapper: {
|
|
32
|
+
id: wrapper.attributes('id'),
|
|
33
|
+
name: wrapper.attributes('name'),
|
|
34
|
+
ariaLabel: wrapper.attributes('aria-label'),
|
|
35
|
+
testId: wrapper.attributes('data-testid'),
|
|
36
|
+
layoutClass: wrapper.classes().includes('curve-model-layout'),
|
|
37
|
+
style: wrapper.attributes('style'),
|
|
38
|
+
},
|
|
39
|
+
}).toEqual({
|
|
40
|
+
select: {
|
|
41
|
+
id: 'curve-model',
|
|
42
|
+
name: 'curve-model',
|
|
43
|
+
ariaLabel: 'Curve model',
|
|
44
|
+
testId: 'curve-model-select',
|
|
45
|
+
layoutClass: false,
|
|
46
|
+
},
|
|
47
|
+
wrapper: {
|
|
48
|
+
id: undefined,
|
|
49
|
+
name: undefined,
|
|
50
|
+
ariaLabel: undefined,
|
|
51
|
+
testId: undefined,
|
|
52
|
+
layoutClass: true,
|
|
53
|
+
style: 'width: 12rem;',
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
6
58
|
it('accepts string and number shorthand options', async () => {
|
|
7
59
|
const wrapper = mount(BaseSelect, {
|
|
8
60
|
props: {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import BaseToggle from '../../components/BaseToggle.vue'
|
|
4
|
+
|
|
5
|
+
describe('BaseToggle', () => {
|
|
6
|
+
it('uses ariaLabel for the switch name without changing the visual label', () => {
|
|
7
|
+
const wrapper = mount(BaseToggle, {
|
|
8
|
+
props: {
|
|
9
|
+
label: 'Enabled',
|
|
10
|
+
ariaLabel: 'Email notifications',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect({
|
|
15
|
+
accessibleName: wrapper.get('[role="switch"]').attributes('aria-label'),
|
|
16
|
+
visualText: wrapper.text(),
|
|
17
|
+
wrapperAriaLabel: wrapper.attributes('aria-label'),
|
|
18
|
+
}).toEqual({
|
|
19
|
+
accessibleName: 'Email notifications',
|
|
20
|
+
visualText: 'Enabled',
|
|
21
|
+
wrapperAriaLabel: undefined,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('keeps using the visual label as the default switch name', () => {
|
|
26
|
+
const wrapper = mount(BaseToggle, {
|
|
27
|
+
props: {
|
|
28
|
+
label: 'Enabled',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(wrapper.get('[role="switch"]').attributes('aria-label')).toBe('Enabled')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps the existing fallback name when no labels are provided', () => {
|
|
36
|
+
const wrapper = mount(BaseToggle)
|
|
37
|
+
|
|
38
|
+
expect(wrapper.get('[role="switch"]').attributes('aria-label')).toBe('Toggle')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -190,6 +190,16 @@ describe('GeneratedPluginView', () => {
|
|
|
190
190
|
expect(wrapper.find('[aria-label="Job resource requirements"]').text()).toBe('Host service')
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
+
it('describes process CPU declarations as an upper bound', async () => {
|
|
194
|
+
const wrapper = mount(GeneratedPluginView, {
|
|
195
|
+
props: { baseUrl: '/api/demo' },
|
|
196
|
+
global: { plugins: [createPinia()], stubs: { 'router-link': true, Teleport: true } },
|
|
197
|
+
})
|
|
198
|
+
await flushPromises()
|
|
199
|
+
|
|
200
|
+
expect(wrapper.find('[aria-label="Job resource requirements"]').text()).toBe('Up to 2 CPU')
|
|
201
|
+
})
|
|
202
|
+
|
|
193
203
|
it('should pass the host transport to the generated analysis composable', async () => {
|
|
194
204
|
const transport = vi.fn()
|
|
195
205
|
mount(GeneratedPluginView, {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
|
+
import type { VueWrapper } from '@vue/test-utils'
|
|
2
3
|
import { createPinia } from 'pinia'
|
|
3
4
|
import { computed, h, nextTick, ref } from 'vue'
|
|
4
5
|
import { describe, expect, it, vi } from 'vitest'
|
|
@@ -10,7 +11,7 @@ import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vu
|
|
|
10
11
|
import PluginWorkspaceView from '../../components/PluginWorkspaceView.vue'
|
|
11
12
|
import { defineDoseDesignControlModel } from '../../composables/useControlSchema'
|
|
12
13
|
import { usePlatformContext } from '../../composables/usePlatformContext'
|
|
13
|
-
import type { PillNavItem, SidebarToolSection } from '../../types'
|
|
14
|
+
import type { PageSelectorItemInput, PillNavItem, SidebarToolSection } from '../../types'
|
|
14
15
|
|
|
15
16
|
vi.mock('../../composables/usePlatformContext', () => ({
|
|
16
17
|
usePlatformContext: vi.fn(() => ({
|
|
@@ -79,6 +80,44 @@ const globalOptions = {
|
|
|
79
80
|
},
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
interface PageSelectorWorkspaceProps {
|
|
84
|
+
pageSelector?: PageSelectorItemInput[]
|
|
85
|
+
currentPageSelectorId?: string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function mountIntegratedPageSelectorWorkspace(
|
|
89
|
+
props: PageSelectorWorkspaceProps = {},
|
|
90
|
+
): VueWrapper {
|
|
91
|
+
vi.mocked(usePlatformContext).mockReturnValueOnce({
|
|
92
|
+
isIntegrated: computed(() => true),
|
|
93
|
+
context: ref({ isIntegrated: true, theme: 'system' }),
|
|
94
|
+
plugin: computed(() => ({
|
|
95
|
+
id: 'workspace-plugin',
|
|
96
|
+
name: 'Workspace Plugin',
|
|
97
|
+
version: '1.0.0',
|
|
98
|
+
route_prefix: '/workspace-plugin',
|
|
99
|
+
api_prefix: '/api/workspace-plugin',
|
|
100
|
+
nav_items: [
|
|
101
|
+
{ path: '/platform-one', label: 'Platform One' },
|
|
102
|
+
{ path: '/platform-two', label: 'Platform Two' },
|
|
103
|
+
],
|
|
104
|
+
})),
|
|
105
|
+
user: computed(() => undefined),
|
|
106
|
+
theme: computed(() => 'system' as const),
|
|
107
|
+
features: computed(() => undefined),
|
|
108
|
+
navigate: vi.fn(),
|
|
109
|
+
notify: vi.fn(),
|
|
110
|
+
sendToPlatform: vi.fn(),
|
|
111
|
+
})
|
|
112
|
+
return mount(PluginWorkspaceView, {
|
|
113
|
+
props: {
|
|
114
|
+
title: 'Workspace',
|
|
115
|
+
...props,
|
|
116
|
+
},
|
|
117
|
+
global: globalOptions,
|
|
118
|
+
})
|
|
119
|
+
}
|
|
120
|
+
|
|
82
121
|
describe('PluginWorkspaceView', () => {
|
|
83
122
|
it('renders the standard AppLayout, AppTopBar, and AppSidebar plugin shell', () => {
|
|
84
123
|
const wrapper = mount(PluginWorkspaceView, {
|
|
@@ -146,22 +185,37 @@ describe('PluginWorkspaceView', () => {
|
|
|
146
185
|
expect(wrapper.emitted('pill-select')).toEqual([[{ id: 'results', label: 'Results' }]])
|
|
147
186
|
})
|
|
148
187
|
|
|
149
|
-
it('
|
|
150
|
-
const wrapper =
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
188
|
+
it('should use platform pages when pageSelector is omitted', () => {
|
|
189
|
+
const wrapper = mountIntegratedPageSelectorWorkspace()
|
|
190
|
+
|
|
191
|
+
expect(wrapper.find('.mint-page-selector').exists()).toBe(true)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('should suppress platform pages when pageSelector is explicitly empty', () => {
|
|
195
|
+
const wrapper = mountIntegratedPageSelectorWorkspace({ pageSelector: [] })
|
|
196
|
+
|
|
197
|
+
expect(wrapper.find('.mint-page-selector').exists()).toBe(false)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should hide one explicit page without falling back to platform pages', () => {
|
|
201
|
+
const wrapper = mountIntegratedPageSelectorWorkspace({
|
|
202
|
+
pageSelector: [{ id: 'downloads', label: 'Downloads', to: '/' }],
|
|
203
|
+
currentPageSelectorId: 'downloads',
|
|
160
204
|
})
|
|
161
205
|
|
|
162
|
-
expect(wrapper.findComponent(AppTopBar).props('pageSelector')).toBeUndefined()
|
|
163
206
|
expect(wrapper.find('.mint-page-selector').exists()).toBe(false)
|
|
164
|
-
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should render multiple explicit pages ahead of platform pages', () => {
|
|
210
|
+
const wrapper = mountIntegratedPageSelectorWorkspace({
|
|
211
|
+
pageSelector: [
|
|
212
|
+
{ id: 'overview', label: 'Overview', to: '/' },
|
|
213
|
+
{ id: 'reports', label: 'Reports', to: '/reports' },
|
|
214
|
+
],
|
|
215
|
+
currentPageSelectorId: 'overview',
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(wrapper.get('.mint-page-selector__label').text()).toBe('Overview')
|
|
165
219
|
})
|
|
166
220
|
|
|
167
221
|
it('passes compact control models to the sidebar for generated forms', () => {
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import SectionCard from '../../components/SectionCard.vue'
|
|
4
|
+
|
|
5
|
+
describe('SectionCard', () => {
|
|
6
|
+
describe('basic rendering', () => {
|
|
7
|
+
it('should render container, header, and title when given the required title prop', () => {
|
|
8
|
+
const wrapper = mount(SectionCard, {
|
|
9
|
+
props: { title: 'Experiments' },
|
|
10
|
+
})
|
|
11
|
+
expect(wrapper.find('.mint-section-card').exists()).toBe(true)
|
|
12
|
+
expect(wrapper.find('.mint-section-card__header').exists()).toBe(true)
|
|
13
|
+
expect(wrapper.find('.mint-section-card__title').text()).toBe('Experiments')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should render default slot content in the body', () => {
|
|
17
|
+
const wrapper = mount(SectionCard, {
|
|
18
|
+
props: { title: 'Experiments' },
|
|
19
|
+
slots: { default: '<p class="body-probe">rows</p>' },
|
|
20
|
+
})
|
|
21
|
+
expect(wrapper.find('.mint-section-card__body .body-probe').exists()).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
describe('count meta', () => {
|
|
26
|
+
it('should render the count in the header when provided', () => {
|
|
27
|
+
const wrapper = mount(SectionCard, {
|
|
28
|
+
props: { title: 'Plugins', count: '10 enabled' },
|
|
29
|
+
})
|
|
30
|
+
expect(wrapper.find('.mint-section-card__count').text()).toBe('10 enabled')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not render a count element when count is omitted', () => {
|
|
34
|
+
const wrapper = mount(SectionCard, {
|
|
35
|
+
props: { title: 'Plugins' },
|
|
36
|
+
})
|
|
37
|
+
expect(wrapper.find('.mint-section-card__count').exists()).toBe(false)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('should accept a numeric count', () => {
|
|
41
|
+
const wrapper = mount(SectionCard, {
|
|
42
|
+
props: { title: 'Artifacts', count: 4 },
|
|
43
|
+
})
|
|
44
|
+
expect(wrapper.find('.mint-section-card__count').text()).toBe('4')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('title style variants', () => {
|
|
49
|
+
it('should apply the uppercase modifier when uppercase is true', () => {
|
|
50
|
+
const wrapper = mount(SectionCard, {
|
|
51
|
+
props: { title: 'Experiments', uppercase: true },
|
|
52
|
+
})
|
|
53
|
+
expect(wrapper.find('.mint-section-card__title--uppercase').exists()).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('should not apply the uppercase modifier by default', () => {
|
|
57
|
+
const wrapper = mount(SectionCard, {
|
|
58
|
+
props: { title: 'Design data' },
|
|
59
|
+
})
|
|
60
|
+
expect(wrapper.find('.mint-section-card__title--uppercase').exists()).toBe(false)
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
describe('icon chip', () => {
|
|
65
|
+
it('should render an icon chip with an SVG path when icon is provided', () => {
|
|
66
|
+
const wrapper = mount(SectionCard, {
|
|
67
|
+
props: { title: 'Experiments', icon: 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7' },
|
|
68
|
+
})
|
|
69
|
+
expect(wrapper.find('.mint-section-card__icon-chip').exists()).toBe(true)
|
|
70
|
+
expect(wrapper.find('.mint-section-card__icon-chip path').exists()).toBe(true)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should render one path per entry when icon is an array of paths', () => {
|
|
74
|
+
const wrapper = mount(SectionCard, {
|
|
75
|
+
props: { title: 'Experiments', icon: ['M4 7v10', 'M4 7c0 2.21'] },
|
|
76
|
+
})
|
|
77
|
+
expect(wrapper.findAll('.mint-section-card__icon-chip path')).toHaveLength(2)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should not render an icon chip when icon is omitted', () => {
|
|
81
|
+
const wrapper = mount(SectionCard, {
|
|
82
|
+
props: { title: 'Experiments' },
|
|
83
|
+
})
|
|
84
|
+
expect(wrapper.find('.mint-section-card__icon-chip').exists()).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should apply the accent modifier class matching the accent prop', () => {
|
|
88
|
+
const wrapper = mount(SectionCard, {
|
|
89
|
+
props: { title: 'Projects', icon: 'M3 7v10', accent: 'project' },
|
|
90
|
+
})
|
|
91
|
+
expect(wrapper.find('.mint-section-card__icon-chip--project').exists()).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should default to the primary accent when accent is omitted', () => {
|
|
95
|
+
const wrapper = mount(SectionCard, {
|
|
96
|
+
props: { title: 'Projects', icon: 'M3 7v10' },
|
|
97
|
+
})
|
|
98
|
+
expect(wrapper.find('.mint-section-card__icon-chip--primary').exists()).toBe(true)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('header divider', () => {
|
|
103
|
+
it('should use the strong divider by default', () => {
|
|
104
|
+
const wrapper = mount(SectionCard, {
|
|
105
|
+
props: { title: 'Experiments' },
|
|
106
|
+
})
|
|
107
|
+
expect(wrapper.find('.mint-section-card__header--divider-light').exists()).toBe(false)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should apply the light divider modifier when divider is light', () => {
|
|
111
|
+
const wrapper = mount(SectionCard, {
|
|
112
|
+
props: { title: 'Design data', divider: 'light' },
|
|
113
|
+
})
|
|
114
|
+
expect(wrapper.find('.mint-section-card__header--divider-light').exists()).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('fill mode', () => {
|
|
119
|
+
it('should not stretch by default', () => {
|
|
120
|
+
const wrapper = mount(SectionCard, {
|
|
121
|
+
props: { title: 'Experiments' },
|
|
122
|
+
})
|
|
123
|
+
expect(wrapper.find('.mint-section-card--fill').exists()).toBe(false)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should apply the fill modifier when fill is true', () => {
|
|
127
|
+
const wrapper = mount(SectionCard, {
|
|
128
|
+
props: { title: 'Experiments', fill: true },
|
|
129
|
+
})
|
|
130
|
+
expect(wrapper.find('.mint-section-card--fill').exists()).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('should render the toolbar slot outside the scrolling body', () => {
|
|
134
|
+
const wrapper = mount(SectionCard, {
|
|
135
|
+
props: { title: 'Experiments', fill: true },
|
|
136
|
+
slots: { toolbar: '<div class="toolbar-probe">chips</div>', default: '<p>rows</p>' },
|
|
137
|
+
})
|
|
138
|
+
expect(wrapper.find('.mint-section-card__toolbar .toolbar-probe').exists()).toBe(true)
|
|
139
|
+
expect(wrapper.find('.mint-section-card__body .toolbar-probe').exists()).toBe(false)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should not render a toolbar region when the slot is unused', () => {
|
|
143
|
+
const wrapper = mount(SectionCard, {
|
|
144
|
+
props: { title: 'Experiments' },
|
|
145
|
+
})
|
|
146
|
+
expect(wrapper.find('.mint-section-card__toolbar').exists()).toBe(false)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('should keep the header, body and footer structure when filling', () => {
|
|
150
|
+
const wrapper = mount(SectionCard, {
|
|
151
|
+
props: { title: 'Experiments', fill: true },
|
|
152
|
+
slots: { default: '<p>rows</p>', footer: '<span>1 / 3</span>' },
|
|
153
|
+
})
|
|
154
|
+
expect(wrapper.find('.mint-section-card__header').exists()).toBe(true)
|
|
155
|
+
expect(wrapper.find('.mint-section-card__body').exists()).toBe(true)
|
|
156
|
+
expect(wrapper.find('.mint-section-card__footer').exists()).toBe(true)
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('slots', () => {
|
|
161
|
+
it('should render the actions slot at the end of the header', () => {
|
|
162
|
+
const wrapper = mount(SectionCard, {
|
|
163
|
+
props: { title: 'Experiments' },
|
|
164
|
+
slots: { actions: '<a class="actions-probe" href="#">All →</a>' },
|
|
165
|
+
})
|
|
166
|
+
expect(wrapper.find('.mint-section-card__header .actions-probe').exists()).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('should render the footer strip only when the footer slot is provided', () => {
|
|
170
|
+
const withFooter = mount(SectionCard, {
|
|
171
|
+
props: { title: 'Experiments' },
|
|
172
|
+
slots: { footer: '<span class="footer-probe">Showing 1–18 of 42</span>' },
|
|
173
|
+
})
|
|
174
|
+
const withoutFooter = mount(SectionCard, {
|
|
175
|
+
props: { title: 'Experiments' },
|
|
176
|
+
})
|
|
177
|
+
expect(withFooter.find('.mint-section-card__footer .footer-probe').exists()).toBe(true)
|
|
178
|
+
expect(withoutFooter.find('.mint-section-card__footer').exists()).toBe(false)
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildAnalysisArtifactTargetQuery,
|
|
5
|
+
parseAnalysisArtifactTarget,
|
|
6
|
+
} from '../../composables/analysisArtifactTarget'
|
|
7
|
+
import {
|
|
8
|
+
currentExperimentIdFromUrl,
|
|
9
|
+
resolveCurrentPluginId,
|
|
10
|
+
} from '../../composables/platformContextHelpers'
|
|
11
|
+
|
|
12
|
+
describe('analysis artifact target', () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
|
|
15
|
+
vi.unstubAllGlobals()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should resolve the canonical injected plugin id', () => {
|
|
19
|
+
;(window as unknown as { __MINT_PLATFORM__: unknown }).__MINT_PLATFORM__ = {
|
|
20
|
+
isIntegrated: true,
|
|
21
|
+
plugin: { id: 'LEAF', name: 'Leaf UI' },
|
|
22
|
+
theme: 'system',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
expect(resolveCurrentPluginId()).toBe('LEAF')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should tolerate a host that provides a partial location object', () => {
|
|
29
|
+
vi.stubGlobal('window', {
|
|
30
|
+
location: { search: '' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(currentExperimentIdFromUrl()).toBeUndefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should build the canonical readable query when an artifact id hint is present', () => {
|
|
37
|
+
const query = buildAnalysisArtifactTargetQuery({
|
|
38
|
+
experimentId: 66,
|
|
39
|
+
artifactPluginId: 'LEAF',
|
|
40
|
+
artifactKey: 'exp316_260703_ys_rna_ide_scr',
|
|
41
|
+
artifactId: 206,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(query.toString()).toBe(
|
|
45
|
+
'experiment_id=66&artifact_plugin_id=LEAF'
|
|
46
|
+
+ '&artifact_key=exp316_260703_ys_rna_ide_scr&artifact_id=206',
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should parse the canonical identity without requiring an artifact id hint', () => {
|
|
51
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
52
|
+
'experiment_id=66&artifact_plugin_id=LEAF&artifact_key=result',
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
expect(target).toEqual({
|
|
56
|
+
experimentId: 66,
|
|
57
|
+
artifactPluginId: 'LEAF',
|
|
58
|
+
artifactKey: 'result',
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should reject an incomplete target instead of guessing another artifact', () => {
|
|
63
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
64
|
+
'experiment_id=66&artifact_key=result',
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
expect(target).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should reject non-positive numeric identifiers', () => {
|
|
71
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
72
|
+
'experiment_id=0&artifact_plugin_id=LEAF&artifact_key=result&artifact_id=-1',
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
expect(target).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -19,7 +19,10 @@ vi.mock('../../composables/useApi', () => ({
|
|
|
19
19
|
}))
|
|
20
20
|
|
|
21
21
|
import { useAnalysisArtifacts } from '../../composables/useAnalysisArtifacts'
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
AnalysisArtifactDetail,
|
|
24
|
+
AnalysisArtifactSummary,
|
|
25
|
+
} from '../../types/analysisArtifactTypes'
|
|
23
26
|
|
|
24
27
|
// ---------------------------------------------------------------------------
|
|
25
28
|
// Fixtures
|
|
@@ -177,6 +180,76 @@ describe('useAnalysisArtifacts', () => {
|
|
|
177
180
|
})
|
|
178
181
|
})
|
|
179
182
|
|
|
183
|
+
describe('resolveTarget', () => {
|
|
184
|
+
it('should resolve the exact stable identity when the numeric hint is stale', async () => {
|
|
185
|
+
const detail = {
|
|
186
|
+
...makeArtifact({
|
|
187
|
+
id: 2,
|
|
188
|
+
experiment_id: 42,
|
|
189
|
+
plugin_id: 'plugin-b',
|
|
190
|
+
artifact_key: 'peaks',
|
|
191
|
+
}),
|
|
192
|
+
result: { schema_version: 'mint.analysis_file.v1' },
|
|
193
|
+
tree: [],
|
|
194
|
+
summary: null,
|
|
195
|
+
} satisfies AnalysisArtifactDetail
|
|
196
|
+
mockGet
|
|
197
|
+
.mockResolvedValueOnce({ artifacts: ARTIFACTS })
|
|
198
|
+
.mockResolvedValueOnce(detail)
|
|
199
|
+
const { resolveTarget } = setup()
|
|
200
|
+
|
|
201
|
+
const resolved = await resolveTarget({
|
|
202
|
+
experimentId: 42,
|
|
203
|
+
artifactPluginId: 'plugin-b',
|
|
204
|
+
artifactKey: 'peaks',
|
|
205
|
+
artifactId: 999,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect({
|
|
209
|
+
resolvedId: resolved?.id,
|
|
210
|
+
detailPath: mockGet.mock.calls[1]?.[0],
|
|
211
|
+
}).toEqual({
|
|
212
|
+
resolvedId: 2,
|
|
213
|
+
detailPath: '/experiments/42/analysis-artifacts/2',
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should fail explicitly when the stable identity is not available', async () => {
|
|
218
|
+
const { resolveTarget, error } = setup()
|
|
219
|
+
|
|
220
|
+
const resolved = await resolveTarget({
|
|
221
|
+
experimentId: 42,
|
|
222
|
+
artifactPluginId: 'LEAF',
|
|
223
|
+
artifactKey: 'missing',
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect({ resolved, error: error.value }).toEqual({
|
|
227
|
+
resolved: null,
|
|
228
|
+
error: 'Analysis artifact LEAF/missing is not available.',
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should reject a target from another experiment before making a request', async () => {
|
|
233
|
+
const { resolveTarget, error } = setup()
|
|
234
|
+
|
|
235
|
+
const resolved = await resolveTarget({
|
|
236
|
+
experimentId: 66,
|
|
237
|
+
artifactPluginId: 'LEAF',
|
|
238
|
+
artifactKey: 'result',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
expect({
|
|
242
|
+
resolved,
|
|
243
|
+
error: error.value,
|
|
244
|
+
requests: mockGet.mock.calls,
|
|
245
|
+
}).toEqual({
|
|
246
|
+
resolved: null,
|
|
247
|
+
error: 'Analysis artifact target belongs to experiment 66, not 42.',
|
|
248
|
+
requests: [],
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
180
253
|
// -------------------------------------------------------------------------
|
|
181
254
|
// experiment id resolution
|
|
182
255
|
// -------------------------------------------------------------------------
|
|
@@ -407,6 +407,7 @@ describe('useGeneratedAnalysis', () => {
|
|
|
407
407
|
uploadUrl: upload?.[0],
|
|
408
408
|
uploadMethod: upload?.[1]?.method,
|
|
409
409
|
uploadSessionHeader: new Headers(upload?.[1]?.headers).get('X-Mint-Job-Session'),
|
|
410
|
+
uploadDefinitionHeader: new Headers(upload?.[1]?.headers).get('X-Mint-Job-Definition'),
|
|
410
411
|
sessionId: form.get('session_id'),
|
|
411
412
|
relativePaths: form.getAll('relative_paths'),
|
|
412
413
|
submittedSource: submit.inputs.source,
|
|
@@ -414,6 +415,7 @@ describe('useGeneratedAnalysis', () => {
|
|
|
414
415
|
uploadUrl: '/api/demo/jobs/uploads',
|
|
415
416
|
uploadMethod: 'POST',
|
|
416
417
|
uploadSessionHeader: 'session-1',
|
|
418
|
+
uploadDefinitionHeader: 'run',
|
|
417
419
|
sessionId: 'session-1',
|
|
418
420
|
relativePaths: ['data.csv'],
|
|
419
421
|
submittedSource: { token: 'upload-token' },
|
|
@@ -22,12 +22,37 @@ import { AuthenticationRequiredError } from '../../composables/useApi'
|
|
|
22
22
|
import { useAuthStore } from '../../stores/auth'
|
|
23
23
|
|
|
24
24
|
const contract: PluginContract = {
|
|
25
|
-
schemaVersion:
|
|
25
|
+
schemaVersion: 2,
|
|
26
26
|
plugin: {
|
|
27
27
|
routesPrefix: '/drp',
|
|
28
28
|
apiPrefix: '/api/drp',
|
|
29
29
|
type: 'analysis',
|
|
30
30
|
},
|
|
31
|
+
settings: {
|
|
32
|
+
modelName: 'PluginSettings',
|
|
33
|
+
valueSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {
|
|
36
|
+
threshold: { type: 'number', default: 0.05 },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
formSchema: {
|
|
40
|
+
groups: [
|
|
41
|
+
{
|
|
42
|
+
id: 'settings',
|
|
43
|
+
label: 'Settings',
|
|
44
|
+
fields: [
|
|
45
|
+
{
|
|
46
|
+
name: 'threshold',
|
|
47
|
+
label: 'Threshold',
|
|
48
|
+
type: 'number',
|
|
49
|
+
defaultValue: 0.05,
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
},
|
|
31
56
|
endpoints: [],
|
|
32
57
|
hash: 'sha256:test',
|
|
33
58
|
}
|
|
@@ -818,6 +843,16 @@ describe('usePluginClient', () => {
|
|
|
818
843
|
})
|
|
819
844
|
})
|
|
820
845
|
|
|
846
|
+
it('should preserve the contract v2 form schema in settingsConfig', () => {
|
|
847
|
+
const settings = usePluginSettings({
|
|
848
|
+
apiBaseUrl: '/api/drp',
|
|
849
|
+
loadOnMount: false,
|
|
850
|
+
schema: contract.settings.formSchema,
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
expect(settings.settingsConfig.value.schema).toBe(contract.settings.formSchema)
|
|
854
|
+
})
|
|
855
|
+
|
|
821
856
|
it('should use the platform facade for an explicit different plugin name', async () => {
|
|
822
857
|
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
823
858
|
isIntegrated: true,
|