@morscherlab/mint-sdk 1.2.0-beta.9 → 1.2.1

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 (215) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DHE9k8TP.js} +5 -5
  5. package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
  6. package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DOcCf0Vo.js} +41 -18
  7. package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
  8. package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-jhF9wUkU.js} +19 -9
  9. package/dist/Skeleton-jhF9wUkU.js.map +1 -0
  10. package/dist/__tests__/apiErrors.test.d.ts +1 -0
  11. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  12. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  14. package/dist/__tests__/components/FormField.test.d.ts +1 -0
  15. package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
  16. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  17. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  18. package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  20. package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
  21. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  22. package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
  23. package/dist/apiErrors.d.ts +26 -0
  24. package/dist/components/AppSidebar.vue.d.ts +7 -7
  25. package/dist/components/BaseInput.vue.d.ts +1 -1
  26. package/dist/components/BasePill.vue.d.ts +3 -0
  27. package/dist/components/BaseSelect.vue.d.ts +1 -1
  28. package/dist/components/BaseTabs.vue.d.ts +1 -4
  29. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  30. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  31. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  32. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  33. package/dist/components/DatePicker.vue.d.ts +1 -1
  34. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  35. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  36. package/dist/components/FilePicker.vue.d.ts +32 -0
  37. package/dist/components/FileUploader.vue.d.ts +8 -2
  38. package/dist/components/FormField.vue.d.ts +8 -1
  39. package/dist/components/FormulaInput.vue.d.ts +1 -1
  40. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  41. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  42. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  43. package/dist/components/NumberInput.vue.d.ts +1 -1
  44. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  45. package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
  46. package/dist/components/ResourceCard.vue.d.ts +1 -1
  47. package/dist/components/SecretInput.vue.d.ts +1 -1
  48. package/dist/components/SequenceInput.vue.d.ts +1 -1
  49. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  50. package/dist/components/TagsInput.vue.d.ts +1 -1
  51. package/dist/components/TimePicker.vue.d.ts +1 -1
  52. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  53. package/dist/components/UnitInput.vue.d.ts +1 -1
  54. package/dist/components/WellPlate.vue.d.ts +1 -1
  55. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  56. package/dist/components/index.d.ts +4 -0
  57. package/dist/components/index.js +8 -8
  58. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  59. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  60. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  61. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  62. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  63. package/dist/{components-CgAUzF8F.js → components-Cp8DcNrU.js} +4363 -1730
  64. package/dist/components-Cp8DcNrU.js.map +1 -0
  65. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  66. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  67. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  68. package/dist/composables/filePicker/validation.d.ts +5 -0
  69. package/dist/composables/index.d.ts +2 -0
  70. package/dist/composables/index.js +10 -10
  71. package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
  72. package/dist/composables/useApi.d.ts +14 -7
  73. package/dist/composables/useFileBrowser.d.ts +55 -0
  74. package/dist/composables/useFilePicker.d.ts +189 -0
  75. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  76. package/dist/composables/useToast.d.ts +22 -1
  77. package/dist/{composables-B_pia3hO.js → composables-D_4qqOlp.js} +300 -11
  78. package/dist/composables-D_4qqOlp.js.map +1 -0
  79. package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
  80. package/dist/errors-BLuqMee5.js.map +1 -0
  81. package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
  82. package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
  83. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  84. package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.js +15 -15
  87. package/dist/install.js +2 -2
  88. package/dist/stores/index.js +2 -2
  89. package/dist/styles.css +2957 -1708
  90. package/dist/templates/index.js +2 -2
  91. package/dist/{templates-Qq113pyZ.js → templates-Dd9bBSs6.js} +2 -2
  92. package/dist/{templates-Qq113pyZ.js.map → templates-Dd9bBSs6.js.map} +1 -1
  93. package/dist/types/components.d.ts +21 -0
  94. package/dist/types/fileBrowserTypes.d.ts +77 -0
  95. package/dist/types/filePicker.d.ts +77 -0
  96. package/dist/types/form-builder.d.ts +2 -0
  97. package/dist/types/generated-ui.d.ts +2 -0
  98. package/dist/types/index.d.ts +3 -1
  99. package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BB2GRPhW.js} +190 -105
  100. package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
  101. package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
  102. package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
  103. package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-G_CAOFTX.js} +4 -4
  104. package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
  105. package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-BnGSdSRG.js} +2 -2
  106. package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
  107. package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-CTxuzLIX.js} +70 -13
  108. package/dist/validation-CTxuzLIX.js.map +1 -0
  109. package/package.json +1 -1
  110. package/src/__tests__/apiErrors.test.ts +81 -0
  111. package/src/__tests__/components/AppSidebar.test.ts +84 -94
  112. package/src/__tests__/components/AppToastContainer.test.ts +55 -1
  113. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
  114. package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
  115. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
  116. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  117. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  118. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  119. package/src/__tests__/components/FilePicker.test.ts +487 -0
  120. package/src/__tests__/components/FormBuilder.test.ts +26 -0
  121. package/src/__tests__/components/FormField.test.ts +26 -0
  122. package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
  123. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  124. package/src/__tests__/components/NumberInput.test.ts +105 -0
  125. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  126. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
  127. package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
  128. package/src/__tests__/components/SegmentedControl.test.ts +19 -0
  129. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  130. package/src/__tests__/composables/localFiles.test.ts +57 -0
  131. package/src/__tests__/composables/useApi.test.ts +92 -9
  132. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  133. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  134. package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
  135. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  136. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  137. package/src/__tests__/generatedUi.test.ts +111 -0
  138. package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
  139. package/src/apiErrors.ts +85 -0
  140. package/src/components/AppLayout.story.vue +1 -1
  141. package/src/components/AppSidebar.story.vue +7 -10
  142. package/src/components/AppSidebar.vue +178 -58
  143. package/src/components/AppToastContainer.vue +65 -23
  144. package/src/components/BasePill.story.vue +2 -0
  145. package/src/components/BasePill.vue +4 -0
  146. package/src/components/BaseTabs.story.vue +0 -42
  147. package/src/components/BaseTabs.vue +3 -6
  148. package/src/components/ExperimentPopover.vue +10 -2
  149. package/src/components/FileBrowserModal.story.vue +203 -0
  150. package/src/components/FileBrowserModal.vue +384 -0
  151. package/src/components/FilePicker.story.vue +44 -0
  152. package/src/components/FilePicker.vue +284 -0
  153. package/src/components/FileUploader.vue +42 -15
  154. package/src/components/FormField.vue +10 -3
  155. package/src/components/GeneratedPathInput.vue +11 -7
  156. package/src/components/GeneratedPlotlyResult.vue +34 -69
  157. package/src/components/GeneratedResultRenderer.vue +0 -5
  158. package/src/components/NumberInput.story.vue +32 -13
  159. package/src/components/NumberInput.vue +116 -55
  160. package/src/components/PlotlyChart.story.vue +61 -0
  161. package/src/components/PlotlyChart.vue +233 -0
  162. package/src/components/PluginWorkspaceView.props.ts +3 -3
  163. package/src/components/SegmentedControl.vue +2 -2
  164. package/src/components/fileBrowserPresentation.ts +39 -0
  165. package/src/components/index.ts +5 -0
  166. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  167. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  168. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  169. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  170. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  171. package/src/components/internal/FormFieldRendererInternal.vue +17 -0
  172. package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
  173. package/src/composables/filePicker/localFiles.ts +65 -0
  174. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  175. package/src/composables/filePicker/usePickerSelection.ts +270 -0
  176. package/src/composables/filePicker/validation.ts +41 -0
  177. package/src/composables/index.ts +7 -0
  178. package/src/composables/pluginWorkspaceModel.ts +0 -3
  179. package/src/composables/useApi.ts +57 -13
  180. package/src/composables/useFileBrowser.ts +314 -0
  181. package/src/composables/useFilePicker.ts +366 -0
  182. package/src/composables/useGeneratedAnalysis.ts +10 -17
  183. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  184. package/src/composables/usePluginClient.ts +3 -0
  185. package/src/composables/usePluginWorkspace.ts +1 -2
  186. package/src/composables/useToast.ts +21 -5
  187. package/src/generated/schema.ts +58 -18
  188. package/src/index.ts +3 -0
  189. package/src/styles/components/app-sidebar.css +282 -121
  190. package/src/styles/components/file-browser-modal.css +489 -0
  191. package/src/styles/components/file-picker.css +502 -0
  192. package/src/styles/components/form-builder.css +1 -1
  193. package/src/styles/components/form-field.css +28 -0
  194. package/src/styles/components/icon-button.css +9 -0
  195. package/src/styles/components/number-input.css +73 -91
  196. package/src/styles/components/pill.css +49 -51
  197. package/src/styles/components/segmented-control.css +16 -11
  198. package/src/styles/components/tabs.css +18 -34
  199. package/src/styles/components/toast.css +136 -27
  200. package/src/styles/index.css +3 -0
  201. package/src/styles/variables.css +25 -5
  202. package/src/types/components.ts +23 -0
  203. package/src/types/fileBrowserTypes.ts +85 -0
  204. package/src/types/filePicker.ts +72 -0
  205. package/src/types/form-builder.ts +2 -0
  206. package/src/types/generated-ui.ts +2 -0
  207. package/src/types/index.ts +16 -0
  208. package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
  209. package/dist/SettingsModal-Dphh3RrI.js.map +0 -1
  210. package/dist/Skeleton-DxtwJ5gt.js.map +0 -1
  211. package/dist/components-CgAUzF8F.js.map +0 -1
  212. package/dist/composables-B_pia3hO.js.map +0 -1
  213. package/dist/errors-_W0-LbA6.js.map +0 -1
  214. package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
  215. package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { mount } from '@vue/test-utils'
3
+ import NumberInput from '../../components/NumberInput.vue'
4
+
5
+ function lastEmitted(wrapper: ReturnType<typeof mount>) {
6
+ return wrapper.emitted('update:modelValue')?.at(-1)?.[0]
7
+ }
8
+
9
+ describe('NumberInput', () => {
10
+ it('should never render a range input, with or without min/max', () => {
11
+ for (const props of [{}, { min: 0 }, { max: 10 }, { min: 0, max: 10 }]) {
12
+ const wrapper = mount(NumberInput, { props: { modelValue: 5, ...props } })
13
+ expect(wrapper.find('input[type="range"]').exists()).toBe(false)
14
+ }
15
+ })
16
+
17
+ it('should draw the range fill only when both min and max are set', () => {
18
+ expect(mount(NumberInput, { props: { modelValue: 5, min: 0 } }).find('.mint-number-input__rule').exists()).toBe(false)
19
+ const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
20
+ expect(wrapper.find('.mint-number-input__rule').exists()).toBe(true)
21
+ expect(wrapper.find('.mint-number-input__scrub').attributes('style')).toContain('--mint-number-input-fill: 50%')
22
+ })
23
+
24
+ it('should expose the scrub area as a slider when min and max are set', () => {
25
+ const scrub = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } }).find('.mint-number-input__scrub')
26
+ expect(scrub.attributes('role')).toBe('slider')
27
+ expect(scrub.attributes('aria-valuenow')).toBe('5')
28
+ expect(scrub.attributes('tabindex')).toBe('0')
29
+ })
30
+
31
+ it('should always render both steppers', () => {
32
+ const wrapper = mount(NumberInput, { props: { modelValue: 5 } })
33
+ expect(wrapper.findAll('.mint-number-input__stepper')).toHaveLength(2)
34
+ })
35
+
36
+ it('should disable the steppers at min and max', () => {
37
+ const atMin = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 10 } })
38
+ expect(atMin.find('.mint-number-input__stepper--down').attributes('disabled')).toBeDefined()
39
+ const atMax = mount(NumberInput, { props: { modelValue: 10, min: 0, max: 10 } })
40
+ expect(atMax.find('.mint-number-input__stepper--up').attributes('disabled')).toBeDefined()
41
+ })
42
+
43
+ it('should emit the step-snapped value when dragging at 25% of the scrub width', async () => {
44
+ const wrapper = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 50, step: 0.5 } })
45
+ const scrub = wrapper.find('.mint-number-input__scrub')
46
+ scrub.element.getBoundingClientRect = () => ({ left: 100, width: 200 } as DOMRect)
47
+ await scrub.trigger('pointerdown', { clientX: 150, pointerId: 1 })
48
+ expect(lastEmitted(wrapper)).toBe(12.5)
49
+ })
50
+
51
+ it('should not scrub when the pointer goes down on the input itself', async () => {
52
+ const wrapper = mount(NumberInput, { props: { modelValue: 0, min: 0, max: 50 } })
53
+ await wrapper.find('input').trigger('pointerdown', { clientX: 150, pointerId: 1 })
54
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
55
+ })
56
+
57
+ it('should step with the arrow keys', async () => {
58
+ const wrapper = mount(NumberInput, { props: { modelValue: 5, step: 2 } })
59
+ await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
60
+ expect(lastEmitted(wrapper)).toBe(7)
61
+ await wrapper.find('input').trigger('keydown', { key: 'ArrowDown' })
62
+ expect(lastEmitted(wrapper)).toBe(3)
63
+ })
64
+
65
+ it('should step by ten steps with PageUp and PageDown', async () => {
66
+ const wrapper = mount(NumberInput, { props: { modelValue: 50, step: 1, min: 0, max: 100 } })
67
+ await wrapper.find('input').trigger('keydown', { key: 'PageUp' })
68
+ expect(lastEmitted(wrapper)).toBe(60)
69
+ await wrapper.find('input').trigger('keydown', { key: 'PageDown' })
70
+ expect(lastEmitted(wrapper)).toBe(40)
71
+ })
72
+
73
+ it('should jump to min and max with Home and End on the slider surface', async () => {
74
+ const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
75
+ const scrub = wrapper.find('.mint-number-input__scrub')
76
+ await scrub.trigger('keydown', { key: 'Home' })
77
+ expect(lastEmitted(wrapper)).toBe(0)
78
+ await scrub.trigger('keydown', { key: 'End' })
79
+ expect(lastEmitted(wrapper)).toBe(10)
80
+ })
81
+
82
+ it('should leave Home and End to the caret inside the text input', async () => {
83
+ const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
84
+ await wrapper.find('input').trigger('keydown', { key: 'Home' })
85
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
86
+ })
87
+
88
+ it('should emit once per arrow press in slider mode', async () => {
89
+ const wrapper = mount(NumberInput, { props: { modelValue: 5, min: 0, max: 10 } })
90
+ await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
91
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1)
92
+ })
93
+
94
+ it('should keep a fractional value precision when stepping by an integer', async () => {
95
+ const wrapper = mount(NumberInput, { props: { modelValue: 2.5, step: 1 } })
96
+ await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
97
+ expect(lastEmitted(wrapper)).toBe(3.5)
98
+ })
99
+
100
+ it('should trim float noise when stepping by a decimal', async () => {
101
+ const wrapper = mount(NumberInput, { props: { modelValue: 0.1, step: 0.2 } })
102
+ await wrapper.find('input').trigger('keydown', { key: 'ArrowUp' })
103
+ expect(lastEmitted(wrapper)).toBe(0.3)
104
+ })
105
+ })
@@ -0,0 +1,305 @@
1
+ import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
2
+ import { reactive } from 'vue'
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
4
+
5
+ import PlotlyChart from '../../components/PlotlyChart.vue'
6
+
7
+ const plotly = vi.hoisted(() => ({
8
+ Plots: { resize: vi.fn() },
9
+ purge: vi.fn(),
10
+ react: vi.fn(),
11
+ }))
12
+
13
+ vi.mock('plotly.js-dist-min', () => ({ default: plotly }))
14
+ enableAutoUnmount(afterEach)
15
+
16
+ describe('PlotlyChart', () => {
17
+ beforeEach(() => {
18
+ vi.clearAllMocks()
19
+ plotly.react.mockReset().mockResolvedValue(undefined)
20
+ plotly.Plots.resize.mockReset().mockResolvedValue(undefined)
21
+ document.documentElement.classList.remove('dark')
22
+ })
23
+
24
+ afterEach(() => {
25
+ vi.unstubAllGlobals()
26
+ document.documentElement.classList.remove('dark')
27
+ document.documentElement.removeAttribute('style')
28
+ })
29
+
30
+ it('should merge MINT defaults with native Plotly props', async () => {
31
+ const trace = { type: 'scatter' as const, x: [1], y: [2] }
32
+ mount(PlotlyChart, {
33
+ props: {
34
+ data: [trace],
35
+ layout: { title: { text: 'QC' } },
36
+ config: { toImageButtonOptions: { filename: 'qc-export' } },
37
+ },
38
+ })
39
+ await flushPromises()
40
+
41
+ expect(plotly.react).toHaveBeenCalledWith(
42
+ expect.any(HTMLElement),
43
+ [trace],
44
+ expect.objectContaining({
45
+ autosize: true,
46
+ title: { text: 'QC' },
47
+ paper_bgcolor: 'rgba(0,0,0,0)',
48
+ plot_bgcolor: 'rgba(0,0,0,0)',
49
+ }),
50
+ expect.objectContaining({
51
+ responsive: true,
52
+ displaylogo: false,
53
+ toImageButtonOptions: { filename: 'qc-export' },
54
+ }),
55
+ )
56
+ })
57
+
58
+ it('should render the shared chart empty state when explicitly empty', async () => {
59
+ const wrapper = mount(PlotlyChart, {
60
+ props: { data: [], empty: true, emptyMessage: 'No peaks found' },
61
+ })
62
+ await flushPromises()
63
+
64
+ expect(wrapper.text()).toContain('No peaks found')
65
+ })
66
+
67
+ it('should show a safe error when Plotly rejects rendering', async () => {
68
+ plotly.react.mockRejectedValue(new Error('<script>unsafe details</script>'))
69
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
70
+ await flushPromises()
71
+
72
+ expect(wrapper.get('[role="alert"]').text()).toBe('Unable to display this chart.')
73
+ })
74
+
75
+ it('should rerender when immutable native props change', async () => {
76
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
77
+ await flushPromises()
78
+
79
+ await wrapper.setProps({ data: [{ x: [3], y: [4] }] })
80
+ await flushPromises()
81
+
82
+ expect(plotly.react).toHaveBeenCalledTimes(2)
83
+ })
84
+
85
+ it('should rerender when the MINT dark theme changes', async () => {
86
+ let themeChanged: MutationCallback | undefined
87
+ vi.stubGlobal('MutationObserver', class {
88
+ constructor(callback: MutationCallback) {
89
+ themeChanged = callback
90
+ }
91
+
92
+ observe(): void {}
93
+ disconnect(): void {}
94
+ takeRecords(): MutationRecord[] { return [] }
95
+ })
96
+ mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
97
+ await flushPromises()
98
+
99
+ document.documentElement.classList.add('dark')
100
+ themeChanged?.([], {} as MutationObserver)
101
+ await flushPromises()
102
+
103
+ expect(plotly.react).toHaveBeenCalledTimes(2)
104
+ })
105
+
106
+ it('should resize the Plotly canvas when its container changes size', async () => {
107
+ let resize: ResizeObserverCallback | undefined
108
+ vi.stubGlobal('ResizeObserver', class {
109
+ constructor(callback: ResizeObserverCallback) {
110
+ resize = callback
111
+ }
112
+
113
+ observe(): void {}
114
+ disconnect(): void {}
115
+ })
116
+ mount(PlotlyChart, { props: { data: [{ x: [1], y: [2] }] } })
117
+ await flushPromises()
118
+
119
+ resize?.([], {} as ResizeObserver)
120
+
121
+ expect(plotly.Plots.resize).toHaveBeenCalledTimes(1)
122
+ })
123
+
124
+ it('should observe the Plotly canvas after leaving the empty state', async () => {
125
+ const observe = vi.fn()
126
+ vi.stubGlobal('ResizeObserver', class {
127
+ observe = observe
128
+ disconnect(): void {}
129
+ })
130
+ const wrapper = mount(PlotlyChart, {
131
+ props: { data: [], empty: true },
132
+ })
133
+ await flushPromises()
134
+
135
+ await wrapper.setProps({ empty: false })
136
+ await flushPromises()
137
+
138
+ expect(observe).toHaveBeenCalledWith(expect.any(HTMLElement))
139
+ })
140
+
141
+ it('should render once when data arrives as loading completes', async () => {
142
+ const wrapper = mount(PlotlyChart, {
143
+ props: { data: [], loading: true },
144
+ })
145
+ await flushPromises()
146
+
147
+ await wrapper.setProps({ data: [{ x: [1], y: [2] }], loading: false })
148
+ await flushPromises()
149
+
150
+ expect(plotly.react).toHaveBeenCalledTimes(1)
151
+ })
152
+
153
+ it('should serialize renders and use the latest props after an in-flight render', async () => {
154
+ let finish!: () => void
155
+ plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
156
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
157
+ await flushPromises()
158
+ await wrapper.setProps({ data: [{ x: [2] }] })
159
+ await wrapper.setProps({ data: [{ x: [3] }] })
160
+ await flushPromises()
161
+ expect(plotly.react).toHaveBeenCalledTimes(1)
162
+
163
+ finish()
164
+ await flushPromises()
165
+ expect(plotly.react).toHaveBeenCalledTimes(2)
166
+ expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [3] }])
167
+ })
168
+
169
+ it('should skip rendering when unmounted before the initial DOM update', async () => {
170
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
171
+ wrapper.unmount()
172
+ await flushPromises()
173
+ expect(plotly.react).not.toHaveBeenCalled()
174
+ })
175
+
176
+ it('should recover from a stale failure without overwriting the newer render', async () => {
177
+ let reject!: (error: Error) => void
178
+ plotly.react.mockImplementationOnce(() => new Promise<void>((_resolve, fail) => { reject = fail }))
179
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
180
+ await flushPromises()
181
+ await wrapper.setProps({ data: [{ x: [2] }] })
182
+ reject(new Error('Old render failed'))
183
+ await flushPromises()
184
+ expect(plotly.react).toHaveBeenCalledTimes(2)
185
+ expect(plotly.react.mock.lastCall?.[1]).toEqual([{ x: [2] }])
186
+ expect(wrapper.find('[role="alert"]').exists()).toBe(false)
187
+ })
188
+
189
+ it('should purge an in-flight canvas removed by loading and then render its replacement', async () => {
190
+ let finish!: () => void
191
+ plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
192
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
193
+ await flushPromises()
194
+ const element = wrapper.get('.mint-plotly-chart__plot').element
195
+ await wrapper.setProps({ loading: true })
196
+ await wrapper.setProps({ loading: false, data: [{ x: [3] }] })
197
+ finish()
198
+ await flushPromises()
199
+ expect(plotly.purge).toHaveBeenCalledWith(element)
200
+ expect(plotly.react).toHaveBeenCalledTimes(2)
201
+ expect(plotly.react.mock.lastCall?.[0]).toBe(wrapper.get('.mint-plotly-chart__plot').element)
202
+ expect(plotly.react.mock.lastCall?.[0]).not.toBe(element)
203
+ })
204
+
205
+ it('should clean up a render that completes after unmount', async () => {
206
+ let finish!: () => void
207
+ plotly.react.mockImplementationOnce(() => new Promise<void>((resolve) => { finish = resolve }))
208
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
209
+ await flushPromises()
210
+ const element = wrapper.get('.mint-plotly-chart__plot').element
211
+ wrapper.unmount()
212
+ plotly.purge.mockClear()
213
+ finish()
214
+ await flushPromises()
215
+ expect(plotly.purge).toHaveBeenCalledWith(element)
216
+ expect(plotly.react).toHaveBeenCalledTimes(1)
217
+ })
218
+
219
+ it('should purge the removed canvas and render new data after loading or empty states', async () => {
220
+ const disconnect = vi.fn()
221
+ vi.stubGlobal('ResizeObserver', class {
222
+ observe = vi.fn()
223
+ unobserve = vi.fn()
224
+ disconnect = disconnect
225
+ })
226
+ const wrapper = mount(PlotlyChart, { props: { data: [{ x: [1] }] } })
227
+ await flushPromises()
228
+ for (const state of ['loading', 'empty'] as const) {
229
+ const element = wrapper.get('.mint-plotly-chart__plot').element
230
+ await wrapper.setProps({ [state]: true })
231
+ await flushPromises()
232
+ expect(plotly.purge).toHaveBeenCalledWith(element)
233
+ expect(disconnect).toHaveBeenCalled()
234
+ await wrapper.setProps({ [state]: false, data: [{ x: [2] }] })
235
+ await flushPromises()
236
+ expect(wrapper.get('.mint-plotly-chart__plot').element).not.toBe(element)
237
+ }
238
+ expect(plotly.react).toHaveBeenCalledTimes(3)
239
+ })
240
+
241
+ it('should use MINT colors and protect reactive native data and nested layout from Plotly mutations', async () => {
242
+ document.documentElement.style.setProperty('--text-primary', '#f8fafc')
243
+ document.documentElement.style.setProperty('--border-color', '#334155')
244
+ const data = reactive([{ x: [1], y: [2] }])
245
+ const layout = reactive({ title: { text: 'Original' }, xaxis: { title: { text: 'Sample' } } })
246
+ plotly.react.mockImplementationOnce((_element, traces, options) => {
247
+ traces[0].x.push(9)
248
+ options.title.text = 'Changed'
249
+ options.xaxis.title.text = 'Changed'
250
+ return Promise.resolve()
251
+ })
252
+ mount(PlotlyChart, { attachTo: document.body, props: { data, layout } })
253
+ await flushPromises()
254
+ expect(plotly.react.mock.lastCall?.[2].font.color).toBe('#f8fafc')
255
+ expect(plotly.react.mock.lastCall?.[2].xaxis.gridcolor).toBe('#334155')
256
+ expect(data[0].x).toEqual([1])
257
+ expect(layout.title.text).toBe('Original')
258
+ expect(layout.xaxis.title.text).toBe('Sample')
259
+ })
260
+
261
+ it('should accept nested reactive arrays, typed arrays, dates, and native export callbacks', async () => {
262
+ const x = reactive([new Date('2026-01-01')])
263
+ const y = new Float64Array([2])
264
+ const click = vi.fn()
265
+ mount(PlotlyChart, {
266
+ props: {
267
+ data: [{ x, y }],
268
+ config: { modeBarButtonsToAdd: [{ name: 'Export', title: 'Export', icon: 'camera', click }] },
269
+ },
270
+ })
271
+ await flushPromises()
272
+ expect(plotly.react).toHaveBeenCalledOnce()
273
+ const [, traces, , config] = plotly.react.mock.lastCall!
274
+ expect(traces[0].x).toEqual(x)
275
+ expect(traces[0].x).not.toBe(x)
276
+ expect(traces[0].x[0]).toBeInstanceOf(Date)
277
+ expect(traces[0].y).toEqual(y)
278
+ expect(traces[0].y).not.toBe(y)
279
+ expect(config.modeBarButtonsToAdd[0].click).toBe(click)
280
+ })
281
+
282
+ it('should ignore resize rejection when the canvas becomes hidden', async () => {
283
+ let resize!: ResizeObserverCallback
284
+ vi.stubGlobal('ResizeObserver', class {
285
+ constructor(callback: ResizeObserverCallback) { resize = callback }
286
+ observe(): void {}
287
+ disconnect(): void {}
288
+ })
289
+ plotly.Plots.resize.mockRejectedValueOnce(new Error('Resize must be passed a displayed plot div'))
290
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
291
+ await flushPromises()
292
+ resize([], {} as ResizeObserver)
293
+ await flushPromises()
294
+ expect(wrapper.find('[role="alert"]').exists()).toBe(false)
295
+ })
296
+
297
+ it('should purge the Plotly canvas when unmounted', async () => {
298
+ const wrapper = mount(PlotlyChart, { props: { data: [] } })
299
+ await flushPromises()
300
+
301
+ wrapper.unmount()
302
+
303
+ expect(plotly.purge).toHaveBeenCalledTimes(1)
304
+ })
305
+ })
@@ -114,9 +114,8 @@ describe('PluginWorkspaceView internal split', () => {
114
114
  activeView: 'data',
115
115
  title: 'Tools',
116
116
  subtitle: 'Current file',
117
- variant: 'analysis',
118
117
  floating: false,
119
- dense: true,
118
+ density: 'compact',
120
119
  })
121
120
  expect(wrapper.get('[data-test="topbar-action"]').text()).toBe('Run')
122
121
  expect(wrapper.get('[data-test="import-action"]').text()).toBe('Upload')
@@ -182,9 +182,8 @@ describe('PluginWorkspaceView', () => {
182
182
  activeView: 'data',
183
183
  title: 'Tools',
184
184
  subtitle: 'Current file',
185
- variant: 'analysis',
186
185
  floating: false,
187
- dense: true,
186
+ density: 'compact',
188
187
  })
189
188
  expect(wrapper.get('[data-test="topbar-action"]').text()).toBe('Run')
190
189
  expect(wrapper.get('[data-test="import-action"]').text()).toBe('Upload')
@@ -3,6 +3,25 @@ import { describe, expect, it } from 'vitest'
3
3
  import SegmentedControl from '../../components/SegmentedControl.vue'
4
4
 
5
5
  describe('SegmentedControl', () => {
6
+ it('defaults to card while allowing an explicit simple variant', async () => {
7
+ const wrapper = mount(SegmentedControl, {
8
+ props: {
9
+ modelValue: 'table',
10
+ options: [{ value: 'table', label: 'Table', description: 'View rows' }],
11
+ },
12
+ })
13
+
14
+ expect(wrapper.classes()).toContain('mint-segmented-control--card')
15
+ expect(wrapper.get('button').classes()).toContain('mint-segmented-control__option--card')
16
+ expect(wrapper.get('.mint-segmented-control__description').text()).toBe('View rows')
17
+
18
+ await wrapper.setProps({ variant: 'simple' })
19
+
20
+ expect(wrapper.classes()).toContain('mint-segmented-control--simple')
21
+ expect(wrapper.get('button').classes()).toContain('mint-segmented-control__option--simple')
22
+ expect(wrapper.find('.mint-segmented-control__description').exists()).toBe(false)
23
+ })
24
+
6
25
  it('accepts string and number shorthand options', async () => {
7
26
  const wrapper = mount(SegmentedControl, {
8
27
  props: {
@@ -0,0 +1,18 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ // jsdom has no layout, so this pins the stylesheet contract instead: a bare
6
+ // <svg> passed to IconButton's slot must be sized by the icon box, otherwise it
7
+ // collapses to zero width as a flex item (FilePicker source toggles, beta.14).
8
+ describe('IconButton icon sizing', () => {
9
+ it('should size a bare slot svg to the icon box when the caller passes no dimensions', () => {
10
+ const css = readFileSync(
11
+ resolve(__dirname, '../../styles/components/icon-button.css'),
12
+ 'utf8',
13
+ )
14
+ expect(css).toMatch(
15
+ /\.mint-icon-button__icon--sm > svg,\s*\.mint-icon-button__icon--md > svg,\s*\.mint-icon-button__icon--lg > svg \{\s*width: 100%;\s*height: 100%;\s*\}/,
16
+ )
17
+ })
18
+ })
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { localFilesAdapter } from '../../composables/filePicker/localFiles'
3
+
4
+ describe('localFilesAdapter', () => {
5
+ it('preserves order, duplicate files, nested statistics and scoped search', async () => {
6
+ const files = [
7
+ new File(['abc'], 'run.raw'),
8
+ new File(['12'], 'run.raw'),
9
+ new File(['1234'], 'sample.mzML.gz'),
10
+ new File(['x'], 'other.raw'),
11
+ ]
12
+ const adapter = localFilesAdapter(files, [
13
+ 'study/batch/run.raw', 'study/batch/run.raw', 'study/sample.mzML.gz', 'study2/other.raw',
14
+ ])
15
+ expect(await adapter.listRoot()).toMatchObject([
16
+ { path: 'study', fileCount: 3, bytes: 9, formats: { raw: 2, 'mzML.gz': 1 } },
17
+ { path: 'study2', fileCount: 1, bytes: 1, formats: { raw: 1 } },
18
+ ])
19
+ expect(await adapter.listChildren('study')).toMatchObject([
20
+ { path: 'study/batch', fileCount: 2, bytes: 5, formats: { raw: 2 } },
21
+ { path: 'study/sample.mzML.gz', file: files[2] },
22
+ ])
23
+ expect(await adapter.listChildren('study/batch')).toMatchObject([
24
+ { path: 'study/batch/run.raw', file: files[0] },
25
+ { path: 'study/batch/run.raw (2)', file: files[1] },
26
+ ])
27
+ expect(await adapter.search('RUN', { path: 'study', recursive: true })).toHaveLength(2)
28
+ expect(await adapter.search('RUN', { path: 'study', recursive: false })).toEqual([])
29
+ expect(await adapter.listChildren('missing')).toEqual([])
30
+ const roots = await adapter.listRoot()
31
+ roots.pop()
32
+ expect(await adapter.listRoot()).toHaveLength(2)
33
+ })
34
+
35
+ it('builds and lists many sibling folders without repeatedly scanning all nodes', async () => {
36
+ const files = Array.from({ length: 200 }, (_, i) => new File(['x'], `${i}.raw`))
37
+ const paths = files.map((file, i) => `folder-${i}/${file.name}`)
38
+ const originalValues = Map.prototype.values
39
+ let visited = 0
40
+ const values = vi.spyOn(Map.prototype, 'values').mockImplementation(function* (this: Map<unknown, unknown>) {
41
+ for (const value of originalValues.call(this)) {
42
+ visited++
43
+ yield value
44
+ }
45
+ } as typeof Map.prototype.values)
46
+ try {
47
+ const adapter = localFilesAdapter(files, paths)
48
+ await adapter.listRoot()
49
+ for (let i = 0; i < files.length; i++) {
50
+ expect(await adapter.listChildren(`folder-${i}`)).toHaveLength(1)
51
+ }
52
+ expect(visited).toBeLessThanOrEqual(files.length * 8)
53
+ } finally {
54
+ values.mockRestore()
55
+ }
56
+ })
57
+ })