@morscherlab/mint-sdk 1.1.4 → 1.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{EmptyState-LMPS3keb.js → EmptyState-p90quq8Z.js} +2 -2
- package/dist/{EmptyState-LMPS3keb.js.map → EmptyState-p90quq8Z.js.map} +1 -1
- package/dist/{ExperimentPopover-D6_RGzTY.js → ExperimentPopover-tLwJ7XF4.js} +3 -3
- package/dist/{ExperimentPopover-D6_RGzTY.js.map → ExperimentPopover-tLwJ7XF4.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-DcQ38zua.js → ExperimentSelectorModal-B6P2msT4.js} +12 -5
- package/dist/ExperimentSelectorModal-B6P2msT4.js.map +1 -0
- package/dist/{SettingsModal-Ccq2z-Ms.js → SettingsModal-D7LFUokT.js} +100 -156
- package/dist/SettingsModal-D7LFUokT.js.map +1 -0
- package/dist/__tests__/components/ActionMenu.test.d.ts +1 -0
- package/dist/__tests__/components/BaseModal.test.d.ts +1 -0
- package/dist/components/ActionMenu.vue.d.ts +21 -0
- package/dist/components/BaseModal.vue.d.ts +17 -1
- package/dist/components/GeneratedPluginView.vue.d.ts +3 -3
- package/dist/components/GeneratedResultRenderer.vue.d.ts +3 -3
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +8 -8
- package/dist/{components-CV5zuWa8.js → components-Dra_ac_5.js} +350 -71
- package/dist/components-Dra_ac_5.js.map +1 -0
- package/dist/composables/controlSchemaDoseDesign.d.ts +8 -1
- package/dist/composables/controlSchemaModel.d.ts +10 -1
- package/dist/composables/index.js +8 -8
- package/dist/composables/useAnalysisArtifacts.d.ts +9 -1
- package/dist/composables/useAppExperiment.d.ts +7 -0
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +9 -1
- package/dist/composables/useControlWorkspace.d.ts +9 -1
- package/dist/composables/useGeneratedAnalysis.d.ts +9 -0
- package/dist/{composables-CW8NRXLB.js → composables-CRR_JxQD.js} +4 -4
- package/dist/{composables-CW8NRXLB.js.map → composables-CRR_JxQD.js.map} +1 -1
- package/dist/{experiment-utils-7Yw-h0tD.js → experiment-utils-CLnzPPq5.js} +170 -13
- package/dist/experiment-utils-CLnzPPq5.js.map +1 -0
- package/dist/index.js +13 -13
- package/dist/install.js +7 -7
- package/dist/styles.css +764 -610
- package/dist/templates/index.js +4 -4
- package/dist/{templates-CJRFiWUX.js → templates-Cr-48-u2.js} +2 -2
- package/dist/{templates-CJRFiWUX.js.map → templates-Cr-48-u2.js.map} +1 -1
- package/dist/types/components.d.ts +32 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/{useControlSchema-BEAgLyyx.js → useControlSchema-BMVb2XIQ.js} +58 -10
- package/dist/useControlSchema-BMVb2XIQ.js.map +1 -0
- package/dist/{useDropdownState-Be-ug_ci.js → useDropdownState-DSb3BC3N.js} +2 -2
- package/dist/{useDropdownState-Be-ug_ci.js.map → useDropdownState-DSb3BC3N.js.map} +1 -1
- package/dist/{useExperimentSelector-DFg_iEHI.js → useExperimentSelector-D0pjj9WH.js} +3 -3
- package/dist/{useExperimentSelector-DFg_iEHI.js.map → useExperimentSelector-D0pjj9WH.js.map} +1 -1
- package/dist/{useFormBuilder-CR9pBApW.js → useFormBuilder-DG3HOFGA.js} +2 -2
- package/dist/{useFormBuilder-CR9pBApW.js.map → useFormBuilder-DG3HOFGA.js.map} +1 -1
- package/dist/{useProtocolTemplates-DXeIHMwV.js → useProtocolTemplates-BMt0LlXf.js} +21 -6
- package/dist/useProtocolTemplates-BMt0LlXf.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/ActionMenu.test.ts +99 -0
- package/src/__tests__/components/BaseModal.test.ts +133 -0
- package/src/__tests__/components/SettingsModal.test.ts +9 -7
- package/src/__tests__/docs/extractDocsComponents.test.ts +15 -1
- package/src/__tests__/docs/extractDocsParsing.test.ts +22 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +80 -0
- package/src/components/ActionMenu.story.vue +26 -0
- package/src/components/ActionMenu.vue +212 -0
- package/src/components/AppSidebar.vue +2 -13
- package/src/components/ArtifactSelectorModal.vue +8 -1
- package/src/components/BaseModal.story.vue +107 -1
- package/src/components/BaseModal.vue +152 -4
- package/src/components/BioTemplateExperimentWorkspaceView.vue +8 -1
- package/src/components/BioTemplatePackWorkspaceView.vue +8 -1
- package/src/components/BioTemplatePresetWorkspaceView.vue +8 -1
- package/src/components/BioTemplateRenderer.vue +8 -1
- package/src/components/ComponentBindingRenderer.vue +8 -1
- package/src/components/ControlWorkspaceView.vue +8 -1
- package/src/components/DoseDesignWorkspaceView.vue +8 -1
- package/src/components/ExperimentSelectorModal.vue +8 -1
- package/src/components/FormBuilder.vue +5 -0
- package/src/components/GeneratedPluginView.vue +12 -2
- package/src/components/GeneratedResultRenderer.vue +12 -2
- package/src/components/JobsStatusTray.vue +8 -1
- package/src/components/PluginWorkspaceView.vue +10 -1
- package/src/components/SettingsModal.vue +102 -149
- package/src/components/index.ts +1 -0
- package/src/composables/controlSchemaDoseDesign.ts +8 -1
- package/src/composables/controlSchemaModel.ts +10 -1
- package/src/composables/useAnalysisArtifacts.ts +9 -1
- package/src/composables/useAppExperiment.ts +7 -0
- package/src/composables/useBioTemplatePackWorkspace.ts +9 -1
- package/src/composables/useControlWorkspace.ts +9 -1
- package/src/composables/useGeneratedAnalysis.ts +9 -0
- package/src/styles/components/action-menu.css +59 -0
- package/src/styles/components/icon-button.css +7 -3
- package/src/styles/components/modal.css +293 -0
- package/src/styles/components/settings-modal.css +3 -273
- package/src/styles/index.css +1 -0
- package/src/types/components.ts +38 -0
- package/src/types/index.ts +3 -0
- package/dist/ExperimentSelectorModal-DcQ38zua.js.map +0 -1
- package/dist/SettingsModal-Ccq2z-Ms.js.map +0 -1
- package/dist/components-CV5zuWa8.js.map +0 -1
- package/dist/experiment-utils-7Yw-h0tD.js.map +0 -1
- package/dist/useControlSchema-BEAgLyyx.js.map +0 -1
- package/dist/useProtocolTemplates-DXeIHMwV.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.6",
|
|
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",
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { enableAutoUnmount, mount } from '@vue/test-utils'
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
+
import ActionMenu from '../../components/ActionMenu.vue'
|
|
4
|
+
|
|
5
|
+
const items = [
|
|
6
|
+
{ id: 'edit', label: 'Edit' },
|
|
7
|
+
{ id: 'delete', label: 'Delete', danger: true },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
enableAutoUnmount(afterEach)
|
|
11
|
+
|
|
12
|
+
describe('ActionMenu', () => {
|
|
13
|
+
it('should expose menu semantics on the square trigger', () => {
|
|
14
|
+
const wrapper = mount(ActionMenu, {
|
|
15
|
+
props: { label: 'More user actions', items },
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
expect(wrapper.get('button').attributes('aria-haspopup')).toBe('menu')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('should teleport the menu to the document body when opened', async () => {
|
|
22
|
+
const wrapper = mount(ActionMenu, {
|
|
23
|
+
attachTo: document.body,
|
|
24
|
+
props: { label: 'More user actions', items },
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
await wrapper.get('button').trigger('click')
|
|
28
|
+
|
|
29
|
+
expect(document.body.querySelector('.mint-action-menu__panel')).not.toBeNull()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should emit the selected action item when an item is clicked', async () => {
|
|
33
|
+
const wrapper = mount(ActionMenu, {
|
|
34
|
+
attachTo: document.body,
|
|
35
|
+
props: { label: 'More user actions', items },
|
|
36
|
+
})
|
|
37
|
+
await wrapper.get('button').trigger('click')
|
|
38
|
+
|
|
39
|
+
const menuItems = document.body.querySelectorAll<HTMLButtonElement>('.mint-action-menu__item')
|
|
40
|
+
menuItems[0]?.click()
|
|
41
|
+
await wrapper.vm.$nextTick()
|
|
42
|
+
|
|
43
|
+
expect(wrapper.emitted('select')).toEqual([[items[0]]])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should render link items as semantic anchors', async () => {
|
|
47
|
+
const wrapper = mount(ActionMenu, {
|
|
48
|
+
attachTo: document.body,
|
|
49
|
+
props: {
|
|
50
|
+
label: 'More plugin actions',
|
|
51
|
+
items: [{ id: 'docs', label: 'Documentation', href: '/docs' }],
|
|
52
|
+
},
|
|
53
|
+
})
|
|
54
|
+
await wrapper.get('button').trigger('click')
|
|
55
|
+
|
|
56
|
+
expect(document.body.querySelector('.mint-action-menu__item')?.tagName).toBe('A')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should mark destructive items with the danger treatment', async () => {
|
|
60
|
+
const wrapper = mount(ActionMenu, {
|
|
61
|
+
attachTo: document.body,
|
|
62
|
+
props: { label: 'More user actions', items },
|
|
63
|
+
})
|
|
64
|
+
await wrapper.get('button').trigger('click')
|
|
65
|
+
|
|
66
|
+
const menuItems = document.body.querySelectorAll('.mint-action-menu__item')
|
|
67
|
+
|
|
68
|
+
expect(menuItems[1]?.classList.contains('mint-action-menu__item--danger')).toBe(true)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should close an open menu when its scroll context moves', async () => {
|
|
72
|
+
const wrapper = mount(ActionMenu, {
|
|
73
|
+
attachTo: document.body,
|
|
74
|
+
props: { label: 'More user actions', items },
|
|
75
|
+
})
|
|
76
|
+
await wrapper.get('button').trigger('click')
|
|
77
|
+
|
|
78
|
+
window.dispatchEvent(new Event('scroll'))
|
|
79
|
+
await wrapper.vm.$nextTick()
|
|
80
|
+
|
|
81
|
+
expect(document.body.querySelector('.mint-action-menu__panel')).toBeNull()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('should return focus to the trigger when Escape closes the menu', async () => {
|
|
85
|
+
const wrapper = mount(ActionMenu, {
|
|
86
|
+
attachTo: document.body,
|
|
87
|
+
props: { label: 'More user actions', items },
|
|
88
|
+
})
|
|
89
|
+
const trigger = wrapper.get<HTMLButtonElement>('button')
|
|
90
|
+
await trigger.trigger('click')
|
|
91
|
+
|
|
92
|
+
document.body.querySelector<HTMLElement>('.mint-action-menu__item')?.dispatchEvent(
|
|
93
|
+
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }),
|
|
94
|
+
)
|
|
95
|
+
await wrapper.vm.$nextTick()
|
|
96
|
+
|
|
97
|
+
expect(document.activeElement).toBe(trigger.element)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import BaseModal from '../../components/BaseModal.vue'
|
|
4
|
+
import type { ModalTab } from '../../types'
|
|
5
|
+
|
|
6
|
+
const stripTabs: ModalTab[] = [
|
|
7
|
+
{ id: 'processing', label: 'Processing', meta: '6' },
|
|
8
|
+
{ id: 'peaks', label: 'Peak detection' },
|
|
9
|
+
{ id: 'correction', label: 'Correction', disabled: true },
|
|
10
|
+
{ id: 'appearance', label: 'Appearance' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
const railTabs: ModalTab[] = [
|
|
14
|
+
{ id: 'profile', label: 'Profile', icon: 'M20 21a8 8 0 0 0-16 0M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8' },
|
|
15
|
+
{ id: 'security', label: 'Security', meta: '2', description: 'Passkeys and institutional sign-in.' },
|
|
16
|
+
{ id: 'appearance', label: 'Appearance' },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
function mountModal(props: Record<string, unknown> = {}, slots: Record<string, string> = {}) {
|
|
20
|
+
return mount(BaseModal, {
|
|
21
|
+
props: {
|
|
22
|
+
modelValue: true,
|
|
23
|
+
title: 'Settings',
|
|
24
|
+
...props,
|
|
25
|
+
},
|
|
26
|
+
slots: {
|
|
27
|
+
default: '<p class="body-content">Body</p>',
|
|
28
|
+
...slots,
|
|
29
|
+
},
|
|
30
|
+
global: {
|
|
31
|
+
stubs: {
|
|
32
|
+
teleport: true,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('BaseModal', () => {
|
|
39
|
+
describe('plain layout', () => {
|
|
40
|
+
it('should not render a tab strip when layout is omitted', () => {
|
|
41
|
+
const wrapper = mountModal()
|
|
42
|
+
expect(wrapper.find('.mint-modal__tabs').exists()).toBe(false)
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('tabs layout', () => {
|
|
47
|
+
it('should render a shell-level tablist when layout is tabs', () => {
|
|
48
|
+
const wrapper = mountModal({ layout: 'tabs', tabs: stripTabs, activeTab: 'processing' })
|
|
49
|
+
expect(wrapper.find('.mint-modal__tabs[role="tablist"]').exists()).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should mark the active tab with aria-selected when a tab is active', () => {
|
|
53
|
+
const wrapper = mountModal({ layout: 'tabs', tabs: stripTabs, activeTab: 'peaks' })
|
|
54
|
+
const active = wrapper.findAll('.mint-modal__tab')[1]
|
|
55
|
+
expect(active.attributes('aria-selected')).toBe('true')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('should render the tab meta when a tab defines one', () => {
|
|
59
|
+
const wrapper = mountModal({ layout: 'tabs', tabs: stripTabs, activeTab: 'processing' })
|
|
60
|
+
expect(wrapper.find('.mint-modal__tab-meta').text()).toBe('6')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should emit update:activeTab when another tab is clicked', async () => {
|
|
64
|
+
const wrapper = mountModal({ layout: 'tabs', tabs: stripTabs, activeTab: 'processing' })
|
|
65
|
+
await wrapper.findAll('.mint-modal__tab')[1].trigger('click')
|
|
66
|
+
expect(wrapper.emitted('update:activeTab')?.at(-1)).toEqual(['peaks'])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should not emit update:activeTab when a disabled tab is clicked', async () => {
|
|
70
|
+
const wrapper = mountModal({ layout: 'tabs', tabs: stripTabs, activeTab: 'processing' })
|
|
71
|
+
await wrapper.findAll('.mint-modal__tab')[2].trigger('click')
|
|
72
|
+
expect(wrapper.emitted('update:activeTab')).toBeUndefined()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should skip disabled tabs when moving with ArrowRight', async () => {
|
|
76
|
+
const wrapper = mountModal({ layout: 'tabs', tabs: stripTabs, activeTab: 'peaks' })
|
|
77
|
+
await wrapper.findAll('.mint-modal__tab')[1].trigger('keydown.right')
|
|
78
|
+
expect(wrapper.emitted('update:activeTab')?.at(-1)).toEqual(['appearance'])
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should apply the fixed-height body modifier when layout is tabs', () => {
|
|
82
|
+
const wrapper = mountModal({ layout: 'tabs', tabs: stripTabs, activeTab: 'processing' })
|
|
83
|
+
expect(wrapper.find('.mint-modal__body').classes()).toContain('mint-modal__body--tabbed')
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('rail layout', () => {
|
|
88
|
+
it('should render a vertical tablist when layout is rail', () => {
|
|
89
|
+
const wrapper = mountModal({ layout: 'rail', tabs: railTabs, activeTab: 'profile' })
|
|
90
|
+
expect(wrapper.find('.mint-modal__rail[aria-orientation="vertical"]').exists()).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should show the active tab label as the pane title', () => {
|
|
94
|
+
const wrapper = mountModal({ layout: 'rail', tabs: railTabs, activeTab: 'security' })
|
|
95
|
+
expect(wrapper.find('.mint-modal__pane-title').text()).toBe('Security')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('should show the active tab description as the pane subtitle', () => {
|
|
99
|
+
const wrapper = mountModal({ layout: 'rail', tabs: railTabs, activeTab: 'security' })
|
|
100
|
+
expect(wrapper.find('.mint-modal__pane-subtitle').text()).toBe('Passkeys and institutional sign-in.')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('should emit update:activeTab when a rail item is clicked', async () => {
|
|
104
|
+
const wrapper = mountModal({ layout: 'rail', tabs: railTabs, activeTab: 'profile' })
|
|
105
|
+
await wrapper.findAll('.mint-modal__rail-item')[1].trigger('click')
|
|
106
|
+
expect(wrapper.emitted('update:activeTab')?.at(-1)).toEqual(['security'])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should move to the next tab when ArrowDown is pressed on a rail item', async () => {
|
|
110
|
+
const wrapper = mountModal({ layout: 'rail', tabs: railTabs, activeTab: 'profile' })
|
|
111
|
+
await wrapper.findAll('.mint-modal__rail-item')[0].trigger('keydown.down')
|
|
112
|
+
expect(wrapper.emitted('update:activeTab')?.at(-1)).toEqual(['security'])
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('should render the rail-footer slot inside the rail', () => {
|
|
116
|
+
const wrapper = mountModal(
|
|
117
|
+
{ layout: 'rail', tabs: railTabs, activeTab: 'profile' },
|
|
118
|
+
{ 'rail-footer': '<button class="rail-logout">Log out</button>' },
|
|
119
|
+
)
|
|
120
|
+
expect(wrapper.find('.mint-modal__rail .rail-logout').exists()).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('should not render the plain body when layout is rail', () => {
|
|
124
|
+
const wrapper = mountModal({ layout: 'rail', tabs: railTabs, activeTab: 'profile' })
|
|
125
|
+
expect(wrapper.find('.mint-modal__body').exists()).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('should render the default slot inside the pane body when layout is rail', () => {
|
|
129
|
+
const wrapper = mountModal({ layout: 'rail', tabs: railTabs, activeTab: 'profile' })
|
|
130
|
+
expect(wrapper.find('.mint-modal__pane-body .body-content').exists()).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -32,14 +32,16 @@ describe('SettingsModal', () => {
|
|
|
32
32
|
'tab-Advanced': '<div class="advanced-tab">Advanced Content</div>',
|
|
33
33
|
})
|
|
34
34
|
|
|
35
|
-
const tabs = wrapper.findAll('.mint-
|
|
35
|
+
const tabs = wrapper.findAll('.mint-modal__tab')
|
|
36
36
|
expect(tabs.map(tab => tab.text())).toEqual(['General', 'Advanced'])
|
|
37
|
-
expect(tabs[0].classes()).toContain('mint-
|
|
37
|
+
expect(tabs[0].classes()).toContain('mint-modal__tab--active')
|
|
38
38
|
expect(wrapper.find('.general-tab').exists()).toBe(true)
|
|
39
39
|
|
|
40
40
|
await tabs[1].trigger('click')
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
// Re-query: the teleport stub rebuilds the strip on re-render, so the
|
|
43
|
+
// pre-click wrappers point at detached nodes.
|
|
44
|
+
expect(wrapper.findAll('.mint-modal__tab')[1].classes()).toContain('mint-modal__tab--active')
|
|
43
45
|
})
|
|
44
46
|
|
|
45
47
|
it('syncs schema changes without remounting the modal', async () => {
|
|
@@ -78,7 +80,7 @@ describe('SettingsModal', () => {
|
|
|
78
80
|
await wrapper.find('input').setValue('0.5')
|
|
79
81
|
await wrapper.setProps({ schema: nextSchema })
|
|
80
82
|
|
|
81
|
-
const tabs = wrapper.findAll('.mint-
|
|
83
|
+
const tabs = wrapper.findAll('.mint-modal__tab')
|
|
82
84
|
expect(tabs.map(tab => tab.text())).toEqual(['General', 'Advanced'])
|
|
83
85
|
expect((wrapper.find('input').element as HTMLInputElement).value).toBe('0.5')
|
|
84
86
|
|
|
@@ -177,7 +179,7 @@ describe('SettingsModal', () => {
|
|
|
177
179
|
|
|
178
180
|
const wrapper = mountSettingsModal({ controls })
|
|
179
181
|
|
|
180
|
-
const tabs = wrapper.findAll('.mint-
|
|
182
|
+
const tabs = wrapper.findAll('.mint-modal__tab')
|
|
181
183
|
expect(tabs.map(tab => tab.text())).toEqual(['Parameters', 'Advanced'])
|
|
182
184
|
expect((wrapper.find('input').element as HTMLInputElement).value).toBe('0.25')
|
|
183
185
|
|
|
@@ -355,7 +357,7 @@ describe('SettingsModal', () => {
|
|
|
355
357
|
|
|
356
358
|
const adminWrapper = mountSettingsModal({ schema, userType: 'admin' })
|
|
357
359
|
|
|
358
|
-
expect(adminWrapper.findAll('.mint-
|
|
360
|
+
expect(adminWrapper.findAll('.mint-modal__tab').map(tab => tab.text())).toEqual([
|
|
359
361
|
'General',
|
|
360
362
|
'Admin',
|
|
361
363
|
])
|
|
@@ -363,7 +365,7 @@ describe('SettingsModal', () => {
|
|
|
363
365
|
expect(adminWrapper.text()).toContain('Admin Secret')
|
|
364
366
|
expect(adminWrapper.text()).not.toContain('User Note')
|
|
365
367
|
|
|
366
|
-
await adminWrapper.findAll('.mint-
|
|
368
|
+
await adminWrapper.findAll('.mint-modal__tab')[1].trigger('click')
|
|
367
369
|
|
|
368
370
|
expect(adminWrapper.text()).toContain('Admin Only')
|
|
369
371
|
})
|
|
@@ -78,7 +78,14 @@ export { default as SpecialThing } from './SpecialThing.vue'
|
|
|
78
78
|
mkdirSync(join(dir, 'src', 'components'), { recursive: true })
|
|
79
79
|
writeFileSync(componentPath, `
|
|
80
80
|
<script setup lang="ts">
|
|
81
|
-
/**
|
|
81
|
+
/**
|
|
82
|
+
* Fancy button docs.
|
|
83
|
+
*
|
|
84
|
+
* @example Recommended
|
|
85
|
+
* \`\`\`vue
|
|
86
|
+
* <FancyButton label="Save" />
|
|
87
|
+
* \`\`\`
|
|
88
|
+
*/
|
|
82
89
|
interface Props {
|
|
83
90
|
/** Visible label. */
|
|
84
91
|
label: string
|
|
@@ -114,6 +121,13 @@ defineSlots<{
|
|
|
114
121
|
group: 'base',
|
|
115
122
|
source: 'packages/sdk-frontend/src/components/FancyButton.vue',
|
|
116
123
|
description: 'Fancy button docs.',
|
|
124
|
+
examples: [
|
|
125
|
+
{
|
|
126
|
+
title: 'Recommended',
|
|
127
|
+
language: 'vue',
|
|
128
|
+
code: '<FancyButton label="Save" />',
|
|
129
|
+
},
|
|
130
|
+
],
|
|
117
131
|
props: [
|
|
118
132
|
{
|
|
119
133
|
name: 'label',
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
cleanJsdocSummary,
|
|
5
5
|
extractBalancedBraces,
|
|
6
6
|
extractBalancedParens,
|
|
7
|
+
extractJsdocExamples,
|
|
7
8
|
extractTrailingJsdoc,
|
|
8
9
|
findMatchingClose,
|
|
9
10
|
splitTopLevel,
|
|
@@ -66,4 +67,25 @@ describe('extract docs parsing helpers', () => {
|
|
|
66
67
|
|
|
67
68
|
expect(extractTrailingJsdoc(source, source.indexOf('export function'))).toContain('Selected.')
|
|
68
69
|
})
|
|
70
|
+
|
|
71
|
+
it('extracts structured examples from JSDoc fenced blocks', () => {
|
|
72
|
+
const comment = `
|
|
73
|
+
/**
|
|
74
|
+
* Component summary.
|
|
75
|
+
*
|
|
76
|
+
* @example Recommended
|
|
77
|
+
* \`\`\`vue
|
|
78
|
+
* <ExperimentSelectorModal v-model="open" />
|
|
79
|
+
* \`\`\`
|
|
80
|
+
*/
|
|
81
|
+
`
|
|
82
|
+
|
|
83
|
+
expect(extractJsdocExamples(comment)).toEqual([
|
|
84
|
+
{
|
|
85
|
+
title: 'Recommended',
|
|
86
|
+
language: 'vue',
|
|
87
|
+
code: '<ExperimentSelectorModal v-model="open" />',
|
|
88
|
+
},
|
|
89
|
+
])
|
|
90
|
+
})
|
|
69
91
|
})
|
|
@@ -3,6 +3,13 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
3
3
|
import { dirname, join, resolve } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
import { describe, expect, it } from 'vitest'
|
|
6
|
+
import { compile } from 'vue'
|
|
7
|
+
import {
|
|
8
|
+
DiagnosticCategory,
|
|
9
|
+
ModuleKind,
|
|
10
|
+
ScriptTarget,
|
|
11
|
+
transpileModule,
|
|
12
|
+
} from 'typescript'
|
|
6
13
|
|
|
7
14
|
interface FrontendManifest {
|
|
8
15
|
categories: {
|
|
@@ -10,10 +17,13 @@ interface FrontendManifest {
|
|
|
10
17
|
name: string
|
|
11
18
|
deprecated?: boolean
|
|
12
19
|
replacement?: string
|
|
20
|
+
examples?: Array<{ title: string; language: string; code: string }>
|
|
21
|
+
props?: Array<{ name: string }>
|
|
13
22
|
}>
|
|
14
23
|
composables: Array<{
|
|
15
24
|
name: string
|
|
16
25
|
members?: Array<{ name: string }>
|
|
26
|
+
examples?: Array<{ title: string; language: string; code: string }>
|
|
17
27
|
}>
|
|
18
28
|
templateExports: Array<{ name: string }>
|
|
19
29
|
templatePacks: Array<{ name: string }>
|
|
@@ -23,6 +33,18 @@ interface FrontendManifest {
|
|
|
23
33
|
|
|
24
34
|
const TEST_DIR = dirname(fileURLToPath(import.meta.url))
|
|
25
35
|
const SDK_ROOT = resolve(TEST_DIR, '../../..')
|
|
36
|
+
const CORE_USAGE_NAMES = [
|
|
37
|
+
'ArtifactSelectorModal',
|
|
38
|
+
'ControlWorkspaceView',
|
|
39
|
+
'ExperimentSelectorModal',
|
|
40
|
+
'GeneratedPluginView',
|
|
41
|
+
'GeneratedResultRenderer',
|
|
42
|
+
'JobsStatusTray',
|
|
43
|
+
'PluginWorkspaceView',
|
|
44
|
+
'useAnalysisArtifacts',
|
|
45
|
+
'useAppExperiment',
|
|
46
|
+
'usePluginJobs',
|
|
47
|
+
]
|
|
26
48
|
|
|
27
49
|
function extractFrontendManifest(): FrontendManifest {
|
|
28
50
|
const output = execFileSync('bun', ['run', 'scripts/extract-docs.ts'], {
|
|
@@ -191,6 +213,64 @@ describe('frontend docs catalog coverage', () => {
|
|
|
191
213
|
expect(missingFromManifest(documentedComponents, publicComponents)).toEqual([])
|
|
192
214
|
})
|
|
193
215
|
|
|
216
|
+
it('keeps source-owned examples for the core frontend documentation contract', () => {
|
|
217
|
+
const manifest = extractFrontendManifest()
|
|
218
|
+
const documented = [
|
|
219
|
+
...manifest.categories.components,
|
|
220
|
+
...manifest.categories.composables,
|
|
221
|
+
]
|
|
222
|
+
const byName = new Map(documented.map(item => [item.name, item]))
|
|
223
|
+
expect(CORE_USAGE_NAMES.filter(name => !byName.get(name)?.examples?.length)).toEqual([])
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('keeps core frontend examples syntactically valid', () => {
|
|
227
|
+
const manifest = extractFrontendManifest()
|
|
228
|
+
const documented = [
|
|
229
|
+
...manifest.categories.components,
|
|
230
|
+
...manifest.categories.composables,
|
|
231
|
+
]
|
|
232
|
+
const byName = new Map(documented.map(item => [item.name, item]))
|
|
233
|
+
const invalid: string[] = []
|
|
234
|
+
|
|
235
|
+
for (const name of CORE_USAGE_NAMES) {
|
|
236
|
+
for (const example of byName.get(name)?.examples ?? []) {
|
|
237
|
+
if (example.language === 'vue') {
|
|
238
|
+
try {
|
|
239
|
+
compile(example.code)
|
|
240
|
+
} catch {
|
|
241
|
+
invalid.push(name)
|
|
242
|
+
}
|
|
243
|
+
continue
|
|
244
|
+
}
|
|
245
|
+
const result = transpileModule(example.code, {
|
|
246
|
+
compilerOptions: {
|
|
247
|
+
module: ModuleKind.ESNext,
|
|
248
|
+
target: ScriptTarget.ESNext,
|
|
249
|
+
},
|
|
250
|
+
reportDiagnostics: true,
|
|
251
|
+
})
|
|
252
|
+
if (result.diagnostics?.some(diagnostic => diagnostic.category === DiagnosticCategory.Error)) {
|
|
253
|
+
invalid.push(name)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
expect(invalid).toEqual([])
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
it('documents required props for generated high-level components', () => {
|
|
262
|
+
const manifest = extractFrontendManifest()
|
|
263
|
+
const byName = new Map(manifest.categories.components.map(item => [item.name, item]))
|
|
264
|
+
|
|
265
|
+
expect({
|
|
266
|
+
GeneratedPluginView: byName.get('GeneratedPluginView')?.props?.map(prop => prop.name),
|
|
267
|
+
GeneratedResultRenderer: byName.get('GeneratedResultRenderer')?.props?.map(prop => prop.name),
|
|
268
|
+
}).toEqual({
|
|
269
|
+
GeneratedPluginView: ['baseUrl', 'showStandaloneLabel', 'transport'],
|
|
270
|
+
GeneratedResultRenderer: ['result'],
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
|
|
194
274
|
it('re-exports every public component from the root SDK entrypoint', () => {
|
|
195
275
|
const publicComponents = parseComponentExports()
|
|
196
276
|
const rootComponents = parseRootComponentExports()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import ActionMenu from './ActionMenu.vue'
|
|
3
|
+
import type { ActionMenuItem } from '../types'
|
|
4
|
+
|
|
5
|
+
const items: ActionMenuItem[] = [
|
|
6
|
+
{ id: 'edit', label: 'Edit' },
|
|
7
|
+
{ id: 'docs', label: 'Documentation ↗', href: 'https://example.com/docs', target: '_blank' },
|
|
8
|
+
{ id: 'delete', label: 'Delete', danger: true },
|
|
9
|
+
]
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<template>
|
|
13
|
+
<Story title="Action/ActionMenu">
|
|
14
|
+
<Variant title="Default">
|
|
15
|
+
<div style="min-height: 14rem; display: flex; justify-content: center; padding: 2rem;">
|
|
16
|
+
<ActionMenu label="More actions" :items="items" />
|
|
17
|
+
</div>
|
|
18
|
+
</Variant>
|
|
19
|
+
|
|
20
|
+
<Variant title="Disabled">
|
|
21
|
+
<div style="display: flex; justify-content: center; padding: 2rem;">
|
|
22
|
+
<ActionMenu label="More actions" :items="items" disabled />
|
|
23
|
+
</div>
|
|
24
|
+
</Variant>
|
|
25
|
+
</Story>
|
|
26
|
+
</template>
|