@morscherlab/mint-sdk 1.0.0-beta.3 → 1.0.0-beta.5

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 +9 -2
  2. package/dist/__tests__/composables/experiment-utils.test.d.ts +1 -0
  3. package/dist/__tests__/composables/useApi.test.d.ts +1 -0
  4. package/dist/components/AppContainer.vue.d.ts +1 -1
  5. package/dist/components/AppLayout.vue.d.ts +20 -1
  6. package/dist/components/AppSidebar.vue.d.ts +57 -5
  7. package/dist/components/AppTopBar.vue.d.ts +7 -25
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +3 -1
  9. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -0
  10. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +5 -0
  11. package/dist/components/ComponentBindingRenderer.vue.d.ts +44 -0
  12. package/dist/components/ControlWorkspaceView.vue.d.ts +24 -7
  13. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +149 -0
  14. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  15. package/dist/components/FormBuilder.vue.d.ts +9 -9
  16. package/dist/components/PlateMapEditor.vue.d.ts +1 -1
  17. package/dist/components/PluginWorkspaceView.vue.d.ts +310 -0
  18. package/dist/components/SettingsModal.vue.d.ts +1 -1
  19. package/dist/components/WellPlate.vue.d.ts +2 -2
  20. package/dist/components/index.d.ts +3 -12
  21. package/dist/components/index.js +3 -3
  22. package/dist/components/{AppPageSelector.vue.d.ts → internal/AppTopBarPageSelectorInternal.vue.d.ts} +1 -1
  23. package/dist/components/{AppPillNav.vue.d.ts → internal/AppTopBarPillNavInternal.vue.d.ts} +3 -1
  24. package/dist/components/{CalendarGridPanel.vue.d.ts → internal/CalendarGridPanelInternal.vue.d.ts} +1 -1
  25. package/dist/components/internal/FormSectionRenderer.vue.d.ts +4 -4
  26. package/dist/components/{WellEditPopup.vue.d.ts → internal/WellEditPopupInternal.vue.d.ts} +1 -1
  27. package/dist/{components-D_Sr0adg.js → components-DihbSJjU.js} +5932 -5408
  28. package/dist/components-DihbSJjU.js.map +1 -0
  29. package/dist/composables/experiment-utils.d.ts +8 -0
  30. package/dist/composables/index.d.ts +5 -7
  31. package/dist/composables/index.js +4 -4
  32. package/dist/composables/useAppExperiment.d.ts +31 -2
  33. package/dist/composables/useBioTemplateComponents.d.ts +5 -3
  34. package/dist/composables/useBioTemplatePackWorkspace.d.ts +3 -2
  35. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +6 -5
  36. package/dist/composables/useBioTemplateWorkspace.d.ts +5 -4
  37. package/dist/composables/useControlSchema.d.ts +43 -21
  38. package/dist/composables/usePluginClient.d.ts +5 -2
  39. package/dist/{composables-C3dpXQN5.js → composables-BcgZ6diz.js} +40 -28
  40. package/dist/composables-BcgZ6diz.js.map +1 -0
  41. package/dist/index.d.ts +5 -12
  42. package/dist/index.js +5 -5
  43. package/dist/install.js +2 -2
  44. package/dist/styles.css +5637 -5663
  45. package/dist/templates/adapters.d.ts +7 -1
  46. package/dist/templates/catalog.d.ts +5 -5
  47. package/dist/templates/componentBindings.d.ts +13 -0
  48. package/dist/templates/index.d.ts +5 -5
  49. package/dist/templates/index.js +2 -2
  50. package/dist/templates/presets.d.ts +4 -4
  51. package/dist/templates/types.d.ts +4 -1
  52. package/dist/{templates-50NPjaxL.js → templates-Cyt0Suwf.js} +322 -73
  53. package/dist/templates-Cyt0Suwf.js.map +1 -0
  54. package/dist/types/components.d.ts +6 -25
  55. package/dist/types/index.d.ts +1 -1
  56. package/dist/{useScheduleDrag-D4oWdh41.js → useExperimentData-CM6Y0u5L.js} +400 -357
  57. package/dist/useExperimentData-CM6Y0u5L.js.map +1 -0
  58. package/package.json +1 -1
  59. package/src/__tests__/components/ActionItem.test.ts +6 -6
  60. package/src/__tests__/components/AppLayout.test.ts +44 -0
  61. package/src/__tests__/components/AppSidebar.test.ts +130 -2
  62. package/src/__tests__/components/AppToastContainer.test.ts +0 -11
  63. package/src/__tests__/components/AppTopBar.test.ts +189 -120
  64. package/src/__tests__/components/{AppPageSelector.test.ts → AppTopBarPageSelector.test.ts} +8 -8
  65. package/src/__tests__/components/{AppPillNav.test.ts → AppTopBarPillNav.test.ts} +53 -6
  66. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +7 -1
  67. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +32 -1
  68. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +48 -1
  69. package/src/__tests__/components/BioTemplateRenderer.test.ts +25 -0
  70. package/src/__tests__/components/CalendarGridPanel.test.ts +3 -3
  71. package/src/__tests__/components/ComponentBindingRenderer.test.ts +278 -0
  72. package/src/__tests__/components/ControlWorkspaceView.test.ts +134 -63
  73. package/src/__tests__/components/DateTimePicker.test.ts +2 -2
  74. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
  75. package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
  76. package/src/__tests__/composables/experiment-utils.test.ts +30 -0
  77. package/src/__tests__/composables/useApi.test.ts +30 -0
  78. package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
  79. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +7 -4
  80. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +7 -7
  81. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +6 -1
  82. package/src/__tests__/composables/useControlSchema.test.ts +151 -37
  83. package/src/__tests__/composables/usePluginClient.test.ts +99 -2
  84. package/src/__tests__/docs/frontendDocsCatalog.test.ts +120 -25
  85. package/src/__tests__/templates/templates.test.ts +56 -0
  86. package/src/components/AppAvatarMenu.vue +3 -3
  87. package/src/components/AppLayout.story.vue +39 -0
  88. package/src/components/AppLayout.vue +83 -2
  89. package/src/components/AppPluginSwitcher.vue +5 -5
  90. package/src/components/AppSidebar.story.vue +113 -5
  91. package/src/components/AppSidebar.vue +147 -27
  92. package/src/components/AppTopBar.story.vue +2 -5
  93. package/src/components/AppTopBar.vue +35 -425
  94. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +2 -2
  95. package/src/components/BioTemplateExperimentWorkspaceView.vue +6 -0
  96. package/src/components/BioTemplatePackWorkspaceView.story.vue +4 -4
  97. package/src/components/BioTemplatePackWorkspaceView.vue +1 -0
  98. package/src/components/BioTemplatePresetWorkspaceView.story.vue +14 -2
  99. package/src/components/BioTemplatePresetWorkspaceView.vue +12 -3
  100. package/src/components/BioTemplateRenderer.story.vue +2 -2
  101. package/src/components/BioTemplateRenderer.vue +15 -227
  102. package/src/components/ComponentBindingRenderer.story.vue +87 -0
  103. package/src/components/ComponentBindingRenderer.vue +317 -0
  104. package/src/components/ControlWorkspaceView.story.vue +20 -9
  105. package/src/components/ControlWorkspaceView.vue +43 -12
  106. package/src/components/DatePicker.vue +2 -2
  107. package/src/components/DateTimePicker.vue +2 -2
  108. package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
  109. package/src/components/DoseDesignWorkspaceView.vue +255 -0
  110. package/src/components/ExperimentPopover.story.vue +2 -2
  111. package/src/components/ExperimentPopover.vue +2 -6
  112. package/src/components/ExperimentSelectorModal.vue +6 -5
  113. package/src/components/FormBuilder.story.vue +190 -0
  114. package/src/components/PluginWorkspaceView.story.vue +334 -0
  115. package/src/components/PluginWorkspaceView.vue +708 -0
  116. package/src/components/SettingsModal.story.vue +87 -0
  117. package/src/components/WellPlate.vue +2 -2
  118. package/src/components/index.ts +3 -12
  119. package/src/components/{AppPageSelector.vue → internal/AppTopBarPageSelectorInternal.vue} +9 -9
  120. package/src/components/internal/AppTopBarPillNavInternal.vue +194 -0
  121. package/src/components/{CalendarGridPanel.vue → internal/CalendarGridPanelInternal.vue} +1 -1
  122. package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +3 -3
  123. package/src/composables/experiment-utils.ts +26 -0
  124. package/src/composables/index.ts +21 -7
  125. package/src/composables/useApi.ts +9 -2
  126. package/src/composables/useAppExperiment.ts +85 -13
  127. package/src/composables/useBioTemplateComponents.ts +12 -0
  128. package/src/composables/useBioTemplatePackWorkspace.ts +6 -2
  129. package/src/composables/useBioTemplatePresetWorkspace.ts +10 -21
  130. package/src/composables/useBioTemplateWorkspace.ts +6 -4
  131. package/src/composables/useControlSchema.ts +157 -69
  132. package/src/composables/usePluginClient.ts +50 -9
  133. package/src/index.ts +6 -563
  134. package/src/styles/components/app-layout.css +82 -0
  135. package/src/styles/components/app-page-selector.css +1 -1
  136. package/src/styles/components/app-pill-nav.css +71 -1
  137. package/src/styles/components/app-sidebar.css +119 -0
  138. package/src/styles/components/app-top-bar.css +0 -235
  139. package/src/styles/components/experiment-popover.css +2 -2
  140. package/src/styles/index.css +0 -1
  141. package/src/templates/adapters.ts +193 -0
  142. package/src/templates/catalog.ts +5 -5
  143. package/src/templates/componentBindings.ts +90 -3
  144. package/src/templates/index.ts +10 -0
  145. package/src/templates/packs.ts +10 -1
  146. package/src/templates/presets.ts +14 -4
  147. package/src/templates/types.ts +4 -0
  148. package/src/types/components.ts +6 -31
  149. package/src/types/index.ts +2 -6
  150. package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
  151. package/dist/components/FormFieldRenderer.vue.d.ts +0 -28
  152. package/dist/components/FormSection.vue.d.ts +0 -30
  153. package/dist/components/GroupingModal.vue.d.ts +0 -12
  154. package/dist/components/SettingsButton.vue.d.ts +0 -30
  155. package/dist/components/ToastNotification.vue.d.ts +0 -2
  156. package/dist/components-D_Sr0adg.js.map +0 -1
  157. package/dist/composables/usePluginApi.d.ts +0 -22
  158. package/dist/composables-C3dpXQN5.js.map +0 -1
  159. package/dist/templates-50NPjaxL.js.map +0 -1
  160. package/dist/useScheduleDrag-D4oWdh41.js.map +0 -1
  161. package/src/__tests__/components/FormCompatibility.test.ts +0 -94
  162. package/src/__tests__/components/GroupingModal.test.ts +0 -73
  163. package/src/__tests__/components/SettingsButton.test.ts +0 -44
  164. package/src/__tests__/composables/usePluginApi.test.ts +0 -81
  165. package/src/components/AppPillNav.vue +0 -71
  166. package/src/components/FormFieldRenderer.vue +0 -35
  167. package/src/components/FormSection.vue +0 -37
  168. package/src/components/GroupingModal.story.vue +0 -52
  169. package/src/components/GroupingModal.vue +0 -61
  170. package/src/components/SettingsButton.story.vue +0 -58
  171. package/src/components/SettingsButton.vue +0 -64
  172. package/src/components/ToastNotification.vue +0 -9
  173. package/src/composables/usePluginApi.ts +0 -32
  174. package/src/styles/components/settings-button.css +0 -31
  175. /package/dist/__tests__/components/{AppPageSelector.test.d.ts → AppTopBarPageSelector.test.d.ts} +0 -0
  176. /package/dist/__tests__/components/{AppPillNav.test.d.ts → AppTopBarPillNav.test.d.ts} +0 -0
  177. /package/dist/__tests__/components/{FormCompatibility.test.d.ts → ComponentBindingRenderer.test.d.ts} +0 -0
  178. /package/dist/__tests__/components/{GroupingModal.test.d.ts → DoseDesignWorkspaceView.test.d.ts} +0 -0
  179. /package/dist/__tests__/components/{SettingsButton.test.d.ts → PluginWorkspaceView.test.d.ts} +0 -0
  180. /package/dist/components/{ActionItem.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +0 -0
  181. /package/src/components/{ActionItem.vue → internal/ActionItemInternal.vue} +0 -0
@@ -21,8 +21,10 @@ const globalOptions = {
21
21
  stubs: {
22
22
  DataFrame: true,
23
23
  DoseCalculator: true,
24
+ ExperimentTimeline: true,
24
25
  PlateMapEditor: true,
25
26
  SampleSelector: true,
27
+ ScheduleCalendar: true,
26
28
  WellPlate: true,
27
29
  },
28
30
  }
@@ -124,14 +126,26 @@ describe('BioTemplatePackWorkspaceView', () => {
124
126
  pack: 'cell-culture-screen',
125
127
  },
126
128
  slots: {
127
- default: ({ bindings, pack, componentPropsById, form, getComponentProps, pillNav, sidebar, topBarSettings }) => h(
129
+ default: ({
130
+ bindings,
131
+ pack,
132
+ componentBindingsById,
133
+ componentPropsById,
134
+ form,
135
+ getComponentProps,
136
+ pillNav,
137
+ sidebar,
138
+ topBarSettings,
139
+ }) => h(
128
140
  'pre',
129
141
  { class: 'component-props' },
130
142
  JSON.stringify({
131
143
  pack: pack.name,
132
144
  activeView: sidebar.activeView,
133
145
  doseMode: componentPropsById['dose-response:DoseCalculator'].mode,
146
+ bindingMode: componentBindingsById['dose-response:DoseCalculator'].props.mode,
134
147
  bindingDoseMode: bindings.componentPropsById['dose-response:DoseCalculator'].mode,
148
+ nestedBindingMode: bindings.componentBindingsById['dose-response:DoseCalculator'].props.mode,
135
149
  hasPlateWells: Boolean(componentPropsById['plate-map:WellPlate'].wells),
136
150
  doseModeFromGetter: getComponentProps('DoseCalculator')?.mode,
137
151
  bindingDoseModeFromGetter: bindings.getComponentProps('DoseCalculator')?.mode,
@@ -148,7 +162,9 @@ describe('BioTemplatePackWorkspaceView', () => {
148
162
  pack: 'cell-culture-screen',
149
163
  activeView: 'design',
150
164
  doseMode: 'serial',
165
+ bindingMode: 'serial',
151
166
  bindingDoseMode: 'serial',
167
+ nestedBindingMode: 'serial',
152
168
  hasPlateWells: true,
153
169
  doseModeFromGetter: 'serial',
154
170
  bindingDoseModeFromGetter: 'serial',
@@ -158,4 +174,19 @@ describe('BioTemplatePackWorkspaceView', () => {
158
174
  })
159
175
  expect(wrapper.find('.mint-bio-template-renderer').exists()).toBe(false)
160
176
  })
177
+
178
+ it('renders metabolism pack run queues as timeline and calendar previews', () => {
179
+ const wrapper = mount(BioTemplatePackWorkspaceView, {
180
+ props: {
181
+ pack: 'metabolism',
182
+ },
183
+ global: globalOptions,
184
+ })
185
+
186
+ expect(wrapper.text()).toContain('Omics assay saves the full template collection')
187
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
188
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
189
+ expect(wrapper.find('[data-template-component="ExperimentTimeline"]').exists()).toBe(true)
190
+ expect(wrapper.find('[data-template-component="ScheduleCalendar"]').exists()).toBe(true)
191
+ })
161
192
  })
@@ -32,6 +32,7 @@ describe('BioTemplatePresetWorkspaceView', () => {
32
32
  expect(wrapper.find('.mint-bio-template-preset-workspace').exists()).toBe(true)
33
33
  expect(wrapper.text()).toContain('Well-plate screen preset saves')
34
34
  expect(wrapper.find('.mint-sidebar').exists()).toBe(true)
35
+ expect(wrapper.findComponent(AppSidebar).props('variant')).toBe('analysis')
35
36
  expect(wrapper.find('.mint-bio-template-renderer').exists()).toBe(true)
36
37
  expect(wrapper.text()).toContain('plate-map')
37
38
  expect(wrapper.text()).toContain('dose-response')
@@ -68,6 +69,26 @@ describe('BioTemplatePresetWorkspaceView', () => {
68
69
  expect(wrapper.text()).toContain('dose-response')
69
70
  })
70
71
 
72
+ it('passes through sidebar variant overrides', () => {
73
+ const wrapper = mount(BioTemplatePresetWorkspaceView, {
74
+ props: {
75
+ preset: 'wellplate-screen',
76
+ sidebarVariant: 'default',
77
+ },
78
+ global: {
79
+ stubs: {
80
+ DataFrame: true,
81
+ DoseCalculator: true,
82
+ PlateMapEditor: true,
83
+ SampleSelector: true,
84
+ WellPlate: true,
85
+ },
86
+ },
87
+ })
88
+
89
+ expect(wrapper.findComponent(AppSidebar).props('variant')).toBe('default')
90
+ })
91
+
71
92
  it('updates the internally generated workspace when the preset changes after mount', async () => {
72
93
  const wrapper = mount(BioTemplatePresetWorkspaceView, {
73
94
  props: {
@@ -194,13 +215,15 @@ describe('BioTemplatePresetWorkspaceView', () => {
194
215
  preset: 'wellplate-screen',
195
216
  },
196
217
  slots: {
197
- default: ({ bindings, collection, componentPropsById, getComponentProps }) => h(
218
+ default: ({ bindings, collection, componentBindingsById, componentPropsById, getComponentProps }) => h(
198
219
  'pre',
199
220
  { class: 'component-props' },
200
221
  JSON.stringify({
201
222
  preset: collection.metadata?.preset,
202
223
  doseMode: componentPropsById['dose-response:DoseCalculator'].mode,
224
+ bindingMode: componentBindingsById['dose-response:DoseCalculator'].props.mode,
203
225
  bindingDoseMode: bindings.componentPropsById.value['dose-response:DoseCalculator'].mode,
226
+ nestedBindingMode: bindings.componentBindingsById.value['dose-response:DoseCalculator'].props.mode,
204
227
  hasPlateWells: Boolean(componentPropsById['plate-map:WellPlate'].wells),
205
228
  doseModeFromGetter: getComponentProps('DoseCalculator')?.mode,
206
229
  bindingDoseModeFromGetter: bindings.getComponentProps('DoseCalculator')?.mode,
@@ -221,7 +244,9 @@ describe('BioTemplatePresetWorkspaceView', () => {
221
244
  expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
222
245
  preset: 'wellplate-screen',
223
246
  doseMode: 'serial',
247
+ bindingMode: 'serial',
224
248
  bindingDoseMode: 'serial',
249
+ nestedBindingMode: 'serial',
225
250
  hasPlateWells: true,
226
251
  doseModeFromGetter: 'serial',
227
252
  bindingDoseModeFromGetter: 'serial',
@@ -278,4 +303,26 @@ describe('BioTemplatePresetWorkspaceView', () => {
278
303
  expect(workspace.activeControlView.value).toBe('analysis')
279
304
  expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('analysis')
280
305
  })
306
+
307
+ it('renders LC-MS batch timeline and calendar previews from one preset prop', () => {
308
+ const wrapper = mount(BioTemplatePresetWorkspaceView, {
309
+ props: {
310
+ preset: 'lcms-batch',
311
+ },
312
+ global: {
313
+ stubs: {
314
+ DataFrame: true,
315
+ ExperimentTimeline: true,
316
+ ScheduleCalendar: true,
317
+ SampleSelector: true,
318
+ },
319
+ },
320
+ })
321
+
322
+ expect(wrapper.text()).toContain('LC-MS batch preset saves')
323
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
324
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
325
+ expect(wrapper.find('[data-template-component="ExperimentTimeline"]').exists()).toBe(true)
326
+ expect(wrapper.find('[data-template-component="ScheduleCalendar"]').exists()).toBe(true)
327
+ })
281
328
  })
@@ -33,6 +33,31 @@ describe('BioTemplateRenderer', () => {
33
33
  expect(wrapper.text()).toContain('dose-response')
34
34
  })
35
35
 
36
+ it('renders instrument-run timelines from LC-MS template collections', () => {
37
+ const collection = createLcmsBatchCollection({
38
+ samples: ['S001', 'S002'],
39
+ instrument: 'LC-MS',
40
+ })
41
+ const wrapper = mount(BioTemplateRenderer, {
42
+ props: { target: collection },
43
+ global: {
44
+ stubs: {
45
+ DataFrame: true,
46
+ ExperimentTimeline: true,
47
+ SampleSelector: true,
48
+ ScheduleCalendar: true,
49
+ },
50
+ },
51
+ })
52
+
53
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
54
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
55
+ expect(wrapper.find('[data-component-binding-id="assay-matrix:SampleSelector"]').exists()).toBe(true)
56
+ expect(wrapper.find('[data-template-component="ScheduleCalendar"]').exists()).toBe(true)
57
+ expect(wrapper.find('[data-template-component="ExperimentTimeline"]').exists()).toBe(true)
58
+ expect(wrapper.find('[data-template-id="instrument-run"]').exists()).toBe(true)
59
+ })
60
+
36
61
  it('filters rendered components with include and exclude lists', () => {
37
62
  const collection = createWellPlateScreenCollection()
38
63
  const wrapper = mount(BioTemplateRenderer, {
@@ -1,6 +1,6 @@
1
1
  import { mount } from '@vue/test-utils'
2
2
  import { describe, expect, it } from 'vitest'
3
- import CalendarGridPanel from '../../components/CalendarGridPanel.vue'
3
+ import CalendarGridPanelInternal from '../../components/internal/CalendarGridPanelInternal.vue'
4
4
  import type { CalendarGridDay } from '../../composables/useCalendarGrid'
5
5
 
6
6
  const days: CalendarGridDay[] = [
@@ -9,9 +9,9 @@ const days: CalendarGridDay[] = [
9
9
  { date: new Date(2026, 3, 22), isCurrentMonth: true, isDisabled: true },
10
10
  ]
11
11
 
12
- describe('CalendarGridPanel', () => {
12
+ describe('CalendarGridPanelInternal', () => {
13
13
  it('renders calendar state and emits navigation and selection events', async () => {
14
- const wrapper = mount(CalendarGridPanel, {
14
+ const wrapper = mount(CalendarGridPanelInternal, {
15
15
  props: {
16
16
  weekDays: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
17
17
  monthYear: 'April 2026',
@@ -0,0 +1,278 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { defineComponent } from 'vue'
3
+ import { describe, expect, it } from 'vitest'
4
+ import ComponentBindingRenderer from '../../components/ComponentBindingRenderer.vue'
5
+
6
+ const WellPlateStub = defineComponent({
7
+ name: 'WellPlate',
8
+ props: {
9
+ modelValue: { type: Array, default: () => [] },
10
+ readonly: { type: Boolean, default: false },
11
+ size: { type: String, default: undefined },
12
+ },
13
+ template: '<div data-test="well-plate" />',
14
+ })
15
+
16
+ const DoseCalculatorStub = defineComponent({
17
+ name: 'DoseCalculator',
18
+ props: {
19
+ mode: { type: String, default: undefined },
20
+ targetWells: { type: Array, default: () => [] },
21
+ },
22
+ template: '<div data-test="dose-calculator" />',
23
+ })
24
+
25
+ const DataFrameStub = defineComponent({
26
+ name: 'DataFrame',
27
+ props: {
28
+ data: { type: Array, default: () => [] },
29
+ columns: { type: Array, default: () => [] },
30
+ size: { type: String, default: undefined },
31
+ maxHeight: { type: String, default: undefined },
32
+ },
33
+ template: '<div data-test="data-frame" />',
34
+ })
35
+
36
+ const PlateMapEditorStub = defineComponent({
37
+ name: 'PlateMapEditor',
38
+ props: {
39
+ modelValue: { type: Object, default: () => ({}) },
40
+ showToolbar: { type: Boolean, default: true },
41
+ showSidebar: { type: Boolean, default: true },
42
+ allowAddPlates: { type: Boolean, default: true },
43
+ allowAddSamples: { type: Boolean, default: true },
44
+ size: { type: String, default: undefined },
45
+ },
46
+ template: '<div data-test="plate-map-editor" />',
47
+ })
48
+
49
+ const ExperimentTimelineStub = defineComponent({
50
+ name: 'ExperimentTimeline',
51
+ props: {
52
+ modelValue: { type: Array, default: () => [] },
53
+ editable: { type: Boolean, default: true },
54
+ size: { type: String, default: undefined },
55
+ },
56
+ template: '<div data-test="experiment-timeline" />',
57
+ })
58
+
59
+ const ScheduleCalendarStub = defineComponent({
60
+ name: 'ScheduleCalendar',
61
+ props: {
62
+ modelValue: { type: [String, Date], default: undefined },
63
+ events: { type: Array, default: () => [] },
64
+ readonly: { type: Boolean, default: false },
65
+ showNavigation: { type: Boolean, default: true },
66
+ showViewToggle: { type: Boolean, default: true },
67
+ view: { type: String, default: undefined },
68
+ },
69
+ template: '<div data-test="schedule-calendar" />',
70
+ })
71
+
72
+ const SampleSelectorStub = defineComponent({
73
+ name: 'SampleSelector',
74
+ props: {
75
+ samples: { type: Array, default: () => [] },
76
+ modelValue: { type: Array, default: () => [] },
77
+ enableGrouping: { type: Boolean, default: true },
78
+ enableSmartGroup: { type: Boolean, default: true },
79
+ },
80
+ template: '<div data-test="sample-selector" />',
81
+ })
82
+
83
+ const globalOptions = {
84
+ stubs: {
85
+ DataFrame: DataFrameStub,
86
+ DoseCalculator: DoseCalculatorStub,
87
+ ExperimentTimeline: ExperimentTimelineStub,
88
+ PlateMapEditor: PlateMapEditorStub,
89
+ SampleSelector: SampleSelectorStub,
90
+ ScheduleCalendar: ScheduleCalendarStub,
91
+ WellPlate: WellPlateStub,
92
+ },
93
+ }
94
+
95
+ describe('ComponentBindingRenderer', () => {
96
+ it('renders known SDK component bindings and skips unsupported names', () => {
97
+ const wrapper = mount(ComponentBindingRenderer, {
98
+ props: {
99
+ bindings: [
100
+ {
101
+ id: 'plate',
102
+ component: 'WellPlate',
103
+ props: { modelValue: ['A1'] },
104
+ description: 'Selected wells',
105
+ },
106
+ {
107
+ id: 'dose',
108
+ component: 'DoseCalculator',
109
+ props: { mode: 'serial', targetWells: ['A1'] },
110
+ },
111
+ {
112
+ id: 'custom',
113
+ component: 'CustomChart',
114
+ props: { score: 0.5 },
115
+ },
116
+ ],
117
+ },
118
+ global: globalOptions,
119
+ })
120
+
121
+ expect(wrapper.find('[data-component-binding-id="plate"]').exists()).toBe(true)
122
+ expect(wrapper.find('[data-component-binding-id="dose"]').exists()).toBe(true)
123
+ expect(wrapper.find('[data-component-binding-id="custom"]').exists()).toBe(false)
124
+ expect(wrapper.findComponent(WellPlateStub).props()).toMatchObject({
125
+ modelValue: ['A1'],
126
+ readonly: true,
127
+ size: 'fill',
128
+ })
129
+ expect(wrapper.findComponent(DoseCalculatorStub).props()).toMatchObject({
130
+ mode: 'serial',
131
+ targetWells: ['A1'],
132
+ })
133
+ expect(wrapper.text()).toContain('Selected wells')
134
+ })
135
+
136
+ it('accepts a single binding and preview-safe dense props', () => {
137
+ const wrapper = mount(ComponentBindingRenderer, {
138
+ props: {
139
+ binding: {
140
+ id: 'plate-map',
141
+ component: 'PlateMapEditor',
142
+ propsObject: { modelValue: { plates: [] } },
143
+ },
144
+ dense: true,
145
+ },
146
+ global: globalOptions,
147
+ })
148
+
149
+ expect(wrapper.findComponent(PlateMapEditorStub).props()).toMatchObject({
150
+ showToolbar: false,
151
+ showSidebar: false,
152
+ allowAddPlates: false,
153
+ allowAddSamples: false,
154
+ size: 'md',
155
+ })
156
+ })
157
+
158
+ it('filters bindings and renders a compact empty state', () => {
159
+ const wrapper = mount(ComponentBindingRenderer, {
160
+ props: {
161
+ bindings: [
162
+ {
163
+ id: 'table',
164
+ component: 'DataFrame',
165
+ props: { data: [{ id: 1 }], columns: [{ key: 'id', label: 'ID' }] },
166
+ },
167
+ ],
168
+ include: ['WellPlate'],
169
+ emptyText: 'No components.',
170
+ },
171
+ global: globalOptions,
172
+ })
173
+
174
+ expect(wrapper.find('[data-component-binding-id="table"]').exists()).toBe(false)
175
+ expect(wrapper.text()).toContain('No components.')
176
+ })
177
+
178
+ it('normalizes DataFrame sizing in dense mode', () => {
179
+ const wrapper = mount(ComponentBindingRenderer, {
180
+ props: {
181
+ bindings: [
182
+ {
183
+ id: 'table',
184
+ component: 'DataFrame',
185
+ props: { data: [{ id: 1 }], columns: [{ key: 'id', label: 'ID' }] },
186
+ },
187
+ ],
188
+ dense: true,
189
+ },
190
+ global: globalOptions,
191
+ })
192
+
193
+ expect(wrapper.findComponent(DataFrameStub).props()).toMatchObject({
194
+ size: 'sm',
195
+ maxHeight: '280px',
196
+ })
197
+ })
198
+
199
+ it('renders ExperimentTimeline bindings with preview-safe props', () => {
200
+ const wrapper = mount(ComponentBindingRenderer, {
201
+ props: {
202
+ binding: {
203
+ id: 'instrument-run:ExperimentTimeline',
204
+ component: 'ExperimentTimeline',
205
+ propsObject: {
206
+ modelValue: [{ id: 's001-run', name: 'S001', type: 'measurement', status: 'pending', order: 1 }],
207
+ editable: true,
208
+ },
209
+ },
210
+ dense: true,
211
+ },
212
+ global: globalOptions,
213
+ })
214
+
215
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
216
+ expect(wrapper.findComponent(ExperimentTimelineStub).props()).toMatchObject({
217
+ modelValue: [{ id: 's001-run', name: 'S001', type: 'measurement', status: 'pending', order: 1 }],
218
+ editable: false,
219
+ size: 'sm',
220
+ })
221
+ })
222
+
223
+ it('renders ScheduleCalendar bindings with preview-safe props', () => {
224
+ const events = [{ id: 's001-run', title: 'S001', start: '2024-01-01T08:00:00.000Z', end: '2024-01-01T08:10:00.000Z' }]
225
+ const wrapper = mount(ComponentBindingRenderer, {
226
+ props: {
227
+ binding: {
228
+ id: 'instrument-run:ScheduleCalendar',
229
+ component: 'ScheduleCalendar',
230
+ propsObject: {
231
+ modelValue: events[0].start,
232
+ events,
233
+ readonly: false,
234
+ showNavigation: true,
235
+ showViewToggle: true,
236
+ view: 'day',
237
+ },
238
+ },
239
+ dense: true,
240
+ },
241
+ global: globalOptions,
242
+ })
243
+
244
+ expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
245
+ expect(wrapper.findComponent(ScheduleCalendarStub).props()).toMatchObject({
246
+ modelValue: events[0].start,
247
+ events,
248
+ readonly: true,
249
+ showNavigation: false,
250
+ showViewToggle: false,
251
+ view: 'day',
252
+ })
253
+ })
254
+
255
+ it('renders SampleSelector bindings with preview-safe grouping props', () => {
256
+ const wrapper = mount(ComponentBindingRenderer, {
257
+ props: {
258
+ binding: {
259
+ id: 'assay-matrix:SampleSelector',
260
+ component: 'SampleSelector',
261
+ propsObject: {
262
+ samples: ['S001', 'S002'],
263
+ modelValue: [],
264
+ },
265
+ },
266
+ },
267
+ global: globalOptions,
268
+ })
269
+
270
+ expect(wrapper.find('[data-component-binding-id="assay-matrix:SampleSelector"]').exists()).toBe(true)
271
+ expect(wrapper.findComponent(SampleSelectorStub).props()).toMatchObject({
272
+ samples: ['S001', 'S002'],
273
+ modelValue: [],
274
+ enableGrouping: false,
275
+ enableSmartGroup: false,
276
+ })
277
+ })
278
+ })