@morscherlab/mint-sdk 1.4.0-beta.2 → 1.4.0-beta.20

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 (330) hide show
  1. package/README.md +14 -3
  2. package/dist/{ActionMenu-CScINahU.js → AlertBox-CzLg0UaL.js} +40 -209
  3. package/dist/AlertBox-CzLg0UaL.js.map +1 -0
  4. package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-6oTT-gPO.js} +1249 -239
  5. package/dist/AppSidebar-6oTT-gPO.js.map +1 -0
  6. package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-CSls2Nuv.js} +2 -2
  7. package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-CSls2Nuv.js.map} +1 -1
  8. package/dist/ExperimentPopover-9l4QZlvQ.js +337 -0
  9. package/dist/ExperimentPopover-9l4QZlvQ.js.map +1 -0
  10. package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-pR5fK3pA.js} +244 -211
  11. package/dist/ExperimentSelectorModal-pR5fK3pA.js.map +1 -0
  12. package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-CJ7EAmOq.js} +2 -2
  13. package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-CJ7EAmOq.js.map} +1 -1
  14. package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-Dyitgzfp.js} +28 -27
  15. package/dist/SettingsModal-Dyitgzfp.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__/composables/useOnboarding.test.d.ts +1 -0
  19. package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
  20. package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
  21. package/dist/components/AppSidebar.vue.d.ts +1 -1
  22. package/dist/components/AppTopBar.vue.d.ts +20 -5
  23. package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
  24. package/dist/components/AuditTrail.vue.d.ts +1 -1
  25. package/dist/components/BaseButton.vue.d.ts +4 -2
  26. package/dist/components/BaseSelect.vue.d.ts +2 -2
  27. package/dist/components/ChartContainer.vue.d.ts +2 -0
  28. package/dist/components/DatePicker.vue.d.ts +1 -1
  29. package/dist/components/ExperimentPopover.vue.d.ts +18 -0
  30. package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -5
  31. package/dist/components/FilePicker.vue.d.ts +1 -1
  32. package/dist/components/FileUploader.vue.d.ts +2 -2
  33. package/dist/components/JobsStatusTray.vue.d.ts +15 -4
  34. package/dist/components/PluginIcon.vue.d.ts +2 -2
  35. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  36. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  37. package/dist/components/SampleSelector.vue.d.ts +6 -6
  38. package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
  39. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  40. package/dist/components/StepWizard.vue.d.ts +3 -0
  41. package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
  42. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
  43. package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +1 -1
  44. package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
  45. package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
  46. package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
  47. package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
  48. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  49. package/dist/composables/createClient.d.ts +2 -0
  50. package/dist/composables/index.d.ts +1 -0
  51. package/dist/composables/pluginIdentity.d.ts +24 -0
  52. package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
  53. package/dist/composables/useAppExperiment.d.ts +8 -0
  54. package/dist/composables/useCalendarGrid.d.ts +1 -1
  55. package/dist/composables/useExperimentSelector.d.ts +6 -2
  56. package/dist/composables/useJobCenter.d.ts +41 -3
  57. package/dist/composables/useOnboarding.d.ts +119 -0
  58. package/dist/composables/usePlatform.d.ts +1 -0
  59. package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-DCUOx8ll.js} +2 -2
  60. package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-DCUOx8ll.js.map} +1 -1
  61. package/dist/{data-C2PwI_dn.js → data-DBnmeBZX.js} +179 -125
  62. package/dist/data-DBnmeBZX.js.map +1 -0
  63. package/dist/data.js +8 -8
  64. package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
  65. package/dist/experiment-DyO54FQB.js.map +1 -0
  66. package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-BFrKpHAH.js} +3 -3
  67. package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-BFrKpHAH.js.map} +1 -1
  68. package/dist/index.js +20 -20
  69. package/dist/{install-DAID6c2-.js → install-qjELyNwc.js} +15 -14
  70. package/dist/{install-DAID6c2-.js.map → install-qjELyNwc.js.map} +1 -1
  71. package/dist/install.js +1 -1
  72. package/dist/{lab-DLwRl_Ql.js → lab-UyTbokQC.js} +136 -148
  73. package/dist/lab-UyTbokQC.js.map +1 -0
  74. package/dist/lab.js +4 -3
  75. package/dist/{records-BzTcxekG.js → records-CR_pYzIP.js} +533 -335
  76. package/dist/records-CR_pYzIP.js.map +1 -0
  77. package/dist/{runtime-D6gVat6r.js → runtime-CoHhBP10.js} +417 -54
  78. package/dist/runtime-CoHhBP10.js.map +1 -0
  79. package/dist/runtime.d.ts +1 -1
  80. package/dist/runtime.js +7 -7
  81. package/dist/{shell-5ju97nOS.js → shell-BDuZf2zb.js} +10 -5
  82. package/dist/{shell-5ju97nOS.js.map → shell-BDuZf2zb.js.map} +1 -1
  83. package/dist/shell.d.ts +1 -0
  84. package/dist/shell.js +6 -6
  85. package/dist/stores/index.d.ts +1 -1
  86. package/dist/stores/index.js +2 -2
  87. package/dist/stores/settings.d.ts +4 -12
  88. package/dist/styles.css +2732 -2083
  89. package/dist/types/componentWorkflowTypes.d.ts +2 -0
  90. package/dist/types/components.d.ts +2 -0
  91. package/dist/types/generated/platformContext.d.ts +1 -0
  92. package/dist/types/generated-ui.d.ts +4 -0
  93. package/dist/types/index.d.ts +1 -1
  94. package/dist/types/jobs.d.ts +9 -0
  95. package/dist/types/platform.d.ts +1 -1
  96. package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-Bb2N4m40.js} +74 -4
  97. package/dist/useDropdownState-Bb2N4m40.js.map +1 -0
  98. package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-Dno1fFMq.js} +4 -4
  99. package/dist/{useExperimentSamples-CxcnxZ8h.js.map → useExperimentSamples-Dno1fFMq.js.map} +1 -1
  100. package/dist/{useListReorder-CcbEYxX-.js → useListReorder-CQ9OCScc.js} +183 -351
  101. package/dist/useListReorder-CQ9OCScc.js.map +1 -0
  102. package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-wL3kPrRT.js} +73 -59
  103. package/dist/useRackEditor-wL3kPrRT.js.map +1 -0
  104. package/dist/useRealtime-C3whhVvb.js +508 -0
  105. package/dist/useRealtime-C3whhVvb.js.map +1 -0
  106. package/dist/useRequest-CDkrXNU7.js +244 -0
  107. package/dist/useRequest-CDkrXNU7.js.map +1 -0
  108. package/dist/utils/time.d.ts +7 -0
  109. package/package.json +1 -1
  110. package/src/__stories__/experiment-helpers.ts +3 -3
  111. package/src/__tests__/components/AppTopBar.test.ts +257 -3
  112. package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
  113. package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
  114. package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
  115. package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
  116. package/src/__tests__/components/BaseButton.test.ts +19 -0
  117. package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
  118. package/src/__tests__/components/BaseSelect.test.ts +2 -1
  119. package/src/__tests__/components/BatchProgressList.test.ts +16 -0
  120. package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
  121. package/src/__tests__/components/ChartContainer.test.ts +11 -0
  122. package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
  123. package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
  124. package/src/__tests__/components/DatePicker.test.ts +21 -0
  125. package/src/__tests__/components/DatePicker.time.test.ts +2 -2
  126. package/src/__tests__/components/EmptyState.test.ts +1 -1
  127. package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
  128. package/src/__tests__/components/ExperimentSelectorModal.test.ts +153 -14
  129. package/src/__tests__/components/GeneratedPluginView.test.ts +23 -2
  130. package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
  131. package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
  132. package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
  133. package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
  134. package/src/__tests__/components/SampleSelector.test.ts +33 -8
  135. package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
  136. package/src/__tests__/components/SettingsModal.test.ts +9 -2
  137. package/src/__tests__/components/StepWizard.test.ts +11 -0
  138. package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
  139. package/src/__tests__/components/deprecationTags.test.ts +1 -1
  140. package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
  141. package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
  142. package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
  143. package/src/__tests__/composables/useJobCenter.test.ts +262 -3
  144. package/src/__tests__/composables/useOnboarding.test.ts +317 -0
  145. package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
  146. package/src/__tests__/stores/settings.test.ts +21 -21
  147. package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
  148. package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
  149. package/src/__tests__/styles/cssTokens.test.ts +6 -3
  150. package/src/__tests__/styles/menusShellRefresh.test.ts +22 -12
  151. package/src/__tests__/styles/paletteTokens.test.ts +44 -0
  152. package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
  153. package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
  154. package/src/__tests__/utils/time.test.ts +18 -1
  155. package/src/components/AdductText.story.vue +7 -0
  156. package/src/components/AdductText.vue +1 -1
  157. package/src/components/AlertBox.story.vue +2 -2
  158. package/src/components/AlertBox.vue +18 -16
  159. package/src/components/AppSidebar.story.vue +0 -28
  160. package/src/components/AppTopBar.onboarding.story.vue +69 -0
  161. package/src/components/AppTopBar.story.vue +105 -15
  162. package/src/components/AppTopBar.vue +248 -122
  163. package/src/components/ArtifactSaveDialog.story.vue +23 -0
  164. package/src/components/ArtifactSaveDialog.vue +102 -12
  165. package/src/components/AuditTrail.story.vue +2 -1
  166. package/src/components/AuditTrail.vue +2 -21
  167. package/src/components/Avatar.vue +10 -14
  168. package/src/components/BaseButton.vue +16 -2
  169. package/src/components/BaseSelect.multiple.story.vue +13 -2
  170. package/src/components/BaseSelect.vue +4 -3
  171. package/src/components/BatchProgressList.vue +35 -25
  172. package/src/components/ChartContainer.story.vue +2 -1
  173. package/src/components/ChartContainer.vue +8 -6
  174. package/src/components/ConfirmDialog.story.vue +1 -1
  175. package/src/components/EmptyState.vue +1 -1
  176. package/src/components/ExperimentPopover.story.vue +165 -53
  177. package/src/components/ExperimentPopover.vue +169 -187
  178. package/src/components/ExperimentSelectorModal.story.vue +73 -31
  179. package/src/components/ExperimentSelectorModal.vue +144 -111
  180. package/src/components/FormField.story.vue +8 -48
  181. package/src/components/GeneratedPluginView.vue +11 -10
  182. package/src/components/GeneratedResultRenderer.vue +3 -4
  183. package/src/components/InstrumentStateBadge.vue +1 -1
  184. package/src/components/JobsStatusTray.story.vue +97 -43
  185. package/src/components/JobsStatusTray.vue +287 -290
  186. package/src/components/LayoutResizeHandle.story.vue +65 -4
  187. package/src/components/LoadingSpinner.vue +1 -1
  188. package/src/components/NumberInput.vue +3 -6
  189. package/src/components/PluginIcon.story.vue +2 -10
  190. package/src/components/PluginIcon.vue +2 -2
  191. package/src/components/PluginWorkspaceView.props.ts +3 -0
  192. package/src/components/PluginWorkspaceView.story.vue +1 -1
  193. package/src/components/PluginWorkspaceView.vue +17 -0
  194. package/src/components/RuntimeUpdateNotice.vue +1 -1
  195. package/src/components/SampleSelector.story.vue +22 -7
  196. package/src/components/SampleSelector.vue +34 -37
  197. package/src/components/SampleSelectorGroupNode.vue +1 -1
  198. package/src/components/SectionCard.story.vue +35 -2
  199. package/src/components/SequenceProgressBar.story.vue +9 -0
  200. package/src/components/SequenceProgressBar.vue +6 -1
  201. package/src/components/SettingsModal.schema.ts +1 -1
  202. package/src/components/SettingsModal.vue +9 -14
  203. package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
  204. package/src/components/SmartGroupManual.story.vue +1 -1
  205. package/src/components/StepWizard.story.vue +33 -0
  206. package/src/components/StepWizard.vue +8 -1
  207. package/src/components/WellPlate.story.vue +3 -1
  208. package/src/components/WellPlate.vue +7 -0
  209. package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
  210. package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
  211. package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
  212. package/src/components/internal/DatePickerDateInternal.vue +16 -40
  213. package/src/components/internal/DatePickerDateTimeInternal.vue +3 -13
  214. package/src/components/internal/DatePickerTimeInternal.vue +5 -8
  215. package/src/components/internal/OnboardingLayerInternal.vue +173 -0
  216. package/src/components/internal/ResourcePickerListInternal.vue +45 -85
  217. package/src/components/internal/SelectMultipleInternal.vue +182 -65
  218. package/src/components/internal/SelectSearchableInternal.vue +2 -3
  219. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  220. package/src/composables/createClient.ts +2 -0
  221. package/src/composables/index.ts +1 -0
  222. package/src/composables/pluginIdentity.ts +31 -0
  223. package/src/composables/pluginWorkspaceModel.ts +2 -2
  224. package/src/composables/useAppExperiment.ts +20 -0
  225. package/src/composables/useCalendarGrid.ts +4 -3
  226. package/src/composables/useExperimentSelector.ts +77 -31
  227. package/src/composables/useJobCenter.ts +188 -6
  228. package/src/composables/useOnboarding.ts +465 -0
  229. package/src/composables/usePluginWorkspace.ts +1 -1
  230. package/src/runtime.ts +2 -1
  231. package/src/shell.ts +17 -0
  232. package/src/stores/index.ts +0 -2
  233. package/src/stores/settings.ts +20 -50
  234. package/src/styles/components/adduct-text.css +13 -5
  235. package/src/styles/components/alert-box.css +39 -58
  236. package/src/styles/components/app-layout.css +7 -5
  237. package/src/styles/components/app-page-selector.css +121 -44
  238. package/src/styles/components/app-pill-nav.css +11 -12
  239. package/src/styles/components/app-sidebar.css +9 -18
  240. package/src/styles/components/app-top-bar.css +219 -55
  241. package/src/styles/components/artifact-save-dialog.css +153 -2
  242. package/src/styles/components/audit-trail.css +16 -19
  243. package/src/styles/components/avatar.css +6 -4
  244. package/src/styles/components/batch-progress-list.css +38 -36
  245. package/src/styles/components/button.css +36 -59
  246. package/src/styles/components/calendar.css +12 -12
  247. package/src/styles/components/chart-container.css +34 -23
  248. package/src/styles/components/checkbox.css +2 -12
  249. package/src/styles/components/collapsible-card.css +27 -29
  250. package/src/styles/components/confirm-dialog.css +13 -14
  251. package/src/styles/components/dataframe.css +13 -5
  252. package/src/styles/components/date-picker.css +80 -92
  253. package/src/styles/components/datetime-picker.css +31 -43
  254. package/src/styles/components/divider.css +20 -20
  255. package/src/styles/components/dose-calculator.css +3 -3
  256. package/src/styles/components/empty-state.css +20 -17
  257. package/src/styles/components/experiment-code-badge.css +2 -0
  258. package/src/styles/components/experiment-popover.css +174 -329
  259. package/src/styles/components/experiment-selector-modal.css +88 -56
  260. package/src/styles/components/file-picker.css +3 -3
  261. package/src/styles/components/file-uploader.css +38 -49
  262. package/src/styles/components/form-builder.css +3 -3
  263. package/src/styles/components/formula-input.css +1 -1
  264. package/src/styles/components/icon-button.css +18 -24
  265. package/src/styles/components/input.css +5 -4
  266. package/src/styles/components/instrument-monitor.css +13 -15
  267. package/src/styles/components/jobs-status-tray.css +140 -296
  268. package/src/styles/components/layout-resize-handle.css +59 -16
  269. package/src/styles/components/modal.css +4 -6
  270. package/src/styles/components/molecule-input.css +2 -2
  271. package/src/styles/components/multi-select.css +65 -117
  272. package/src/styles/components/number-input.css +26 -19
  273. package/src/styles/components/onboarding.css +368 -0
  274. package/src/styles/components/pill.css +3 -2
  275. package/src/styles/components/plate-editor.css +0 -1
  276. package/src/styles/components/plugin-icon.css +26 -23
  277. package/src/styles/components/popover.css +2 -2
  278. package/src/styles/components/presence-avatars.css +1 -1
  279. package/src/styles/components/progress-bar.css +3 -3
  280. package/src/styles/components/protocol-step-editor.css +1 -3
  281. package/src/styles/components/resource-card.css +2 -2
  282. package/src/styles/components/resource-picker-list.css +6 -57
  283. package/src/styles/components/sample-hierarchy-tree.css +4 -4
  284. package/src/styles/components/sample-selector.css +138 -79
  285. package/src/styles/components/schedule-calendar.css +8 -8
  286. package/src/styles/components/searchable-select.css +12 -12
  287. package/src/styles/components/section-card.css +54 -49
  288. package/src/styles/components/segmented-control.css +18 -15
  289. package/src/styles/components/select.css +3 -3
  290. package/src/styles/components/sequence-input.css +2 -2
  291. package/src/styles/components/sequence-progress-bar.css +17 -8
  292. package/src/styles/components/settings-modal.css +7 -2
  293. package/src/styles/components/smart-group.css +18 -18
  294. package/src/styles/components/step-wizard.css +52 -16
  295. package/src/styles/components/tabs.css +2 -2
  296. package/src/styles/components/textarea.css +3 -3
  297. package/src/styles/components/time-picker.css +36 -21
  298. package/src/styles/components/toast.css +12 -10
  299. package/src/styles/components/toggle.css +1 -11
  300. package/src/styles/components/tooltip.css +10 -9
  301. package/src/styles/components/unit-input.css +8 -7
  302. package/src/styles/components/well-plate.css +9 -2
  303. package/src/styles/index.css +1 -0
  304. package/src/styles/variables.css +241 -78
  305. package/src/types/componentWorkflowTypes.ts +2 -0
  306. package/src/types/components.ts +2 -0
  307. package/src/types/generated/platformContext.ts +1 -0
  308. package/src/types/generated-ui.ts +4 -0
  309. package/src/types/index.ts +1 -0
  310. package/src/types/jobs.ts +10 -0
  311. package/src/types/platform.ts +1 -1
  312. package/src/utils/time.ts +21 -0
  313. package/dist/ActionMenu-CScINahU.js.map +0 -1
  314. package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
  315. package/dist/BaseModal-DfRIuVoj.js.map +0 -1
  316. package/dist/ExperimentPopover-C4sGw-is.js +0 -359
  317. package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
  318. package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
  319. package/dist/SettingsModal-CSLE31yg.js.map +0 -1
  320. package/dist/data-C2PwI_dn.js.map +0 -1
  321. package/dist/experiment-43OJtH7R.js.map +0 -1
  322. package/dist/lab-DLwRl_Ql.js.map +0 -1
  323. package/dist/records-BzTcxekG.js.map +0 -1
  324. package/dist/runtime-D6gVat6r.js.map +0 -1
  325. package/dist/useListReorder-CcbEYxX-.js.map +0 -1
  326. package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
  327. package/dist/useRealtime-PalKue84.js +0 -696
  328. package/dist/useRealtime-PalKue84.js.map +0 -1
  329. package/dist/useRequest-D5JDfsgZ.js +0 -76
  330. package/dist/useRequest-D5JDfsgZ.js.map +0 -1
@@ -0,0 +1,160 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
3
+ import { computed, defineComponent, h, nextTick } from 'vue'
4
+ import { createPinia } from 'pinia'
5
+ import AppTopBar from '../../components/AppTopBar.vue'
6
+ import { provideOnboarding, type OnboardingDefinition } from '../../composables/useOnboarding'
7
+
8
+ vi.mock('../../composables/usePlatform', () => ({
9
+ usePlatform: vi.fn(() => ({
10
+ integrated: computed(() => false),
11
+ isAdmin: computed(() => true),
12
+ can: () => true,
13
+ plugin: computed(() => undefined),
14
+ user: computed(() => undefined),
15
+ theme: computed(() => 'system' as const),
16
+ features: computed(() => undefined),
17
+ })),
18
+ }))
19
+
20
+ const definition: OnboardingDefinition = {
21
+ tours: [{
22
+ id: 'intro',
23
+ title: 'Top bar tour',
24
+ steps: [
25
+ { id: 'settings', anchor: 'topbar-settings', title: 'Settings live here', body: 'Theme and palette.' },
26
+ { id: 'help', anchor: 'topbar-help', title: 'Help', body: 'Replay tours.', advanceOn: 'click' },
27
+ ],
28
+ }],
29
+ checklist: [{ id: 'create', title: 'Create an experiment' }],
30
+ }
31
+
32
+ enableAutoUnmount(afterEach)
33
+
34
+ function mountTopBar(onboarding?: OnboardingDefinition) {
35
+ return mount(defineComponent({
36
+ setup() {
37
+ if (onboarding) provideOnboarding(onboarding, { appId: 'test', userId: 1, autoStart: false })
38
+ return () => h(AppTopBar, { spec: { settings: { tabs: [] } } })
39
+ },
40
+ }), { global: { plugins: [createPinia()], stubs: { 'router-link': true } }, attachTo: document.body })
41
+ }
42
+
43
+ const card = () => document.querySelector('.mint-onboarding__card')
44
+
45
+ describe('AppTopBar onboarding', () => {
46
+ beforeEach(() => localStorage.clear())
47
+
48
+ it.each([
49
+ [undefined, false],
50
+ [definition, true],
51
+ ])('should show the Help button and checklist chip when onboarding is %s', (onboarding, shown) => {
52
+ const wrapper = mountTopBar(onboarding)
53
+
54
+ expect(wrapper.find('[data-onboarding="topbar-help"]').exists()).toBe(shown)
55
+ expect(wrapper.find('.mint-onboarding-help__chip').exists()).toBe(shown)
56
+ if (shown) expect(wrapper.find('.mint-onboarding-help__chip').text()).toContain('0/1')
57
+ })
58
+
59
+ it('should run a tour picked in the Help panel and end it with Escape', async () => {
60
+ const wrapper = mountTopBar(definition)
61
+ await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
62
+ const tour = [...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!
63
+ tour.click()
64
+ await flushPromises()
65
+
66
+ expect(document.querySelector('.mint-onboarding-help__panel')).toBeNull()
67
+ expect(card()?.getAttribute('role')).toBe('dialog')
68
+ expect(card()?.textContent).toContain('Settings live here')
69
+ expect(card()?.textContent).toContain('Next')
70
+
71
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
72
+ await nextTick()
73
+
74
+ expect(card()).toBeNull()
75
+ expect(JSON.parse(localStorage.getItem('mint.onboarding.test.1')!)).toEqual(
76
+ expect.arrayContaining(['step:intro/settings', 'step:intro/help']),
77
+ )
78
+ })
79
+
80
+ it('should focus the card, not the Next button, when a step opens', async () => {
81
+ const wrapper = mountTopBar(definition)
82
+ await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
83
+ ;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
84
+ await flushPromises()
85
+
86
+ expect(document.activeElement).toBe(card())
87
+ })
88
+
89
+ it.each([
90
+ [{ key: 'Tab', shiftKey: true }, 'Next'],
91
+ [{ key: 'Tab', shiftKey: false }, ''],
92
+ ])('should keep %o from the focused card inside the card', async (init, label) => {
93
+ const wrapper = mountTopBar(definition)
94
+ await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
95
+ ;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
96
+ await flushPromises()
97
+
98
+ document.dispatchEvent(new KeyboardEvent('keydown', { ...init, bubbles: true, cancelable: true }))
99
+
100
+ expect(card()!.contains(document.activeElement)).toBe(true)
101
+ expect((document.activeElement as HTMLElement).textContent?.trim()).toBe(label)
102
+ })
103
+
104
+ it('should leave Escape and Tab to a control outside the tour that has focus', async () => {
105
+ const wrapper = mountTopBar(definition)
106
+ await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
107
+ ;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
108
+ await flushPromises()
109
+ const input = document.body.appendChild(document.createElement('input'))
110
+ input.focus()
111
+
112
+ for (const key of ['Tab', 'Escape']) document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))
113
+ await nextTick()
114
+
115
+ expect(document.activeElement).toBe(input)
116
+ expect(card()).not.toBeNull()
117
+ input.remove()
118
+ })
119
+
120
+ it('should go to the next step when Enter is pressed on the focused card', async () => {
121
+ const wrapper = mountTopBar(definition)
122
+ await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
123
+ ;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
124
+ await flushPromises()
125
+
126
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
127
+ await flushPromises()
128
+
129
+ expect(card()?.textContent).toContain('Replay tours.')
130
+ })
131
+
132
+ it('should show no Next button on an advanceOn click step', async () => {
133
+ const wrapper = mountTopBar(definition)
134
+ await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
135
+ document.querySelector<HTMLElement>('.mint-menu-item')!.click()
136
+ await flushPromises()
137
+ ;(document.querySelector('.mint-onboarding__card .mint-button') as HTMLElement).click()
138
+ await flushPromises()
139
+
140
+ expect(card()?.textContent).toContain('Click the highlighted control.')
141
+ expect(card()?.textContent).not.toContain('Done')
142
+ })
143
+
144
+ it('should keep Tab on the anchor and the card on an advanceOn click step', async () => {
145
+ const wrapper = mountTopBar(definition)
146
+ await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
147
+ document.querySelector<HTMLElement>('.mint-menu-item')!.click()
148
+ await flushPromises()
149
+ ;(document.querySelector('.mint-onboarding__card .mint-button') as HTMLElement).click()
150
+ await flushPromises()
151
+ const anchor = document.querySelector<HTMLElement>('[data-onboarding="topbar-help"]')!
152
+ expect(document.activeElement).toBe(anchor)
153
+
154
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
155
+
156
+ expect(card()?.contains(document.activeElement)).toBe(true)
157
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }))
158
+ expect(document.activeElement).toBe(anchor)
159
+ })
160
+ })
@@ -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
  })
@@ -196,4 +196,39 @@ describe('AppTopBarPillNavInternal', () => {
196
196
 
197
197
  expect(wrapper.find('.mint-pill-nav__item').text()).toBe('Records')
198
198
  })
199
+ const SIX = ['One', 'Two', 'Three', 'Four', 'Five', { id: 'grp', label: 'Group', children: ['Six'] }]
200
+
201
+ it('should fold the items after the fourth into a More dropdown', async () => {
202
+ const wrapper = mount(AppTopBarPillNavInternal, { props: { currentItemId: 'One', items: SIX } })
203
+
204
+ expect(wrapper.findAll('.mint-pill-nav__item').map(item => item.text())).toEqual([
205
+ 'One', 'Two', 'Three', 'Four', 'More',
206
+ ])
207
+ await wrapper.findAll('.mint-pill-nav__item')[4].trigger('click')
208
+ expect(wrapper.findAll('.mint-pill-nav__dropdown-label').map(option => option.text())).toEqual(['Five', 'Six'])
209
+ })
210
+
211
+ it('should emit the original item or group when a folded entry is chosen', async () => {
212
+ const wrapper = mount(AppTopBarPillNavInternal, { props: { currentItemId: 'One', items: SIX } })
213
+
214
+ await wrapper.findAll('.mint-pill-nav__item')[4].trigger('click')
215
+ await wrapper.findAll('.mint-pill-nav__dropdown-item')[0].trigger('click')
216
+ await wrapper.findAll('.mint-pill-nav__item')[4].trigger('click')
217
+ await wrapper.findAll('.mint-pill-nav__dropdown-item')[1].trigger('click')
218
+
219
+ expect(wrapper.emitted('select')).toEqual([[{ id: 'Five', label: 'Five' }]])
220
+ expect(wrapper.emitted('option-select')?.[0]).toEqual([
221
+ { id: 'Six', label: 'Six' },
222
+ { id: 'grp', label: 'Group', children: [{ id: 'Six', label: 'Six' }] },
223
+ ])
224
+ })
225
+
226
+ it('should label the More pill with a folded item when that item is current', () => {
227
+ const wrapper = mount(AppTopBarPillNavInternal, { props: { currentItemId: 'Five', items: SIX } })
228
+
229
+ const more = wrapper.findAll('.mint-pill-nav__item')[4]
230
+ expect(more.text()).toBe('Five')
231
+ expect(more.find('.mint-pill-nav__chevron').exists()).toBe(true)
232
+ expect(more.classes()).toContain('mint-pill-nav__item--active')
233
+ })
199
234
  })
@@ -565,4 +565,82 @@ describe('ArtifactSaveDialog', () => {
565
565
  expect(wrapper.find('.mint-artifact-save-dialog__error').exists()).toBe(true)
566
566
  })
567
567
  })
568
+
569
+ // -------------------------------------------------------------------------
570
+ // Save mode (replace vs new) and experiment strip
571
+ // -------------------------------------------------------------------------
572
+
573
+ describe('save mode', () => {
574
+ it.each([
575
+ [null, false],
576
+ [makeDetail(), true],
577
+ ])('should offer the replace/new cards only when an artifact can be replaced (%#)', (existingArtifact, shown) => {
578
+ const { wrapper } = mountDialog({ result: { x: 1 }, existingArtifact })
579
+
580
+ expect(wrapper.find('.mint-artifact-save-dialog__modes').exists()).toBe(shown)
581
+ })
582
+
583
+ it('should send a create payload when save as new is picked over an existing artifact', async () => {
584
+ const { wrapper, saveHandler } = mountDialog({ mode: 'json', result: { x: 1 }, existingArtifact: makeDetail() })
585
+ expect(wrapper.get('.mint-artifact-save-dialog__submit').text()).toBe('Replace result')
586
+
587
+ await wrapper.findAll('.mint-artifact-save-dialog__mode')[1].trigger('click')
588
+ expect(wrapper.find('.mint-artifact-save-dialog__name').exists()).toBe(true)
589
+ expect(wrapper.get('.mint-artifact-save-dialog__submit').text()).toBe('Save result')
590
+ await submit(wrapper)
591
+
592
+ expect(saveHandler.mock.calls[0][0]).toMatchObject({ kind: 'json', mode: 'create', artifactKey: 'default' })
593
+ })
594
+
595
+ // An explicit "new result" must never upsert over an existing JSON artifact.
596
+ it.each(['fit', 'other'])('should block a json save as new when the key %s exists', async (key) => {
597
+ const { wrapper, saveHandler } = mountDialog({
598
+ mode: 'json',
599
+ result: { x: 1 },
600
+ existingArtifact: makeDetail(),
601
+ existingArtifactKeys: ['other'],
602
+ })
603
+
604
+ await wrapper.findAll('.mint-artifact-save-dialog__mode')[1].trigger('click')
605
+ await wrapper.get('.mint-artifact-save-dialog__key input').setValue(key)
606
+ await submit(wrapper)
607
+
608
+ expect(saveHandler).not.toHaveBeenCalled()
609
+ expect(wrapper.get('.mint-artifact-save-dialog__submit').attributes('disabled')).toBeDefined()
610
+ expect(wrapper.get('.mint-artifact-save-dialog__key').text()).toContain('already used')
611
+ })
612
+
613
+ it('should show the experiment strip with code and name when the experiment is known', () => {
614
+ const { wrapper } = mountDialog({
615
+ experiment: { id: 42, experiment_code: 'EXP-042', name: 'Dose response' },
616
+ })
617
+
618
+ expect(wrapper.get('.mint-artifact-save-dialog__experiment').text()).toContain('EXP-042')
619
+ expect(wrapper.get('.mint-artifact-save-dialog__experiment-name').text()).toBe('Dose response')
620
+ })
621
+
622
+ it.each([
623
+ [false, false],
624
+ [true, true],
625
+ ])('should show the Change link only when a change-experiment listener is bound (%s)', (bound, shown) => {
626
+ const listener = vi.fn()
627
+ const { wrapper } = mountDialog({
628
+ experiment: { id: 42, name: 'Dose response' },
629
+ ...(bound ? { onChangeExperiment: listener } : {}),
630
+ })
631
+
632
+ expect(wrapper.find('.mint-artifact-save-dialog__change').exists()).toBe(shown)
633
+ })
634
+
635
+ it('should emit change-experiment when Change is clicked', async () => {
636
+ const { wrapper } = mountDialog({
637
+ experiment: { id: 42, name: 'Dose response' },
638
+ onChangeExperiment: vi.fn(),
639
+ })
640
+
641
+ await wrapper.get('.mint-artifact-save-dialog__change').trigger('click')
642
+
643
+ expect(wrapper.emitted('change-experiment')).toHaveLength(1)
644
+ })
645
+ })
568
646
  })
@@ -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).
@@ -8,56 +8,111 @@ const options = [
8
8
  { value: 'c', label: 'C' },
9
9
  ]
10
10
 
11
+ function mountMultiple(props: Record<string, unknown> = {}) {
12
+ return mount(BaseSelect, { props: { multiple: true, modelValue: ['a'], options, ...props } })
13
+ }
14
+
15
+ async function openPanel(wrapper: ReturnType<typeof mountMultiple>) {
16
+ await wrapper.find('.mint-multi-select__field').trigger('click')
17
+ return wrapper
18
+ }
19
+
11
20
  describe('BaseSelect multiple', () => {
12
- it('should render the chip list when multiple is a bare template attribute', () => {
21
+ it('should render the chip field when multiple is a bare template attribute', () => {
13
22
  // A compiled `<BaseSelect multiple>` passes '' because `multiple` has no runtime Boolean type.
14
23
  const wrapper = mount(BaseSelect, { props: { multiple: '' as unknown as true, modelValue: [], options } })
15
24
 
16
- expect(wrapper.find('.mint-multi-select').exists()).toBe(true)
25
+ expect(wrapper.find('.mint-multi-select__field').exists()).toBe(true)
17
26
  })
18
27
 
19
28
  it('accepts string and number shorthand options', async () => {
20
- const wrapper = mount(BaseSelect, {
21
- props: {
22
- multiple: true,
23
- modelValue: ['glucose', 2],
24
- options: ['glucose', 2, { value: 'dox', label: 'Doxorubicin' }],
25
- },
26
- })
29
+ const wrapper = await openPanel(mountMultiple({
30
+ modelValue: ['glucose', 2],
31
+ options: ['glucose', 2, { value: 'dox', label: 'Doxorubicin' }],
32
+ }))
27
33
 
28
- expect(wrapper.findAll('button').map(button => button.text())).toEqual([
29
- 'glucose',
30
- '2',
31
- 'Doxorubicin',
32
- ])
33
- expect(wrapper.find('.mint-multi-select__summary').text()).toBe('glucose, 2')
34
+ expect(wrapper.findAll('.mint-multi-select__chip').map(chip => chip.text())).toEqual(['glucose', '2'])
35
+ expect(wrapper.findAll('[role="option"]').map(row => row.text())).toEqual(['glucose', '2', 'Doxorubicin'])
34
36
 
35
- await wrapper.findAll('button')[2].trigger('click')
37
+ await wrapper.findAll('[role="option"]')[2].trigger('click')
36
38
 
37
- expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([
38
- ['glucose', 2, 'dox'],
39
- ])
39
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['glucose', 2, 'dox']])
40
40
  })
41
41
 
42
42
  it('uses the shared selection limit for max', async () => {
43
- const wrapper = mount(BaseSelect, {
44
- props: {
45
- multiple: true,
46
- modelValue: ['a'],
47
- options,
48
- max: 1,
49
- },
50
- })
43
+ const wrapper = await openPanel(mountMultiple({ max: 1 }))
51
44
 
52
- await wrapper.findAll('button')[1].trigger('click')
45
+ await wrapper.findAll('[role="option"]')[1].trigger('click')
53
46
 
54
47
  expect(wrapper.emitted('update:modelValue')).toBeUndefined()
55
- expect(wrapper.findAll('button')[1].attributes('disabled')).toBeDefined()
48
+ expect(wrapper.findAll('[role="option"]')[1].attributes('aria-disabled')).toBe('true')
49
+ })
50
+
51
+ it('should reserve the check box on every row when only one is selected', async () => {
52
+ const wrapper = await openPanel(mountMultiple())
53
+
54
+ expect(wrapper.findAll('.mint-multi-select__check')).toHaveLength(3)
55
+ expect(wrapper.findAll('.mint-multi-select__check--on')).toHaveLength(1)
56
+ })
57
+
58
+ it.each(['Enter', ' ', 'ArrowDown'])('should open the listbox when %j is pressed on the field', async (key) => {
59
+ const wrapper = mountMultiple()
60
+
61
+ await wrapper.find('.mint-multi-select__field').trigger('keydown', { key })
62
+
63
+ expect(wrapper.find('[role="listbox"]').exists()).toBe(true)
64
+ expect(wrapper.find('.mint-multi-select__field').attributes('aria-expanded')).toBe('true')
65
+ })
66
+
67
+ it('should close the listbox when the field is clicked while it is open', async () => {
68
+ const wrapper = await openPanel(mountMultiple())
69
+
70
+ await wrapper.find('.mint-multi-select__field').trigger('click')
71
+
72
+ expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
73
+ expect(wrapper.find('.mint-multi-select__field').attributes('aria-expanded')).toBe('false')
74
+ })
75
+
76
+ it('should close the listbox when Escape is pressed in the filter', async () => {
77
+ const wrapper = await openPanel(mountMultiple())
78
+
79
+ await wrapper.find('input[type="search"]').trigger('keydown', { key: 'Escape' })
80
+
81
+ expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
82
+ })
83
+
84
+ it('should remove the last chip when Backspace is pressed in an empty filter', async () => {
85
+ const wrapper = await openPanel(mountMultiple({ modelValue: ['a', 'b'] }))
86
+
87
+ await wrapper.find('input[type="search"]').trigger('keydown', { key: 'Backspace' })
88
+
89
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['a']])
90
+ })
91
+
92
+ it('should remove a chip without opening the listbox when its remove button is clicked', async () => {
93
+ const wrapper = mountMultiple({ modelValue: ['a', 'b'] })
94
+
95
+ await wrapper.find('button[aria-label="Remove A"]').trigger('click')
96
+
97
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['b']])
98
+ expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
99
+ })
100
+
101
+ it('should narrow the rows and toggle the active row with Enter when the filter has text', async () => {
102
+ const wrapper = await openPanel(mountMultiple())
103
+ const filter = wrapper.find('input[type="search"]')
104
+
105
+ await filter.setValue('c')
106
+ await filter.trigger('keydown', { key: 'Enter' })
107
+
108
+ expect(wrapper.findAll('[role="option"]').map(row => row.text())).toEqual(['C'])
109
+ expect(wrapper.emitted('update:modelValue')?.[0]).toEqual([['a', 'c']])
56
110
  })
57
111
 
58
- it('should reserve the check slot on every chip when only one is selected', () => {
59
- const wrapper = mount(BaseSelect, { props: { multiple: true, modelValue: ['a'], options } })
112
+ it('should neither open nor offer chip removal when disabled', async () => {
113
+ const wrapper = await openPanel(mountMultiple({ disabled: true }))
60
114
 
61
- expect(wrapper.findAll('.mint-multi-select__chip-check')).toHaveLength(3)
115
+ expect(wrapper.find('[role="listbox"]').exists()).toBe(false)
116
+ expect(wrapper.find('.mint-multi-select__chip-remove').exists()).toBe(false)
62
117
  })
63
118
  })
@@ -58,7 +58,8 @@ describe('BaseSelect', () => {
58
58
  it.each([
59
59
  ['aria-labelledby', 'native', {}, 'select'],
60
60
  ['aria-describedby', 'searchable', { searchable: true }, '.mint-searchable-select__trigger'],
61
- ['aria-describedby', 'multiple', { multiple: true, modelValue: [] as string[] }, null],
61
+ ['aria-describedby', 'multiple', { multiple: true, modelValue: [] as string[] }, '.mint-multi-select__field'],
62
+ ['aria-labelledby', 'multiple', { multiple: true, modelValue: [] as string[] }, '.mint-multi-select__field'],
62
63
  ] as const)('should forward %s when the select is %s', (attr, _, extra, selector) => {
63
64
  const wrapper = mount(BaseSelect, { props: { options: ['linear'], ...extra }, attrs: { [attr]: 'hint' } })
64
65
  const target = selector ? wrapper.get(selector) : wrapper
@@ -57,4 +57,20 @@ describe('BatchProgressList', () => {
57
57
  await wrapper.get('.mint-batch-progress__completed-toggle').trigger('click')
58
58
  expect(wrapper.findAll('.mint-batch-progress__item')).toHaveLength(3)
59
59
  })
60
+
61
+ it.each([
62
+ ['completed', undefined, '100%', '100%'],
63
+ ['processing', 40, '40%', '40%'],
64
+ ['pending', undefined, 'Queued', '0%'],
65
+ ['error', undefined, 'Failed', '0%'],
66
+ ['skipped', undefined, 'Skipped', '0%'],
67
+ ] as const)('should show %s rows with their status text and bar width', async (status, progress, text, width) => {
68
+ const wrapper = mount(BatchProgressList, {
69
+ props: { items: [{ id: 'x', label: 'X', status, progress }], autoScroll: false },
70
+ })
71
+ if (status === 'completed') await wrapper.get('.mint-batch-progress__completed-toggle').trigger('click')
72
+
73
+ expect(wrapper.get('.mint-batch-progress__item-status').text()).toBe(text)
74
+ expect(wrapper.get('.mint-batch-progress__item-progress-bar').attributes('style')).toContain(`width: ${width}`)
75
+ })
60
76
  })