@morscherlab/mint-sdk 1.0.63 → 1.1.0-beta.10

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.
Files changed (181) hide show
  1. package/README.md +119 -6
  2. package/dist/EmptyState-DgBRoYAy.js +647 -0
  3. package/dist/EmptyState-DgBRoYAy.js.map +1 -0
  4. package/dist/{ExperimentPopover-BbPkIFsI.js → ExperimentPopover-BFw3QX8L.js} +10 -9
  5. package/dist/{ExperimentPopover-BbPkIFsI.js.map → ExperimentPopover-BFw3QX8L.js.map} +1 -1
  6. package/dist/{ExperimentSelectorModal-DZeSd-yr.js → ExperimentSelectorModal-BBWXC7KW.js} +12 -216
  7. package/dist/ExperimentSelectorModal-BBWXC7KW.js.map +1 -0
  8. package/dist/{SettingsModal-CJgLadsN.js → SettingsModal-Dpg-X1OM.js} +12 -9
  9. package/dist/{SettingsModal-CJgLadsN.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
  10. package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
  11. package/dist/__tests__/components/GeneratedPathInput.test.d.ts +1 -0
  12. package/dist/__tests__/components/GeneratedPlotlyResult.test.d.ts +1 -0
  13. package/dist/__tests__/components/GeneratedPluginView.test.d.ts +1 -0
  14. package/dist/__tests__/components/GeneratedResultRenderer.test.d.ts +1 -0
  15. package/dist/__tests__/components/InstrumentAlertLog.test.d.ts +1 -0
  16. package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
  17. package/dist/__tests__/composables/useGeneratedAnalysis.test.d.ts +1 -0
  18. package/dist/__tests__/generatedUi.test.d.ts +1 -0
  19. package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
  20. package/dist/{analysisArtifactTypes-Cu40dzSG.js → analysisArtifactTypes-ejqR8MX3.js} +1 -1
  21. package/dist/{analysisArtifactTypes-Cu40dzSG.js.map → analysisArtifactTypes-ejqR8MX3.js.map} +1 -1
  22. package/dist/{auth-DZAwEeis.js → auth-B87xM--s.js} +1 -1
  23. package/dist/{auth-DZAwEeis.js.map → auth-B87xM--s.js.map} +1 -1
  24. package/dist/chunk-BoAXSpZd.js +33 -0
  25. package/dist/components/AppSidebar.vue.d.ts +1 -1
  26. package/dist/components/AutoGroupModal.vue.d.ts +2 -2
  27. package/dist/components/BaseSelect.vue.d.ts +1 -1
  28. package/dist/components/BaseToggle.vue.d.ts +2 -0
  29. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  30. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  31. package/dist/components/ComponentBindingRenderer.vue.d.ts +1 -1
  32. package/dist/components/ConfirmDialog.vue.d.ts +1 -1
  33. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  34. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  35. package/dist/components/FileUploader.vue.d.ts +4 -2
  36. package/dist/components/FitPanel.vue.d.ts +2 -2
  37. package/dist/components/FormActions.vue.d.ts +1 -1
  38. package/dist/components/FormBuilder.vue.d.ts +1 -1
  39. package/dist/components/GeneratedPathInput.vue.d.ts +12 -0
  40. package/dist/components/GeneratedPlotlyResult.vue.d.ts +11 -0
  41. package/dist/components/GeneratedPluginView.vue.d.ts +10 -0
  42. package/dist/components/GeneratedResultRenderer.vue.d.ts +6 -0
  43. package/dist/components/JobsStatusTray.vue.d.ts +5 -1
  44. package/dist/components/LcmsSequenceTable.vue.d.ts +1 -1
  45. package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
  46. package/dist/components/SampleLegend.vue.d.ts +1 -1
  47. package/dist/components/SampleSelector.vue.d.ts +2 -2
  48. package/dist/components/SectionCard.vue.d.ts +47 -0
  49. package/dist/components/SettingsModal.vue.d.ts +1 -1
  50. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
  51. package/dist/components/index.d.ts +5 -0
  52. package/dist/components/index.js +8 -9
  53. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +2 -2
  54. package/dist/{components-DdzHh3a_.js → components-Ci_BTx00.js} +2322 -2292
  55. package/dist/components-Ci_BTx00.js.map +1 -0
  56. package/dist/composables/index.d.ts +3 -2
  57. package/dist/composables/index.js +10 -11
  58. package/dist/composables/useGeneratedAnalysis.d.ts +62 -0
  59. package/dist/composables/useJobsStatusTray.d.ts +48 -4
  60. package/dist/composables/usePlatformContext.d.ts +3 -0
  61. package/dist/composables/usePluginClient.d.ts +17 -8
  62. package/dist/{composables-BUUphBqg.js → composables-C0VEJ73O.js} +154 -38
  63. package/dist/composables-C0VEJ73O.js.map +1 -0
  64. package/dist/experiment-utils-CxkruZ3G.js +392 -0
  65. package/dist/experiment-utils-CxkruZ3G.js.map +1 -0
  66. package/dist/generated/schema.d.ts +7 -0
  67. package/dist/generated-ui/main.d.ts +0 -0
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.js +15 -18
  70. package/dist/index.js.map +1 -1
  71. package/dist/install.js +7 -8
  72. package/dist/install.js.map +1 -1
  73. package/dist/plotly.min-BR53hja4.js +180634 -0
  74. package/dist/plotly.min-BR53hja4.js.map +1 -0
  75. package/dist/stores/auth.d.ts +2 -2
  76. package/dist/stores/index.js +1 -1
  77. package/dist/styles.css +8657 -8217
  78. package/dist/tailwind.preset.d.ts +5 -0
  79. package/dist/tailwind.preset.js +7 -2
  80. package/dist/tailwind.preset.js.map +1 -1
  81. package/dist/templates/index.js +4 -3
  82. package/dist/templates/packs.d.ts +0 -1
  83. package/dist/{templates-CNbPQNID.js → templates-Cdj93S9C.js} +2 -4
  84. package/dist/templates-Cdj93S9C.js.map +1 -0
  85. package/dist/types/components.d.ts +2 -0
  86. package/dist/types/form-builder.d.ts +1 -1
  87. package/dist/types/generated-ui.d.ts +183 -0
  88. package/dist/types/index.d.ts +2 -1
  89. package/dist/types/index.js +1 -1
  90. package/dist/types/instrument.d.ts +1 -0
  91. package/dist/types/jobs.d.ts +7 -1
  92. package/dist/types/platform.d.ts +2 -0
  93. package/dist/{useControlSchema-BZNdalmL.js → useControlSchema-B_qgU4rW.js} +1882 -172
  94. package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
  95. package/dist/{useDropdownState-Bb-c_PeE.js → useDropdownState-gVaMXtwW.js} +2 -2
  96. package/dist/{useDropdownState-Bb-c_PeE.js.map → useDropdownState-gVaMXtwW.js.map} +1 -1
  97. package/dist/{useExperimentSelector-DRbj6vse.js → useExperimentSelector-BuQZRgMt.js} +4 -255
  98. package/dist/useExperimentSelector-BuQZRgMt.js.map +1 -0
  99. package/dist/{useFormBuilder-DKekvXRs.js → useFormBuilder-D1G5J5Z5.js} +2 -2
  100. package/dist/{useFormBuilder-DKekvXRs.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
  101. package/dist/{useAnalysisArtifacts-C3l0a0vu.js → useProtocolTemplates-DeaOmMaw.js} +307 -475
  102. package/dist/useProtocolTemplates-DeaOmMaw.js.map +1 -0
  103. package/package.json +7 -3
  104. package/src/__tests__/components/BaseSelect.test.ts +52 -0
  105. package/src/__tests__/components/BaseToggle.test.ts +40 -0
  106. package/src/__tests__/components/GeneratedPathInput.test.ts +132 -0
  107. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +117 -0
  108. package/src/__tests__/components/GeneratedPluginView.test.ts +240 -0
  109. package/src/__tests__/components/GeneratedResultRenderer.test.ts +93 -0
  110. package/src/__tests__/components/InstrumentAlertLog.test.ts +59 -0
  111. package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
  112. package/src/__tests__/components/SectionCard.test.ts +181 -0
  113. package/src/__tests__/composables/formBuilderRegistry.test.ts +5 -1
  114. package/src/__tests__/composables/useApi.test.ts +8 -0
  115. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +1742 -0
  116. package/src/__tests__/composables/useJobsStatusTray.test.ts +179 -1
  117. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  118. package/src/__tests__/composables/usePluginClient.test.ts +402 -21
  119. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  120. package/src/__tests__/docs/extractDocsTemplates.test.ts +0 -1
  121. package/src/__tests__/generatedUi.test.ts +119 -0
  122. package/src/__tests__/templates/templates.test.ts +1 -1
  123. package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
  124. package/src/components/BaseSelect.vue +16 -2
  125. package/src/components/BaseToggle.vue +3 -1
  126. package/src/components/FileUploader.vue +49 -11
  127. package/src/components/GeneratedPathInput.vue +218 -0
  128. package/src/components/GeneratedPlotlyResult.vue +87 -0
  129. package/src/components/GeneratedPluginView.vue +279 -0
  130. package/src/components/GeneratedResultRenderer.vue +137 -0
  131. package/src/components/InstrumentAlertLog.vue +2 -2
  132. package/src/components/JobsStatusTray.vue +14 -8
  133. package/src/components/PluginWorkspaceView.props.ts +1 -1
  134. package/src/components/PluginWorkspaceView.vue +1 -5
  135. package/src/components/SectionCard.story.vue +163 -0
  136. package/src/components/SectionCard.vue +98 -0
  137. package/src/components/index.ts +5 -0
  138. package/src/composables/formBuilderRegistry.ts +2 -0
  139. package/src/composables/index.ts +19 -0
  140. package/src/composables/useApi.ts +1 -0
  141. package/src/composables/useGeneratedAnalysis.ts +1270 -0
  142. package/src/composables/useJobsStatusTray.ts +234 -45
  143. package/src/composables/usePluginClient.ts +164 -30
  144. package/src/composables/usePluginConfig.ts +156 -20
  145. package/src/generated/schema.ts +130 -0
  146. package/src/generated-ui/main.ts +21 -0
  147. package/src/index.ts +3 -0
  148. package/src/jobs.ts +5 -0
  149. package/src/plotly-dist.d.ts +3 -0
  150. package/src/styles/components/section-card.css +161 -0
  151. package/src/styles/index.css +1 -0
  152. package/src/styles/variables.css +25 -0
  153. package/src/tailwind.preset.ts +5 -0
  154. package/src/templates/packs.ts +0 -3
  155. package/src/types/components.ts +12 -0
  156. package/src/types/form-builder.ts +1 -0
  157. package/src/types/generated-ui.ts +189 -0
  158. package/src/types/index.ts +4 -0
  159. package/src/types/instrument.ts +1 -0
  160. package/src/types/jobs.ts +10 -1
  161. package/src/types/platform.ts +2 -0
  162. package/dist/BaseButton-Dgqrze41.js +0 -74
  163. package/dist/BaseButton-Dgqrze41.js.map +0 -1
  164. package/dist/BaseModal-B9UA8Y_I.js +0 -165
  165. package/dist/BaseModal-B9UA8Y_I.js.map +0 -1
  166. package/dist/BaseSelect-ekgr9fDo.js +0 -179
  167. package/dist/BaseSelect-ekgr9fDo.js.map +0 -1
  168. package/dist/ExperimentPopover-Ryusjrhv.js +0 -4
  169. package/dist/ExperimentSelectorModal-Cg4-LXBD.js +0 -5
  170. package/dist/ExperimentSelectorModal-DZeSd-yr.js.map +0 -1
  171. package/dist/SettingsModal-Bl_9w2Y3.js +0 -5
  172. package/dist/components-DdzHh3a_.js.map +0 -1
  173. package/dist/composables-BUUphBqg.js.map +0 -1
  174. package/dist/experiment-utils-Bfa7CwPU.js +0 -108
  175. package/dist/experiment-utils-Bfa7CwPU.js.map +0 -1
  176. package/dist/templates-CNbPQNID.js.map +0 -1
  177. package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +0 -1
  178. package/dist/useControlSchema-BZNdalmL.js.map +0 -1
  179. package/dist/useEventListener-CfVkP9Xz.js +0 -57
  180. package/dist/useEventListener-CfVkP9Xz.js.map +0 -1
  181. package/dist/useExperimentSelector-DRbj6vse.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.0.63",
3
+ "version": "1.1.0-beta.10",
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",
@@ -54,7 +54,9 @@
54
54
  "src"
55
55
  ],
56
56
  "scripts": {
57
- "build": "vue-tsc --noEmit && vite build && node scripts/check-package-exports.mjs",
57
+ "build": "vue-tsc --noEmit && bun run build:library && bun run build:generated-ui && node scripts/check-package-exports.mjs",
58
+ "build:library": "vite build",
59
+ "build:generated-ui": "vite build --config vite.generated-ui.config.ts",
58
60
  "check:exports": "node scripts/check-package-exports.mjs",
59
61
  "dev": "vite build --watch",
60
62
  "typecheck": "vue-tsc --noEmit",
@@ -81,13 +83,15 @@
81
83
  },
82
84
  "dependencies": {
83
85
  "@floating-ui/dom": "^1.7.6",
84
- "axios": "1.13.6"
86
+ "axios": "1.13.6",
87
+ "plotly.js-dist-min": "^3.7.0"
85
88
  },
86
89
  "devDependencies": {
87
90
  "@histoire/plugin-vue": "1.0.0-beta.1",
88
91
  "@simplewebauthn/browser": "10.0.0",
89
92
  "@simplewebauthn/types": "10.0.0",
90
93
  "@types/node": "22.19.13",
94
+ "@types/plotly.js": "^3.0.10",
91
95
  "@vitejs/plugin-vue": "6.0.5",
92
96
  "@vue/test-utils": "2.4.6",
93
97
  "happy-dom": "15.11.7",
@@ -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
+ })
@@ -0,0 +1,132 @@
1
+ import { flushPromises, mount } from '@vue/test-utils'
2
+ import { createPinia } from 'pinia'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import ArtifactSelectorModal from '../../components/ArtifactSelectorModal.vue'
6
+ import FileUploader from '../../components/FileUploader.vue'
7
+ import GeneratedPathInput from '../../components/GeneratedPathInput.vue'
8
+ import type { AnalysisArtifactDetail } from '../../types/analysisArtifactTypes'
9
+
10
+ describe('GeneratedPathInput', () => {
11
+ beforeEach(() => {
12
+ Object.assign(window, {
13
+ __MINT_PLATFORM__: {
14
+ isIntegrated: true,
15
+ platformApiUrl: '/api',
16
+ theme: 'system',
17
+ currentExperimentId: 42,
18
+ },
19
+ })
20
+ })
21
+
22
+ afterEach(() => {
23
+ delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
24
+ vi.unstubAllGlobals()
25
+ })
26
+
27
+ it('should preserve selected directory paths for SDK staging', async () => {
28
+ const wrapper = mount(GeneratedPathInput, {
29
+ global: { plugins: [createPinia()], stubs: { Teleport: true } },
30
+ })
31
+ const files = [new File(['a'], 'a.txt'), new File(['b'], 'b.txt')]
32
+
33
+ await wrapper.findComponent(FileUploader).vm.$emit(
34
+ 'upload',
35
+ files,
36
+ 'dataset',
37
+ ['dataset/a.txt', 'dataset/sub/b.txt'],
38
+ )
39
+
40
+ expect(wrapper.emitted('update:modelValue')?.at(-1)?.[0]).toEqual({
41
+ kind: 'path',
42
+ files,
43
+ rootName: 'dataset',
44
+ relativePaths: ['dataset/a.txt', 'dataset/sub/b.txt'],
45
+ })
46
+ })
47
+
48
+ it('should emit one path selection per file when multiple paths are requested', async () => {
49
+ const wrapper = mount(GeneratedPathInput, {
50
+ props: { multiple: true },
51
+ global: { plugins: [createPinia()], stubs: { Teleport: true } },
52
+ })
53
+ const files = [new File(['a'], 'a.txt'), new File(['b'], 'b.txt')]
54
+
55
+ await wrapper.findComponent(FileUploader).vm.$emit(
56
+ 'upload',
57
+ files,
58
+ undefined,
59
+ ['a.txt', 'b.txt'],
60
+ )
61
+
62
+ expect(wrapper.emitted('update:modelValue')?.at(-1)?.[0]).toEqual([
63
+ { kind: 'path', files: [files[0]], relativePaths: ['a.txt'] },
64
+ { kind: 'path', files: [files[1]], relativePaths: ['b.txt'] },
65
+ ])
66
+ })
67
+
68
+ it('should remove a file from the submitted value when the uploader removes it', async () => {
69
+ const wrapper = mount(GeneratedPathInput, {
70
+ global: { plugins: [createPinia()], stubs: { Teleport: true } },
71
+ })
72
+ const uploader = wrapper.findComponent(FileUploader)
73
+ const file = new File(['a'], 'a.txt')
74
+
75
+ const input = uploader.get('input[type="file"]')
76
+ Object.defineProperty(input.element, 'files', { value: [file] })
77
+ await input.trigger('change')
78
+ await wrapper.setProps({
79
+ modelValue: { kind: 'path', files: [file], relativePaths: ['a.txt'] },
80
+ })
81
+ await wrapper.get('button[aria-label="Remove a.txt"]').trigger('click')
82
+
83
+ expect(wrapper.emitted('update:modelValue')?.at(-1)?.[0]).toBeNull()
84
+ })
85
+
86
+ it('should download a selected platform file artifact for staging', async () => {
87
+ const fetcher = vi.fn().mockResolvedValue(new Response('artifact-data', {
88
+ headers: { 'Content-Type': 'text/csv' },
89
+ }))
90
+ vi.stubGlobal('fetch', fetcher)
91
+ const wrapper = mount(GeneratedPathInput, {
92
+ global: { plugins: [createPinia()], stubs: { Teleport: true } },
93
+ })
94
+ const detail = {
95
+ id: 7,
96
+ experiment_id: 42,
97
+ plugin_id: 'leaf',
98
+ artifact_key: 'matrix',
99
+ display_name: 'Matrix',
100
+ status: 'active',
101
+ result_keys: ['file'],
102
+ tree: [],
103
+ summary: null,
104
+ result: {
105
+ schema_version: 'mint.analysis_file.v1',
106
+ filename: 'matrix.csv',
107
+ file: {
108
+ key: 'analysis-artifacts/matrix/v1/matrix.csv',
109
+ plugin_id: 'leaf',
110
+ experiment_id: 42,
111
+ },
112
+ },
113
+ } satisfies AnalysisArtifactDetail
114
+
115
+ wrapper.findComponent(ArtifactSelectorModal).vm.$emit('selectDetail', detail)
116
+ await flushPromises()
117
+
118
+ const selection = wrapper.emitted('update:modelValue')?.at(-1)?.[0] as {
119
+ files: File[]
120
+ relativePaths: string[]
121
+ }
122
+ expect({
123
+ url: fetcher.mock.calls[0]?.[0],
124
+ filename: selection.files[0]?.name,
125
+ relativePaths: selection.relativePaths,
126
+ }).toEqual({
127
+ url: '/api/experiments/42/objects/analysis-artifacts/matrix/v1/matrix.csv?plugin_id=leaf&format=raw',
128
+ filename: 'matrix.csv',
129
+ relativePaths: ['matrix.csv'],
130
+ })
131
+ })
132
+ })
@@ -0,0 +1,117 @@
1
+ import { flushPromises, mount, type VueWrapper } from '@vue/test-utils'
2
+ import { nextTick } from 'vue'
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import GeneratedPlotlyResult from '../../components/GeneratedPlotlyResult.vue'
6
+
7
+ const plotly = vi.hoisted(() => ({
8
+ purge: vi.fn(),
9
+ react: vi.fn(),
10
+ }))
11
+
12
+ vi.mock('plotly.js-dist-min', () => ({ default: plotly }))
13
+
14
+ async function mountAndRecover(
15
+ fallback?: { media_type: string; data: string },
16
+ ): Promise<VueWrapper> {
17
+ const wrapper = mount(GeneratedPlotlyResult, {
18
+ props: {
19
+ spec: { data: [] },
20
+ ...(fallback ? { fallback } : {}),
21
+ },
22
+ })
23
+ await flushPromises()
24
+
25
+ plotly.react.mockResolvedValue(undefined)
26
+ await wrapper.setProps({
27
+ spec: { data: [{ type: 'scatter', x: [1], y: [2] }] },
28
+ })
29
+ await flushPromises()
30
+
31
+ return wrapper
32
+ }
33
+
34
+ describe('GeneratedPlotlyResult', () => {
35
+ beforeEach(() => {
36
+ vi.clearAllMocks()
37
+ plotly.react.mockRejectedValue(new Error('<script>unsafe details</script>'))
38
+ })
39
+
40
+ it('should render a safe error when Plotly fails without a fallback', async () => {
41
+ const wrapper = mount(GeneratedPlotlyResult, {
42
+ props: { spec: { data: [] } },
43
+ })
44
+ await flushPromises()
45
+
46
+ expect(wrapper.get('[role="alert"]').text()).toBe('Unable to display this chart.')
47
+ })
48
+
49
+ it('should render the static image when Plotly fails with a fallback', async () => {
50
+ const wrapper = mount(GeneratedPlotlyResult, {
51
+ props: {
52
+ spec: { data: [] },
53
+ fallback: { media_type: 'image/png', data: 'cG5n' },
54
+ },
55
+ })
56
+ await flushPromises()
57
+
58
+ expect(wrapper.get('img').attributes('src')).toBe('data:image/png;base64,cG5n')
59
+ })
60
+
61
+ it('should not redraw an immutable result for nested spec mutations', async () => {
62
+ plotly.react.mockResolvedValue(undefined)
63
+ const wrapper = mount(GeneratedPlotlyResult, {
64
+ props: { spec: { data: [{ x: [1] }] } },
65
+ })
66
+ await flushPromises()
67
+
68
+ const spec = wrapper.props('spec') as { data: Array<{ x: number[] }> }
69
+ spec.data[0]!.x.push(2)
70
+ await nextTick()
71
+ await flushPromises()
72
+
73
+ expect(plotly.react).toHaveBeenCalledTimes(1)
74
+ })
75
+
76
+ it.each([
77
+ ['Heatmap', { type: 'heatmap', z: [[1, 2], [3, 4]] }],
78
+ ['Scatter3d', { type: 'scatter3d', x: [1], y: [2], z: [3] }],
79
+ ])('should pass a %s trace to the Plotly renderer', async (_name, trace) => {
80
+ plotly.react.mockResolvedValue(undefined)
81
+ mount(GeneratedPlotlyResult, {
82
+ props: { spec: { data: [trace] } },
83
+ })
84
+ await flushPromises()
85
+
86
+ expect(plotly.react).toHaveBeenCalledWith(
87
+ expect.any(HTMLElement),
88
+ [trace],
89
+ {},
90
+ { responsive: true, displaylogo: false },
91
+ )
92
+ })
93
+
94
+ it('should retry Plotly when a new immutable spec follows a render failure', async () => {
95
+ await mountAndRecover()
96
+
97
+ expect(plotly.react).toHaveBeenCalledTimes(2)
98
+ })
99
+
100
+ it('should remove the fallback after a new immutable spec renders successfully', async () => {
101
+ const wrapper = await mountAndRecover({ media_type: 'image/png', data: 'cG5n' })
102
+
103
+ expect(wrapper.find('img').exists()).toBe(false)
104
+ })
105
+
106
+ it('should remove the error after a new immutable spec renders successfully', async () => {
107
+ const wrapper = await mountAndRecover()
108
+
109
+ expect(wrapper.find('[role="alert"]').exists()).toBe(false)
110
+ })
111
+
112
+ it('should restore the interactive chart after a new immutable spec renders successfully', async () => {
113
+ const wrapper = await mountAndRecover()
114
+
115
+ expect(wrapper.get('.mint-generated-result__plotly').isVisible()).toBe(true)
116
+ })
117
+ })
@@ -0,0 +1,240 @@
1
+ import { flushPromises, mount } from '@vue/test-utils'
2
+ import { createPinia } from 'pinia'
3
+ import { computed, ref } from 'vue'
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
5
+
6
+ import GeneratedPluginView from '../../components/GeneratedPluginView.vue'
7
+ import GeneratedResultRenderer from '../../components/GeneratedResultRenderer.vue'
8
+ import JobsStatusTray from '../../components/JobsStatusTray.vue'
9
+ import PluginWorkspaceView from '../../components/PluginWorkspaceView.vue'
10
+
11
+ const generated = vi.hoisted(() => ({
12
+ initialize: vi.fn(),
13
+ submit: vi.fn(),
14
+ loadResult: vi.fn(),
15
+ resultForJob: vi.fn(),
16
+ options: undefined as unknown,
17
+ jobs: [] as Array<Record<string, unknown>>,
18
+ }))
19
+
20
+ vi.mock('../../composables/useGeneratedAnalysis', async () => {
21
+ const { computed, ref, shallowRef } = await import('vue')
22
+ return {
23
+ useGeneratedAnalysis: (options: unknown) => {
24
+ generated.options = options
25
+ const jobs = shallowRef(generated.jobs)
26
+ return ({
27
+ manifest: shallowRef({
28
+ schema_version: '1',
29
+ plugin: {
30
+ name: 'Demo', version: '1.0.0', description: 'Demo plugin', routes_prefix: '/demo',
31
+ },
32
+ ui: { title: 'Demo analysis', description: 'Backend-only workspace' },
33
+ jobs: [
34
+ {
35
+ id: 'run',
36
+ title: 'Run analysis',
37
+ description: 'Analyze one dataset',
38
+ cpu: 2,
39
+ input_schema: {
40
+ type: 'object',
41
+ properties: { threshold: { type: 'number', title: 'Threshold', default: 2.5 } },
42
+ },
43
+ output_schema: {},
44
+ },
45
+ {
46
+ id: 'sync',
47
+ title: 'Sync service',
48
+ description: 'Synchronize remote metadata',
49
+ cpu: 0,
50
+ lifecycle: 'service',
51
+ profile: { kind: 'service', timeoutSeconds: 60, maxConcurrency: 2 },
52
+ input_schema: { type: 'object', properties: {} },
53
+ output_schema: {},
54
+ },
55
+ ],
56
+ }),
57
+ session: shallowRef({ id: 'session-1' }),
58
+ jobs,
59
+ latestResult: computed(() => ({ kind: 'text', text: 'complete' })),
60
+ isLoading: ref(false),
61
+ isSubmitting: ref(false),
62
+ error: shallowRef(null),
63
+ initialize: generated.initialize,
64
+ submit: generated.submit,
65
+ loadResult: generated.loadResult,
66
+ resultForJob: generated.resultForJob,
67
+ refreshJobs: vi.fn(),
68
+ jobsAdapter: {},
69
+ })
70
+ },
71
+ }
72
+ })
73
+
74
+ vi.mock('../../composables/usePlatformContext', () => ({
75
+ usePlatformContext: vi.fn(() => ({
76
+ isIntegrated: computed(() => false),
77
+ context: ref({ isIntegrated: false, theme: 'system' }),
78
+ plugin: computed(() => undefined),
79
+ user: computed(() => undefined),
80
+ theme: computed(() => 'system'),
81
+ features: computed(() => undefined),
82
+ navigate: vi.fn(),
83
+ notify: vi.fn(),
84
+ sendToPlatform: vi.fn(),
85
+ })),
86
+ }))
87
+
88
+ vi.mock('../../composables/useTheme', () => ({
89
+ useTheme: vi.fn(() => ({ isDark: ref(false), theme: ref('light'), toggleTheme: vi.fn() })),
90
+ }))
91
+
92
+ describe('GeneratedPluginView', () => {
93
+ beforeEach(() => {
94
+ vi.clearAllMocks()
95
+ const baseJob = {
96
+ kind: 'analysis',
97
+ name: 'Run analysis',
98
+ status: 'completed',
99
+ progress: {
100
+ current_file: 0, total_files: 0, current_filename: null, percent: 100, message: '', stage: 'completed',
101
+ },
102
+ queue_position: 0,
103
+ warnings: [],
104
+ error: null,
105
+ output_path: null,
106
+ save_path: null,
107
+ session_id: 'session-1',
108
+ folders: [],
109
+ folder_path: null,
110
+ user_id: 'user-1',
111
+ owner_user_id: 'user-1',
112
+ experiment_id: null,
113
+ extraction_result_id: null,
114
+ metadata: { definition_id: 'run', has_result: true },
115
+ created_at: '',
116
+ started_at: '',
117
+ completed_at: '',
118
+ can_cancel: false,
119
+ can_delete: true,
120
+ }
121
+ generated.jobs = [
122
+ { ...baseJob, id: 'job-1', job_id: 'job-1' },
123
+ { ...baseJob, id: 'job-2', job_id: 'job-2', session_id: 'shared-session' },
124
+ ]
125
+ generated.initialize.mockResolvedValue(undefined)
126
+ generated.submit.mockResolvedValue(undefined)
127
+ generated.loadResult.mockResolvedValue({ kind: 'text', text: 'complete' })
128
+ generated.resultForJob.mockReturnValue({ kind: 'text', text: 'complete' })
129
+ })
130
+
131
+ it('composes the selected B workspace from SDK-owned layout, form, jobs, and results', async () => {
132
+ const wrapper = mount(GeneratedPluginView, {
133
+ props: { baseUrl: '/api/demo' },
134
+ global: {
135
+ plugins: [createPinia()],
136
+ stubs: {
137
+ 'router-link': { template: '<a><slot /></a>' },
138
+ Teleport: true,
139
+ },
140
+ },
141
+ })
142
+ await flushPromises()
143
+
144
+ expect({
145
+ workspace: wrapper.findComponent(PluginWorkspaceView).props(),
146
+ result: wrapper.findComponent(GeneratedResultRenderer).text(),
147
+ jobs: wrapper.findComponent(JobsStatusTray).exists(),
148
+ jobSource: wrapper.findComponent(JobsStatusTray).props('source'),
149
+ legacyJobs: wrapper.findComponent(JobsStatusTray).props('jobs'),
150
+ legacyAdapter: wrapper.findComponent(JobsStatusTray).props('adapter'),
151
+ }).toMatchObject({
152
+ workspace: {
153
+ title: 'Demo analysis',
154
+ subtitle: 'Backend-only workspace',
155
+ sidebarPosition: 'left',
156
+ showFormActions: true,
157
+ values: { threshold: 2.5 },
158
+ },
159
+ result: 'complete',
160
+ jobs: true,
161
+ jobSource: expect.any(Object),
162
+ legacyJobs: undefined,
163
+ legacyAdapter: undefined,
164
+ })
165
+ })
166
+
167
+ it('submits normalized form values through the current job definition', async () => {
168
+ const wrapper = mount(GeneratedPluginView, {
169
+ props: { baseUrl: '/api/demo' },
170
+ global: { plugins: [createPinia()], stubs: { 'router-link': true, Teleport: true } },
171
+ })
172
+ await flushPromises()
173
+
174
+ wrapper.findComponent(PluginWorkspaceView).vm.$emit('form-submit', 'run', { threshold: 4 })
175
+ await flushPromises()
176
+
177
+ expect(generated.submit).toHaveBeenCalledWith('run', { threshold: 4 })
178
+ })
179
+
180
+ it('describes host service jobs without a misleading CPU reservation', async () => {
181
+ const wrapper = mount(GeneratedPluginView, {
182
+ props: { baseUrl: '/api/demo' },
183
+ global: { plugins: [createPinia()], stubs: { 'router-link': true, Teleport: true } },
184
+ })
185
+ await flushPromises()
186
+
187
+ wrapper.findComponent(PluginWorkspaceView).vm.$emit('update:activeView', 'sync')
188
+ await flushPromises()
189
+
190
+ expect(wrapper.find('[aria-label="Job resource requirements"]').text()).toBe('Host service')
191
+ })
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
+
203
+ it('should pass the host transport to the generated analysis composable', async () => {
204
+ const transport = vi.fn()
205
+ mount(GeneratedPluginView, {
206
+ props: { baseUrl: '/api/demo', transport },
207
+ global: { plugins: [createPinia()], stubs: { 'router-link': true, Teleport: true } },
208
+ })
209
+ await flushPromises()
210
+
211
+ expect(generated.options).toMatchObject({ baseUrl: '/api/demo', transport })
212
+ })
213
+
214
+ it('should lazily load the current job result by job id', async () => {
215
+ mount(GeneratedPluginView, {
216
+ props: { baseUrl: '/api/demo' },
217
+ global: { plugins: [createPinia()], stubs: { 'router-link': true, Teleport: true } },
218
+ })
219
+ await flushPromises()
220
+
221
+ expect(generated.loadResult).toHaveBeenCalledWith(expect.objectContaining({ job_id: 'job-1' }))
222
+ })
223
+
224
+ it('should display the exact historical job selected from the jobs tray', async () => {
225
+ generated.resultForJob.mockImplementation(jobId => ({ kind: 'text', text: String(jobId) }))
226
+ const wrapper = mount(GeneratedPluginView, {
227
+ props: { baseUrl: '/api/demo' },
228
+ global: { plugins: [createPinia()], stubs: { 'router-link': true, Teleport: true } },
229
+ })
230
+ await flushPromises()
231
+
232
+ wrapper.findComponent(JobsStatusTray).vm.$emit('select', generated.jobs[1])
233
+ await flushPromises()
234
+
235
+ expect({
236
+ result: wrapper.findComponent(GeneratedResultRenderer).text(),
237
+ loadedJob: generated.loadResult.mock.calls.at(-1)?.[0]?.job_id,
238
+ }).toEqual({ result: 'job-2', loadedJob: 'job-2' })
239
+ })
240
+ })