@morscherlab/mint-sdk 1.4.0-beta.9 → 1.4.0

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 (198) hide show
  1. package/README.md +14 -3
  2. package/dist/{AlertBox-B2Bgr3ei.js → AlertBox-DwgNTEqf.js} +3 -3
  3. package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-DwgNTEqf.js.map} +1 -1
  4. package/dist/{AppSidebar-Dq9W1jE2.js → AppSidebar-CZZaxni0.js} +1103 -144
  5. package/dist/AppSidebar-CZZaxni0.js.map +1 -0
  6. package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CjWoxAD1.js} +2 -2
  7. package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
  8. package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-DujiuQIO.js} +5 -5
  9. package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-DujiuQIO.js.map} +1 -1
  10. package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-B4j4eUOJ.js} +114 -98
  11. package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
  12. package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-DyUPH3I5.js} +2 -2
  13. package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
  14. package/dist/{SettingsModal-483nyI6b.js → SettingsModal-D2MOOtuO.js} +26 -25
  15. package/dist/SettingsModal-D2MOOtuO.js.map +1 -0
  16. package/dist/__stories__/experiment-helpers.d.ts +10 -0
  17. package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
  18. package/dist/__tests__/components/FloatingPanelsInModal.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFloatingPanel.test.d.ts +1 -0
  20. package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
  21. package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
  22. package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
  23. package/dist/components/AppTopBar.vue.d.ts +20 -5
  24. package/dist/components/BaseButton.vue.d.ts +4 -2
  25. package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
  26. package/dist/components/JobsStatusTray.vue.d.ts +15 -4
  27. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  28. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  29. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
  30. package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
  31. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
  32. package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +2 -2
  33. package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
  34. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  35. package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
  36. package/dist/composables/useFileImport.d.ts +7 -0
  37. package/dist/composables/useFloatingPanel.d.ts +29 -0
  38. package/dist/composables/useJobCenter.d.ts +41 -3
  39. package/dist/composables/useOnboarding.d.ts +119 -0
  40. package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
  41. package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
  42. package/dist/{data-CgILxDLW.js → data-Boeh4uPI.js} +13 -12
  43. package/dist/{data-CgILxDLW.js.map → data-Boeh4uPI.js.map} +1 -1
  44. package/dist/data.d.ts +1 -1
  45. package/dist/data.js +9 -9
  46. package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
  47. package/dist/experiment-DyO54FQB.js.map +1 -0
  48. package/dist/{experimentStatus-Cqp6qNrR.js → experimentStatus-DCn2QKG7.js} +2 -2
  49. package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
  50. package/dist/index.js +20 -20
  51. package/dist/{install-BtLWTk6H.js → install-BEHp7Mo-.js} +15 -15
  52. package/dist/{install-BtLWTk6H.js.map → install-BEHp7Mo-.js.map} +1 -1
  53. package/dist/install.js +1 -1
  54. package/dist/{lab-MHzp-_J8.js → lab-D61eaX6f.js} +68 -61
  55. package/dist/{lab-MHzp-_J8.js.map → lab-D61eaX6f.js.map} +1 -1
  56. package/dist/lab.js +4 -4
  57. package/dist/{records-B6gpM_3e.js → records-D4aQXyZu.js} +193 -133
  58. package/dist/records-D4aQXyZu.js.map +1 -0
  59. package/dist/{runtime-7vfl_bI3.js → runtime-q-R0CNLh.js} +400 -33
  60. package/dist/runtime-q-R0CNLh.js.map +1 -0
  61. package/dist/runtime.d.ts +1 -1
  62. package/dist/runtime.js +7 -7
  63. package/dist/{shell-Cs9OJMnm.js → shell-6CGFvUUw.js} +10 -5
  64. package/dist/{shell-Cs9OJMnm.js.map → shell-6CGFvUUw.js.map} +1 -1
  65. package/dist/shell.d.ts +1 -0
  66. package/dist/shell.js +6 -6
  67. package/dist/stores/index.d.ts +1 -1
  68. package/dist/stores/index.js +2 -2
  69. package/dist/stores/settings.d.ts +4 -12
  70. package/dist/styles.css +1191 -479
  71. package/dist/types/components.d.ts +2 -0
  72. package/dist/types/index.d.ts +1 -1
  73. package/dist/types/jobs.d.ts +9 -0
  74. package/dist/types/platform.d.ts +1 -1
  75. package/dist/{useDropdownState-8yWMu-7_.js → useDropdownState-uFIz6Yv8.js} +16 -4
  76. package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
  77. package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-QEFoq4tf.js} +48 -6
  78. package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
  79. package/dist/{useListReorder-BitdUhxn.js → useListReorder-DsuzwWGS.js} +122 -172
  80. package/dist/useListReorder-DsuzwWGS.js.map +1 -0
  81. package/dist/{useRackEditor-DiwCt77M.js → useRackEditor-KpZ4iJ6o.js} +22 -32
  82. package/dist/useRackEditor-KpZ4iJ6o.js.map +1 -0
  83. package/dist/useRealtime-B58Lo-Tm.js +508 -0
  84. package/dist/useRealtime-B58Lo-Tm.js.map +1 -0
  85. package/dist/{useRequest-D506eHi8.js → useRequest-BuDmW8Jc.js} +8 -37
  86. package/dist/useRequest-BuDmW8Jc.js.map +1 -0
  87. package/package.json +3 -2
  88. package/src/__stories__/experiment-helpers.ts +3 -3
  89. package/src/__tests__/components/ActionMenu.test.ts +2 -2
  90. package/src/__tests__/components/AppTopBar.test.ts +206 -1
  91. package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
  92. package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
  93. package/src/__tests__/components/BaseButton.test.ts +19 -0
  94. package/src/__tests__/components/BaseModal.test.ts +13 -0
  95. package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
  96. package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
  97. package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
  98. package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
  99. package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
  100. package/src/__tests__/components/SampleSelector.test.ts +20 -0
  101. package/src/__tests__/components/SettingsModal.test.ts +9 -2
  102. package/src/__tests__/composables/useFileImport.test.ts +36 -0
  103. package/src/__tests__/composables/useFloatingPanel.test.ts +68 -0
  104. package/src/__tests__/composables/useJobCenter.test.ts +262 -3
  105. package/src/__tests__/composables/useOnboarding.test.ts +317 -0
  106. package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
  107. package/src/__tests__/fixtures/table-import/cells.xlsx +0 -0
  108. package/src/__tests__/fixtures/table-import/samples.xlsx +0 -0
  109. package/src/__tests__/stores/settings.test.ts +21 -21
  110. package/src/__tests__/styles/cssTokens.test.ts +4 -1
  111. package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
  112. package/src/__tests__/styles/paletteTokens.test.ts +44 -0
  113. package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
  114. package/src/components/ActionMenu.vue +6 -37
  115. package/src/components/AppTopBar.onboarding.story.vue +69 -0
  116. package/src/components/AppTopBar.story.vue +59 -4
  117. package/src/components/AppTopBar.vue +132 -10
  118. package/src/components/BaseButton.vue +16 -2
  119. package/src/components/BaseModal.vue +2 -1
  120. package/src/components/ExperimentSelectorModal.story.vue +38 -0
  121. package/src/components/ExperimentSelectorModal.vue +20 -1
  122. package/src/components/GeneratedPluginView.vue +4 -9
  123. package/src/components/GeneratedResultRenderer.vue +3 -4
  124. package/src/components/JobsStatusTray.story.vue +97 -43
  125. package/src/components/JobsStatusTray.vue +287 -290
  126. package/src/components/LayoutResizeHandle.story.vue +65 -4
  127. package/src/components/PluginWorkspaceView.props.ts +3 -0
  128. package/src/components/PluginWorkspaceView.vue +17 -0
  129. package/src/components/SampleSelector.story.vue +21 -6
  130. package/src/components/SampleSelector.vue +10 -3
  131. package/src/components/SectionCard.story.vue +33 -0
  132. package/src/components/SettingsModal.vue +7 -12
  133. package/src/components/Tooltip.vue +9 -32
  134. package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
  135. package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
  136. package/src/components/internal/DatePickerDateInternal.vue +7 -45
  137. package/src/components/internal/DatePickerDateTimeInternal.vue +7 -1
  138. package/src/components/internal/DatePickerTimeInternal.vue +8 -1
  139. package/src/components/internal/NumberUnitInputInternal.vue +4 -18
  140. package/src/components/internal/OnboardingLayerInternal.vue +173 -0
  141. package/src/components/internal/SelectMultipleInternal.vue +6 -1
  142. package/src/components/internal/SelectSearchableInternal.vue +6 -1
  143. package/src/components/internal/WellEditPopupInternal.vue +11 -22
  144. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  145. package/src/composables/pluginWorkspaceModel.ts +2 -2
  146. package/src/composables/useFileImport.ts +51 -1
  147. package/src/composables/useFloatingPanel.ts +107 -0
  148. package/src/composables/useJobCenter.ts +188 -6
  149. package/src/composables/useOnboarding.ts +465 -0
  150. package/src/composables/usePluginWorkspace.ts +1 -1
  151. package/src/data.ts +1 -0
  152. package/src/runtime.ts +2 -1
  153. package/src/shell.ts +17 -0
  154. package/src/stores/index.ts +0 -2
  155. package/src/stores/settings.ts +20 -50
  156. package/src/styles/components/action-menu.css +1 -1
  157. package/src/styles/components/app-layout.css +6 -3
  158. package/src/styles/components/app-page-selector.css +120 -45
  159. package/src/styles/components/app-top-bar.css +165 -3
  160. package/src/styles/components/button.css +8 -0
  161. package/src/styles/components/dataframe.css +1 -1
  162. package/src/styles/components/date-picker.css +4 -2
  163. package/src/styles/components/datetime-picker.css +4 -4
  164. package/src/styles/components/experiment-selector-modal.css +13 -0
  165. package/src/styles/components/jobs-status-tray.css +134 -296
  166. package/src/styles/components/layout-resize-handle.css +59 -16
  167. package/src/styles/components/modal.css +3 -3
  168. package/src/styles/components/number-input.css +1 -1
  169. package/src/styles/components/onboarding.css +368 -0
  170. package/src/styles/components/progress-bar.css +3 -3
  171. package/src/styles/components/sample-selector.css +43 -0
  172. package/src/styles/components/searchable-select.css +3 -7
  173. package/src/styles/components/section-card.css +30 -6
  174. package/src/styles/components/settings-modal.css +7 -2
  175. package/src/styles/components/time-picker.css +3 -6
  176. package/src/styles/components/toast.css +1 -1
  177. package/src/styles/components/tooltip.css +2 -2
  178. package/src/styles/components/unit-input.css +1 -1
  179. package/src/styles/components/well-edit-popup.css +2 -2
  180. package/src/styles/index.css +1 -0
  181. package/src/styles/variables.css +153 -15
  182. package/src/types/components.ts +2 -0
  183. package/src/types/index.ts +1 -0
  184. package/src/types/jobs.ts +10 -0
  185. package/src/types/platform.ts +1 -1
  186. package/dist/AppSidebar-Dq9W1jE2.js.map +0 -1
  187. package/dist/ExperimentSelectorModal-65peG4Up.js.map +0 -1
  188. package/dist/SettingsModal-483nyI6b.js.map +0 -1
  189. package/dist/experiment-43OJtH7R.js.map +0 -1
  190. package/dist/records-B6gpM_3e.js.map +0 -1
  191. package/dist/runtime-7vfl_bI3.js.map +0 -1
  192. package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
  193. package/dist/useExperimentSamples-DHnxKFAF.js.map +0 -1
  194. package/dist/useListReorder-BitdUhxn.js.map +0 -1
  195. package/dist/useRackEditor-DiwCt77M.js.map +0 -1
  196. package/dist/useRealtime-C3iy9QkU.js +0 -699
  197. package/dist/useRealtime-C3iy9QkU.js.map +0 -1
  198. package/dist/useRequest-D506eHi8.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import { mount } from '@vue/test-utils'
2
+ import { nextTick } from 'vue'
2
3
  import { describe, expect, it } from 'vitest'
3
4
  import AppTopBarPageSelectorInternal from '../../components/internal/AppTopBarPageSelectorInternal.vue'
4
5
 
@@ -24,26 +25,139 @@ describe('AppTopBarPageSelectorInternal', () => {
24
25
  expect(wrapper.emitted('select')).toEqual([[{ id: 'Results', label: 'Results' }]])
25
26
  })
26
27
 
27
- it('omits page hints from the menu for compact scanning', async () => {
28
+ it('should show each page hint under its label when the page has one', async () => {
28
29
  const wrapper = mount(AppTopBarPageSelectorInternal, {
29
30
  props: {
30
31
  currentPageId: 'downloads',
31
32
  pages: [
32
33
  { id: 'downloads', label: 'Downloads', hint: 'Paste links and queue downloads' },
33
- { id: 'settings', label: 'Settings', hint: 'Credentials and destination' },
34
+ { id: 'settings', label: 'Settings' },
34
35
  ],
35
36
  },
36
37
  })
37
38
 
38
39
  await wrapper.get('.mint-page-selector__trigger').trigger('click')
39
40
 
40
- expect(wrapper.findAll('.mint-page-selector__item-label').map(item => item.text())).toEqual([
41
- 'Downloads',
42
- 'Settings',
41
+ const rows = wrapper.findAll('.mint-page-selector__item')
42
+ expect(rows.map(row => row.find('.mint-page-selector__item-hint').exists() ? row.get('.mint-page-selector__item-hint').text() : null)).toEqual([
43
+ 'Paste links and queue downloads',
44
+ null,
43
45
  ])
44
- expect(wrapper.find('.mint-page-selector__item-hint').exists()).toBe(false)
45
- expect(wrapper.text()).not.toContain('Paste links')
46
- expect(wrapper.text()).not.toContain('Credentials')
46
+ })
47
+
48
+ it.each([
49
+ { pluginName: 'LEAF', crumb: ['LEAF', '/'], title: 'LEAF pages' },
50
+ { pluginName: undefined, crumb: [], title: 'Pages' },
51
+ ])('should name the menu after the plugin when pluginName is $pluginName', async ({ pluginName, crumb, title }) => {
52
+ const wrapper = mount(AppTopBarPageSelectorInternal, {
53
+ props: { pluginName, currentPageId: 'Extract', pages: ['Extract', 'Analysis'] },
54
+ })
55
+
56
+ expect(wrapper.findAll('.mint-page-selector__plugin, .mint-page-selector__slash').map(el => el.text())).toEqual(crumb)
57
+
58
+ await wrapper.get('.mint-page-selector__trigger').trigger('click')
59
+
60
+ expect(wrapper.get('.mint-page-selector__menu-title').text()).toBe(title)
61
+ expect(wrapper.get('[role="menu"]').attributes('aria-label')).toBe(title)
62
+ })
63
+
64
+ it('should show number keys for the first nine pages only', async () => {
65
+ const pages = Array.from({ length: 10 }, (_, i) => `Page ${i + 1}`)
66
+ const wrapper = mount(AppTopBarPageSelectorInternal, { props: { pages } })
67
+
68
+ await wrapper.get('.mint-page-selector__trigger').trigger('click')
69
+
70
+ expect(wrapper.findAll('.mint-page-selector__item').map(row => row.find('.mint-page-selector__item-key').exists() ? row.get('.mint-page-selector__item-key').text() : null)).toEqual(
71
+ ['1', '2', '3', '4', '5', '6', '7', '8', '9', null],
72
+ )
73
+ })
74
+
75
+ describe('keyboard', () => {
76
+ const pages = [
77
+ { id: 'check', label: 'Quick check' },
78
+ { id: 'extract', label: 'Extract' },
79
+ { id: 'locked', label: 'Locked', disabled: true },
80
+ { id: 'analysis', label: 'Analysis' },
81
+ ]
82
+ const focusedLabel = () => document.activeElement?.querySelector('.mint-page-selector__item-label')?.textContent
83
+
84
+ function mountAttached() {
85
+ return mount(AppTopBarPageSelectorInternal, {
86
+ props: { pages, currentPageId: 'extract' },
87
+ attachTo: document.body,
88
+ })
89
+ }
90
+
91
+ it.each(['ArrowDown', 'Enter', ' '])('should open the menu on the current row when %j is pressed on the trigger', async (key) => {
92
+ const wrapper = mountAttached()
93
+
94
+ await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key })
95
+ await nextTick()
96
+
97
+ expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(true)
98
+ expect(focusedLabel()).toBe('Extract')
99
+ wrapper.unmount()
100
+ })
101
+
102
+ it('should move between enabled rows with arrows, Home and End', async () => {
103
+ const wrapper = mountAttached()
104
+ await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
105
+ await nextTick()
106
+ const menu = wrapper.get('.mint-page-selector__menu')
107
+
108
+ const visited: Array<string | null | undefined> = []
109
+ for (const key of ['ArrowDown', 'ArrowDown', 'ArrowUp', 'Home', 'ArrowUp', 'End']) {
110
+ await menu.trigger('keydown', { key })
111
+ visited.push(focusedLabel())
112
+ }
113
+
114
+ expect(visited).toEqual(['Analysis', 'Quick check', 'Analysis', 'Quick check', 'Analysis', 'Analysis'])
115
+ wrapper.unmount()
116
+ })
117
+
118
+ it.each(['Enter', ' '])('should activate the focused row and refocus the trigger when %j is pressed', async (key) => {
119
+ const wrapper = mountAttached()
120
+ await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
121
+ await nextTick()
122
+ const menu = wrapper.get('.mint-page-selector__menu')
123
+
124
+ await menu.trigger('keydown', { key: 'ArrowDown' })
125
+ await menu.trigger('keydown', { key })
126
+
127
+ expect(wrapper.emitted('select')).toEqual([[pages[3]]])
128
+ expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(false)
129
+ expect(document.activeElement).toBe(wrapper.get('.mint-page-selector__trigger').element)
130
+ wrapper.unmount()
131
+ })
132
+
133
+ it.each([
134
+ { key: '4', emitted: [[pages[3]]], open: false },
135
+ { key: '3', emitted: undefined, open: true },
136
+ { key: '9', emitted: undefined, open: true },
137
+ ])('should handle digit $key as a jump to that page', async ({ key, emitted, open }) => {
138
+ const wrapper = mountAttached()
139
+ await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
140
+ await nextTick()
141
+
142
+ await wrapper.get('.mint-page-selector__menu').trigger('keydown', { key })
143
+
144
+ expect(wrapper.emitted('select')).toEqual(emitted)
145
+ expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(open)
146
+ wrapper.unmount()
147
+ })
148
+
149
+ it('should close on Escape and return focus to the trigger', async () => {
150
+ const wrapper = mountAttached()
151
+ await wrapper.get('.mint-page-selector__trigger').trigger('keydown', { key: 'ArrowDown' })
152
+ await nextTick()
153
+
154
+ await wrapper.get('.mint-page-selector__menu').trigger('keydown', { key: 'Escape' })
155
+ await nextTick()
156
+
157
+ expect(wrapper.find('.mint-page-selector__menu').exists()).toBe(false)
158
+ expect(document.activeElement).toBe(wrapper.get('.mint-page-selector__trigger').element)
159
+ wrapper.unmount()
160
+ })
47
161
  })
48
162
 
49
163
  it('closes linked page actions without emitting select', async () => {
@@ -171,6 +285,8 @@ describe('AppTopBarPageSelectorInternal', () => {
171
285
 
172
286
  await wrapper.get('.mint-page-selector__trigger').trigger('click')
173
287
 
174
- expect(wrapper.findAll('.mint-page-selector__item')[0].classes()).toContain('mint-page-selector__item--active')
288
+ const rows = wrapper.findAll('.mint-page-selector__item')
289
+ expect(rows[0].classes()).toContain('mint-page-selector__item--active')
290
+ expect(rows.map(row => row.attributes('aria-current'))).toEqual(['page', undefined])
175
291
  })
176
292
  })
@@ -35,6 +35,25 @@ describe('BaseButton icon', () => {
35
35
  })
36
36
  })
37
37
 
38
+ // Text-only labels get a span so the stylesheet can trim the text box to the cap
39
+ // height (Geist sits ~1.6px high in a flex-centred button otherwise). Element
40
+ // children keep their flex layout, so they are not wrapped.
41
+ describe('BaseButton label', () => {
42
+ it.each([
43
+ ['plain text', 'Cancel', 'Cancel'],
44
+ ['element children', '<svg /><span>Export</span>', null],
45
+ ])('should wrap the label in a span only for a %s slot', (_case, slot, label) => {
46
+ const wrapper = mount(BaseButton, { slots: { default: slot } })
47
+ const span = wrapper.find('.mint-button__label')
48
+ expect(span.exists() ? span.text() : null).toBe(label)
49
+ })
50
+
51
+ it('should trim the label text box to the cap height', () => {
52
+ const css = readFileSync(resolve(__dirname, '../../styles/components/button.css'), 'utf8')
53
+ expect(css).toMatch(/\.mint-button__label \{\s*text-box: trim-both cap alphabetic;\s*\}/)
54
+ })
55
+ })
56
+
38
57
  // jsdom has no layout, so this pins the stylesheet contract instead: a bare
39
58
  // <svg> rendered by an icon-only BaseButton must be sized by the icon box, otherwise it
40
59
  // collapses to zero width as a flex item (FilePicker source toggles, beta.14).
@@ -196,6 +196,19 @@ describe('BaseModal', () => {
196
196
  inner.unmount()
197
197
  })
198
198
 
199
+ it('should stay open when a control inside it already handled the Escape', () => {
200
+ const wrapper = mount(BaseModal, {
201
+ props: { open: true, title: 'Settings' },
202
+ slots: { default: '<input class="handled" @keydown.escape.prevent>' },
203
+ attachTo: document.body,
204
+ })
205
+ document.querySelector('.handled')!.dispatchEvent(
206
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }),
207
+ )
208
+ expect(wrapper.emitted('update:open')).toBeUndefined()
209
+ wrapper.unmount()
210
+ })
211
+
199
212
  it('should focus the first focusable element when mounted already open', async () => {
200
213
  const attached = mount(BaseModal, {
201
214
  props: { open: true, title: 'Settings' },
@@ -64,9 +64,10 @@ interface ModalProps {
64
64
  currentExperimentId?: number | null
65
65
  }
66
66
 
67
- function mountModal(props: ModalProps) {
67
+ function mountModal(props: ModalProps, slots: Record<string, string> = {}) {
68
68
  return mount(ExperimentSelectorModal, {
69
69
  props,
70
+ slots,
70
71
  global: {
71
72
  stubs: {
72
73
  BaseModal: {
@@ -202,6 +203,38 @@ describe('ExperimentSelectorModal', () => {
202
203
  expect(wrapper.emitted('update:open')).toEqual([[false]])
203
204
  })
204
205
 
206
+ it('should show the description slot above the experiment list', () => {
207
+ const wrapper = mountModal({ open: true }, { description: '<p class="why">Keep the result with its samples.</p>' })
208
+
209
+ const description = wrapper.get('.mint-experiment-selector__description')
210
+ expect(description.find('.why').text()).toBe('Keep the result with its samples.')
211
+ expect(description.element.compareDocumentPosition(wrapper.get('.mint-experiment-selector').element))
212
+ .toBe(Node.DOCUMENT_POSITION_FOLLOWING)
213
+ })
214
+
215
+ it.each([
216
+ [null, false],
217
+ [7, true],
218
+ ])('should render the footer slot when the current experiment is %s, with the clear button %s', (currentExperimentId, hasClear) => {
219
+ const wrapper = mountModal({ open: true, currentExperimentId }, { footer: '<span class="extra">Skip</span>' })
220
+
221
+ const footer = wrapper.get('.mint-experiment-selector__footer')
222
+ expect(footer.find('.extra').exists()).toBe(true)
223
+ expect(footer.find('.mint-experiment-selector__clear-btn').exists()).toBe(hasClear)
224
+ })
225
+
226
+ it('should close without touching the store from the footer slot close()', async () => {
227
+ const store = useExperimentStore()
228
+ const wrapper = mountModal({ open: true }, {
229
+ footer: '<template #footer="{ close }"><button class="skip" @click="close">Skip</button></template>',
230
+ })
231
+
232
+ await wrapper.get('.skip').trigger('click')
233
+
234
+ expect(wrapper.emitted('update:open')).toEqual([[false]])
235
+ expect(store.current).toBeNull()
236
+ })
237
+
205
238
  it('should list each status with its count and check the current one', async () => {
206
239
  statusCounts.value = { ongoing: 3, planned: 1, completed: 0, cancelled: 2 }
207
240
  filters.status = 'ongoing'
@@ -0,0 +1,94 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { defineComponent, nextTick } from 'vue'
3
+ import { afterEach, describe, expect, it } from 'vitest'
4
+ import BaseModal from '../../components/BaseModal.vue'
5
+ import BaseSelect from '../../components/BaseSelect.vue'
6
+ import DatePicker from '../../components/DatePicker.vue'
7
+ import WellEditPopupInternal from '../../components/internal/WellEditPopupInternal.vue'
8
+
9
+ const options = ['Orbitrap Exploris 480', 'Q Exactive HF', 'Vanquish UHPLC', 'Exploris 240', 'timsTOF Pro 2']
10
+
11
+ // [name, control markup, component root, trigger selector, panel selector]
12
+ const movedPanels = [
13
+ ['multiple select', '<BaseSelect multiple :model-value="[]" :options="options" />', '.mint-multi-select', '.mint-multi-select__field', '.mint-searchable-select__panel'],
14
+ ['searchable select', '<BaseSelect searchable :options="options" />', '.mint-searchable-select', '.mint-searchable-select__trigger', '.mint-searchable-select__panel'],
15
+ ['date-time picker', '<DatePicker type="datetime" />', '.mint-datetime-picker', '.mint-datetime-picker__input', '.mint-datetime-picker__dropdown'],
16
+ ['time picker', '<DatePicker type="time" />', '.mint-time-picker', '.mint-time-picker__input', '.mint-time-picker__dropdown'],
17
+ ] as const
18
+
19
+ async function settle() {
20
+ for (let i = 0; i < 3; i++) await nextTick()
21
+ }
22
+
23
+ describe('floating panels inside BaseModal', () => {
24
+ afterEach(() => {
25
+ document.body.innerHTML = ''
26
+ })
27
+
28
+ it.each(movedPanels)(
29
+ 'should keep the %s panel in its root after the trigger and position it fixed, so the modal body cannot clip it',
30
+ async (_, control, rootSelector, triggerSelector, panelSelector) => {
31
+ mount(defineComponent({
32
+ components: { BaseModal, BaseSelect, DatePicker },
33
+ setup: () => ({ options }),
34
+ template: `<BaseModal open title="New service token">${control}</BaseModal>`,
35
+ }), { attachTo: document.body })
36
+ await settle()
37
+
38
+ document.querySelector<HTMLElement>(triggerSelector)!.click()
39
+ await settle()
40
+
41
+ const panel = document.querySelector<HTMLElement>(panelSelector)!
42
+ const root = document.querySelector(rootSelector)!
43
+ expect(root.contains(panel)).toBe(true)
44
+ expect(root.querySelector(triggerSelector)!.compareDocumentPosition(panel) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy()
45
+ expect(panel.style.position).toBe('fixed')
46
+ },
47
+ )
48
+ })
49
+
50
+ describe('Escape in a calendar panel inside BaseModal', () => {
51
+ afterEach(() => {
52
+ document.body.innerHTML = ''
53
+ })
54
+
55
+ it.each([
56
+ ['date', '.mint-date-picker__input', '.mint-date-picker__dropdown'],
57
+ ['datetime', '.mint-datetime-picker__input', '.mint-datetime-picker__dropdown'],
58
+ ])('should close only the %s panel when Escape is pressed in it', async (type, triggerSelector, panelSelector) => {
59
+ const wrapper = mount(defineComponent({
60
+ components: { BaseModal, DatePicker },
61
+ emits: ['close'],
62
+ template: `<BaseModal open title="Edit experiment" @update:open="$emit('close')"><DatePicker type="${type}" /></BaseModal>`,
63
+ }), { attachTo: document.body })
64
+ await settle()
65
+ document.querySelector<HTMLElement>(triggerSelector)!.click()
66
+ await settle()
67
+
68
+ document.querySelector(panelSelector)!.querySelector('button')!.dispatchEvent(
69
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }),
70
+ )
71
+ await settle()
72
+
73
+ expect(document.querySelector(panelSelector)).toBeNull()
74
+ expect(wrapper.emitted('close')).toBeUndefined()
75
+ })
76
+
77
+ it('should close only the well popup when Escape is pressed in it', async () => {
78
+ const wrapper = mount(defineComponent({
79
+ components: { BaseModal, WellEditPopupInternal },
80
+ emits: ['close', 'popupClose'],
81
+ template: `<BaseModal open title="Plate" @update:open="$emit('close')">
82
+ <WellEditPopupInternal well-id="A1" :position="{ x: 40, y: 40 }" @close="$emit('popupClose')" />
83
+ </BaseModal>`,
84
+ }), { attachTo: document.body })
85
+ await settle()
86
+
87
+ document.querySelector('.mint-well-edit-popup input')!.dispatchEvent(
88
+ new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }),
89
+ )
90
+
91
+ expect(wrapper.emitted('popupClose')).toHaveLength(1)
92
+ expect(wrapper.emitted('close')).toBeUndefined()
93
+ })
94
+ })
@@ -147,7 +147,7 @@ describe('GeneratedPluginView', () => {
147
147
  workspace: wrapper.findComponent(PluginWorkspaceView).props(),
148
148
  result: wrapper.findComponent(GeneratedResultRenderer).text(),
149
149
  jobs: wrapper.findComponent(JobsStatusTray).exists(),
150
- jobSource: wrapper.findComponent(JobsStatusTray).props('source'),
150
+ jobSource: wrapper.findComponent(PluginWorkspaceView).props('spec')?.jobs,
151
151
  }).toMatchObject({
152
152
  workspace: {
153
153
  spec: {