@morscherlab/mint-sdk 1.0.0-rc.4 → 1.0.0-rc.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 (304) hide show
  1. package/dist/__tests__/components/AppTopBar.navigation.test.d.ts +1 -0
  2. package/dist/__tests__/components/DoseCalculatorVolumeField.test.d.ts +1 -0
  3. package/dist/__tests__/components/PlateMapEditorToolbarInternal.test.d.ts +1 -0
  4. package/dist/__tests__/components/PluginWorkspaceView.controls.test.d.ts +1 -0
  5. package/dist/__tests__/components/PluginWorkspaceView.navigation.test.d.ts +1 -0
  6. package/dist/__tests__/components/PluginWorkspaceView.shell.test.d.ts +1 -0
  7. package/dist/__tests__/components/ProtocolStep.presentation.test.d.ts +1 -0
  8. package/dist/__tests__/components/ProtocolStepEditor.state.test.d.ts +1 -0
  9. package/dist/__tests__/components/ProtocolStepParameterField.test.d.ts +1 -0
  10. package/dist/__tests__/components/ReagentList.presentation.test.d.ts +1 -0
  11. package/dist/__tests__/components/SampleSelector.colors.test.d.ts +1 -0
  12. package/dist/__tests__/components/SampleSelector.drag.test.d.ts +1 -0
  13. package/dist/__tests__/components/SampleSelector.groups.test.d.ts +1 -0
  14. package/dist/__tests__/components/SampleSelector.selection.test.d.ts +1 -0
  15. package/dist/__tests__/components/SampleSelectorSampleRow.test.d.ts +1 -0
  16. package/dist/__tests__/components/ScheduleCalendar.test.d.ts +1 -0
  17. package/dist/__tests__/components/SettingsModal.schema.test.d.ts +1 -0
  18. package/dist/__tests__/components/WellPlate.colors.test.d.ts +1 -0
  19. package/dist/__tests__/components/WellPlate.conditions.test.d.ts +1 -0
  20. package/dist/__tests__/components/WellPlate.geometry.test.d.ts +1 -0
  21. package/dist/__tests__/components/WellPlate.interaction.test.d.ts +1 -0
  22. package/dist/__tests__/components/WellPlate.legend.test.d.ts +1 -0
  23. package/dist/__tests__/components/WellPlate.rendering.test.d.ts +1 -0
  24. package/dist/__tests__/components/WellPlate.sampleDrop.test.d.ts +1 -0
  25. package/dist/__tests__/composables/autoGroup/classify.test.d.ts +1 -0
  26. package/dist/__tests__/composables/autoGroup/columns.test.d.ts +1 -0
  27. package/dist/__tests__/composables/autoGroup/compose.test.d.ts +1 -0
  28. package/dist/__tests__/composables/autoGroup/cooccurrence.test.d.ts +1 -0
  29. package/dist/__tests__/composables/autoGroup/fingerprint.test.d.ts +1 -0
  30. package/dist/__tests__/composables/autoGroup/integration.test.d.ts +1 -0
  31. package/dist/__tests__/composables/autoGroup/template.test.d.ts +1 -0
  32. package/dist/__tests__/composables/autoGroup/tokenize.test.d.ts +1 -0
  33. package/dist/__tests__/composables/useAutoGroupInputSources.test.d.ts +1 -0
  34. package/dist/__tests__/composables/useScheduleCalendarLayout.test.d.ts +1 -0
  35. package/dist/__tests__/docs/extractDocsComponents.test.d.ts +1 -0
  36. package/dist/__tests__/docs/extractDocsExports.test.d.ts +1 -0
  37. package/dist/__tests__/docs/extractDocsParsing.test.d.ts +1 -0
  38. package/dist/__tests__/docs/extractDocsTemplates.test.d.ts +1 -0
  39. package/dist/__tests__/docs/extractDocsTheme.test.d.ts +1 -0
  40. package/dist/components/AppSidebar.vue.d.ts +3 -3
  41. package/dist/components/AppTopBar.navigation.d.ts +11 -0
  42. package/dist/components/BaseButton.vue.d.ts +1 -1
  43. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  44. package/dist/components/BaseInput.vue.d.ts +2 -2
  45. package/dist/components/BasePill.vue.d.ts +1 -1
  46. package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
  47. package/dist/components/BaseSelect.vue.d.ts +1 -1
  48. package/dist/components/BaseSlider.vue.d.ts +2 -2
  49. package/dist/components/BaseTextarea.vue.d.ts +2 -2
  50. package/dist/components/BaseToggle.vue.d.ts +1 -1
  51. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  52. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
  53. package/dist/components/ColorSlider.vue.d.ts +2 -2
  54. package/dist/components/ConcentrationInput.vue.d.ts +2 -2
  55. package/dist/components/ControlWorkspaceView.vue.d.ts +3 -3
  56. package/dist/components/DatePicker.vue.d.ts +1 -1
  57. package/dist/components/DateTimePicker.vue.d.ts +2 -2
  58. package/dist/components/DoseCalculatorVolumeField.vue.d.ts +15 -0
  59. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
  60. package/dist/components/DropdownButton.vue.d.ts +1 -1
  61. package/dist/components/FileUploader.vue.d.ts +2 -2
  62. package/dist/components/FormulaInput.vue.d.ts +2 -2
  63. package/dist/components/IconButton.vue.d.ts +1 -1
  64. package/dist/components/LoadingSpinner.vue.d.ts +1 -1
  65. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  66. package/dist/components/MultiSelect.vue.d.ts +1 -1
  67. package/dist/components/NumberInput.vue.d.ts +1 -1
  68. package/dist/components/PlateMapEditor.vue.d.ts +6 -6
  69. package/dist/components/PluginWorkspaceView.controls.d.ts +28 -0
  70. package/dist/components/PluginWorkspaceView.navigation.d.ts +29 -0
  71. package/dist/components/PluginWorkspaceView.props.d.ts +151 -0
  72. package/dist/components/PluginWorkspaceView.shell.d.ts +19 -0
  73. package/dist/components/PluginWorkspaceView.vue.d.ts +46 -195
  74. package/dist/components/ProgressBar.vue.d.ts +1 -1
  75. package/dist/components/ProtocolStep.presentation.d.ts +4 -0
  76. package/dist/components/ProtocolStepEditor.state.d.ts +18 -0
  77. package/dist/components/ProtocolStepParameterField.vue.d.ts +12 -0
  78. package/dist/components/ReagentList.presentation.d.ts +16 -0
  79. package/dist/components/ResourceCard.vue.d.ts +1 -1
  80. package/dist/components/SampleSelector.colors.d.ts +13 -0
  81. package/dist/components/SampleSelector.drag.d.ts +24 -0
  82. package/dist/components/SampleSelector.groups.d.ts +15 -0
  83. package/dist/components/SampleSelector.selection.d.ts +26 -0
  84. package/dist/components/SampleSelector.vue.d.ts +4 -1
  85. package/dist/components/SampleSelectorSampleRow.vue.d.ts +21 -0
  86. package/dist/components/SegmentedControl.vue.d.ts +1 -1
  87. package/dist/components/SequenceInput.vue.d.ts +2 -2
  88. package/dist/components/SequenceProgressBar.vue.d.ts +1 -1
  89. package/dist/components/SettingsModal.schema.d.ts +9 -0
  90. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  91. package/dist/components/TagsInput.vue.d.ts +2 -2
  92. package/dist/components/TimePicker.vue.d.ts +2 -2
  93. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  94. package/dist/components/UnitInput.vue.d.ts +2 -2
  95. package/dist/components/WellPlate.colors.d.ts +9 -0
  96. package/dist/components/WellPlate.conditions.d.ts +26 -0
  97. package/dist/components/WellPlate.geometry.d.ts +23 -0
  98. package/dist/components/WellPlate.interaction.d.ts +71 -0
  99. package/dist/components/WellPlate.legend.d.ts +2 -0
  100. package/dist/components/WellPlate.rendering.d.ts +24 -0
  101. package/dist/components/WellPlate.sampleDrop.d.ts +8 -0
  102. package/dist/components/WellPlate.vue.d.ts +1 -1
  103. package/dist/components/index.js +2 -2
  104. package/dist/components/internal/ActionItemInternal.vue.d.ts +1 -1
  105. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +28 -0
  106. package/dist/{components-DafPc4rM.js → components-DtHA2bgp.js} +3754 -2991
  107. package/dist/components-DtHA2bgp.js.map +1 -0
  108. package/dist/composables/autoGroup/classKey.d.ts +4 -0
  109. package/dist/composables/autoGroup/classify.d.ts +28 -0
  110. package/dist/composables/autoGroup/colors.d.ts +2 -0
  111. package/dist/composables/autoGroup/columns.d.ts +10 -0
  112. package/dist/composables/autoGroup/compose.d.ts +8 -0
  113. package/dist/composables/autoGroup/cooccurrence.d.ts +2 -0
  114. package/dist/composables/autoGroup/csv-shim.d.ts +2 -0
  115. package/dist/composables/autoGroup/fingerprint.d.ts +3 -0
  116. package/dist/composables/autoGroup/index.d.ts +16 -0
  117. package/dist/composables/autoGroup/replicatePreGroup.d.ts +38 -0
  118. package/dist/composables/autoGroup/template.d.ts +15 -0
  119. package/dist/composables/autoGroup/tokenize.d.ts +8 -0
  120. package/dist/composables/autoGroupConstants.d.ts +1 -0
  121. package/dist/composables/autoGroupGrouping.d.ts +3 -0
  122. package/dist/composables/controlComponentBindings.d.ts +1 -1
  123. package/dist/composables/controlSchemaAdapters.d.ts +1 -1
  124. package/dist/composables/controlSchemaDoseDesign.d.ts +11 -0
  125. package/dist/composables/controlSchemaLayout.d.ts +1 -1
  126. package/dist/composables/controlSchemaModel.d.ts +5 -0
  127. package/dist/composables/controlSchemaNormalize.d.ts +1 -1
  128. package/dist/composables/controlSchemaTypes.d.ts +305 -0
  129. package/dist/composables/controlWorkspaceOptions.d.ts +1 -1
  130. package/dist/composables/formBuilderSchema.d.ts +18 -0
  131. package/dist/composables/index.js +3 -3
  132. package/dist/composables/pluginEndpointBuilder.d.ts +13 -0
  133. package/dist/composables/protocolTemplateCatalog.d.ts +26 -0
  134. package/dist/composables/useAutoGroup.d.ts +61 -74
  135. package/dist/composables/useAutoGroupInputSources.d.ts +32 -0
  136. package/dist/composables/useBioTemplateControls.d.ts +1 -1
  137. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +1 -1
  138. package/dist/composables/useBioTemplateWorkspace.d.ts +1 -1
  139. package/dist/composables/useControlSchema.d.ts +4 -316
  140. package/dist/composables/useControlWorkspace.d.ts +1 -1
  141. package/dist/composables/useForm.d.ts +2 -33
  142. package/dist/composables/useFormBuilder.d.ts +2 -9
  143. package/dist/composables/useFormValidation.d.ts +34 -0
  144. package/dist/composables/usePluginClient.d.ts +1 -4
  145. package/dist/composables/useProtocolTemplates.d.ts +2 -24
  146. package/dist/composables/useScheduleCalendarLayout.d.ts +49 -0
  147. package/dist/{composables-BMkPQhVK.js → composables-Dlg8jenH.js} +33 -31
  148. package/dist/composables-Dlg8jenH.js.map +1 -0
  149. package/dist/index.js +4 -4
  150. package/dist/install.js +2 -2
  151. package/dist/styles.css +547 -516
  152. package/dist/templates/controlSchemaTypes.d.ts +1 -1
  153. package/dist/templates/index.js +1 -1
  154. package/dist/templates/templateAdapterTypes.d.ts +48 -0
  155. package/dist/templates/templateCreateOptions.d.ts +165 -0
  156. package/dist/templates/templateQpcrTypes.d.ts +42 -0
  157. package/dist/templates/types.d.ts +5 -250
  158. package/dist/{templates-bUAWMn5L.js → templates-DtdUvJ4c.js} +144 -136
  159. package/dist/templates-DtdUvJ4c.js.map +1 -0
  160. package/dist/types/auto-group.d.ts +79 -9
  161. package/dist/types/componentLabTypes.d.ts +161 -0
  162. package/dist/types/componentWorkflowTypes.d.ts +150 -0
  163. package/dist/types/components.d.ts +2 -311
  164. package/dist/{useProtocolTemplates-QZtHFFH2.js → useProtocolTemplates-Bm5vyH4_.js} +1220 -454
  165. package/dist/useProtocolTemplates-Bm5vyH4_.js.map +1 -0
  166. package/package.json +1 -1
  167. package/src/__tests__/components/AppTopBar.navigation.test.ts +70 -0
  168. package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +53 -0
  169. package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +54 -0
  170. package/src/__tests__/components/PluginWorkspaceView.controls.test.ts +156 -0
  171. package/src/__tests__/components/PluginWorkspaceView.navigation.test.ts +102 -0
  172. package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +41 -0
  173. package/src/__tests__/components/ProtocolStep.presentation.test.ts +31 -0
  174. package/src/__tests__/components/ProtocolStepEditor.state.test.ts +165 -0
  175. package/src/__tests__/components/ProtocolStepParameterField.test.ts +44 -0
  176. package/src/__tests__/components/ReagentList.presentation.test.ts +68 -0
  177. package/src/__tests__/components/SampleSelector.colors.test.ts +49 -0
  178. package/src/__tests__/components/SampleSelector.drag.test.ts +100 -0
  179. package/src/__tests__/components/SampleSelector.groups.test.ts +81 -0
  180. package/src/__tests__/components/SampleSelector.selection.test.ts +70 -0
  181. package/src/__tests__/components/SampleSelector.test.ts +32 -0
  182. package/src/__tests__/components/SampleSelectorSampleRow.test.ts +37 -0
  183. package/src/__tests__/components/ScheduleCalendar.test.ts +44 -0
  184. package/src/__tests__/components/SettingsModal.schema.test.ts +97 -0
  185. package/src/__tests__/components/WellPlate.colors.test.ts +28 -0
  186. package/src/__tests__/components/WellPlate.conditions.test.ts +68 -0
  187. package/src/__tests__/components/WellPlate.geometry.test.ts +54 -0
  188. package/src/__tests__/components/WellPlate.interaction.test.ts +171 -0
  189. package/src/__tests__/components/WellPlate.legend.test.ts +13 -0
  190. package/src/__tests__/components/WellPlate.rendering.test.ts +122 -0
  191. package/src/__tests__/components/WellPlate.sampleDrop.test.ts +70 -0
  192. package/src/__tests__/composables/autoGroup/classify.test.ts +107 -0
  193. package/src/__tests__/composables/autoGroup/columns.test.ts +135 -0
  194. package/src/__tests__/composables/autoGroup/compose.test.ts +227 -0
  195. package/src/__tests__/composables/autoGroup/cooccurrence.test.ts +91 -0
  196. package/src/__tests__/composables/autoGroup/fingerprint.test.ts +50 -0
  197. package/src/__tests__/composables/autoGroup/integration.test.ts +79 -0
  198. package/src/__tests__/composables/autoGroup/template.test.ts +70 -0
  199. package/src/__tests__/composables/autoGroup/tokenize.test.ts +33 -0
  200. package/src/__tests__/composables/useAutoGroup.test.ts +129 -625
  201. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +107 -0
  202. package/src/__tests__/composables/useControlSchema.test.ts +23 -0
  203. package/src/__tests__/composables/useScheduleCalendarLayout.test.ts +89 -0
  204. package/src/__tests__/docs/extractDocsComponents.test.ts +142 -0
  205. package/src/__tests__/docs/extractDocsExports.test.ts +77 -0
  206. package/src/__tests__/docs/extractDocsParsing.test.ts +69 -0
  207. package/src/__tests__/docs/extractDocsTemplates.test.ts +54 -0
  208. package/src/__tests__/docs/extractDocsTheme.test.ts +89 -0
  209. package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -1
  210. package/src/__tests__/fixtures/auto-group/mixed-lc-ms-batch.txt +187 -0
  211. package/src/components/AppSidebar.vue +2 -6
  212. package/src/components/AppTopBar.navigation.ts +62 -0
  213. package/src/components/AppTopBar.vue +17 -44
  214. package/src/components/AutoGroupModal.story.vue +50 -0
  215. package/src/components/AutoGroupModal.vue +441 -158
  216. package/src/components/ControlWorkspaceView.vue +2 -6
  217. package/src/components/DoseCalculator.vue +13 -73
  218. package/src/components/DoseCalculatorVolumeField.vue +61 -0
  219. package/src/components/ExperimentTimeline.vue +6 -31
  220. package/src/components/FormBuilder.vue +2 -7
  221. package/src/components/PlateMapEditor.vue +32 -106
  222. package/src/components/PluginWorkspaceView.controls.ts +182 -0
  223. package/src/components/PluginWorkspaceView.navigation.ts +106 -0
  224. package/src/components/PluginWorkspaceView.props.ts +174 -0
  225. package/src/components/PluginWorkspaceView.shell.ts +66 -0
  226. package/src/components/PluginWorkspaceView.vue +85 -404
  227. package/src/components/ProtocolStep.presentation.ts +31 -0
  228. package/src/components/ProtocolStepEditor.state.ts +104 -0
  229. package/src/components/ProtocolStepEditor.vue +48 -179
  230. package/src/components/ProtocolStepParameterField.vue +134 -0
  231. package/src/components/ReagentList.presentation.ts +105 -0
  232. package/src/components/ReagentList.vue +16 -79
  233. package/src/components/SampleSelector.colors.ts +43 -0
  234. package/src/components/SampleSelector.drag.ts +164 -0
  235. package/src/components/SampleSelector.groups.ts +109 -0
  236. package/src/components/SampleSelector.selection.ts +103 -0
  237. package/src/components/SampleSelector.vue +82 -349
  238. package/src/components/SampleSelectorSampleRow.vue +64 -0
  239. package/src/components/ScheduleCalendar.vue +44 -199
  240. package/src/components/SettingsModal.schema.ts +71 -0
  241. package/src/components/SettingsModal.vue +16 -46
  242. package/src/components/WellPlate.colors.ts +56 -0
  243. package/src/components/WellPlate.conditions.ts +100 -0
  244. package/src/components/WellPlate.geometry.ts +91 -0
  245. package/src/components/WellPlate.interaction.ts +272 -0
  246. package/src/components/WellPlate.legend.ts +8 -0
  247. package/src/components/WellPlate.rendering.ts +105 -0
  248. package/src/components/WellPlate.sampleDrop.ts +73 -0
  249. package/src/components/WellPlate.vue +102 -550
  250. package/src/components/internal/PlateMapEditorToolbarInternal.vue +128 -0
  251. package/src/composables/autoGroup/classKey.ts +5 -0
  252. package/src/composables/autoGroup/classify.ts +205 -0
  253. package/src/composables/autoGroup/colors.ts +6 -0
  254. package/src/composables/autoGroup/columns.ts +226 -0
  255. package/src/composables/autoGroup/compose.ts +156 -0
  256. package/src/composables/autoGroup/cooccurrence.ts +46 -0
  257. package/src/composables/autoGroup/csv-shim.ts +44 -0
  258. package/src/composables/autoGroup/fingerprint.ts +49 -0
  259. package/src/composables/autoGroup/index.ts +20 -0
  260. package/src/composables/autoGroup/replicatePreGroup.ts +90 -0
  261. package/src/composables/autoGroup/template.ts +126 -0
  262. package/src/composables/autoGroup/tokenize.ts +41 -0
  263. package/src/composables/autoGroup/vocab.json +67 -0
  264. package/src/composables/autoGroupConstants.ts +4 -0
  265. package/src/composables/autoGroupGrouping.ts +148 -0
  266. package/src/composables/controlComponentBindings.ts +1 -1
  267. package/src/composables/controlSchemaAdapters.ts +1 -1
  268. package/src/composables/controlSchemaDoseDesign.ts +215 -0
  269. package/src/composables/controlSchemaFormFields.ts +1 -1
  270. package/src/composables/controlSchemaLayout.ts +1 -1
  271. package/src/composables/controlSchemaModel.ts +163 -0
  272. package/src/composables/controlSchemaNormalize.ts +1 -1
  273. package/src/composables/controlSchemaTypes.ts +364 -0
  274. package/src/composables/controlWorkspaceOptions.ts +1 -1
  275. package/src/composables/formBuilderSchema.ts +153 -0
  276. package/src/composables/pluginEndpointBuilder.ts +203 -0
  277. package/src/composables/protocolTemplateCatalog.ts +325 -0
  278. package/src/composables/useAutoGroup.ts +395 -549
  279. package/src/composables/useAutoGroupInputSources.ts +147 -0
  280. package/src/composables/useBioTemplateControls.ts +1 -1
  281. package/src/composables/useBioTemplatePresetWorkspace.ts +1 -1
  282. package/src/composables/useBioTemplateWorkspace.ts +1 -1
  283. package/src/composables/useControlSchema.ts +21 -692
  284. package/src/composables/useControlWorkspace.ts +7 -13
  285. package/src/composables/useForm.ts +5 -187
  286. package/src/composables/useFormBuilder.ts +11 -153
  287. package/src/composables/useFormValidation.ts +154 -0
  288. package/src/composables/usePluginClient.ts +10 -193
  289. package/src/composables/useProtocolTemplates.ts +10 -328
  290. package/src/composables/useScheduleCalendarLayout.ts +287 -0
  291. package/src/styles/components/auto-group-modal.css +248 -310
  292. package/src/templates/controlSchemaTypes.ts +1 -1
  293. package/src/templates/templateAdapterTypes.ts +58 -0
  294. package/src/templates/templateCreateOptions.ts +208 -0
  295. package/src/templates/templateQpcrTypes.ts +48 -0
  296. package/src/templates/types.ts +79 -275
  297. package/src/types/auto-group.ts +107 -9
  298. package/src/types/componentLabTypes.ts +235 -0
  299. package/src/types/componentWorkflowTypes.ts +190 -0
  300. package/src/types/components.ts +74 -424
  301. package/dist/components-DafPc4rM.js.map +0 -1
  302. package/dist/composables-BMkPQhVK.js.map +0 -1
  303. package/dist/templates-bUAWMn5L.js.map +0 -1
  304. package/dist/useProtocolTemplates-QZtHFFH2.js.map +0 -1
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { inferRole, parseNumeric, buildClassSchema } from '../../../composables/autoGroup/columns'
3
+
4
+ describe('parseNumeric', () => {
5
+ it('parses value+unit tokens', () => {
6
+ expect(parseNumeric('5nM')).toEqual({ value: 5, unit: 'nM', raw: '5nM' })
7
+ expect(parseNumeric('400ulES')).toEqual({ value: 400, unit: 'ulES', raw: '400ulES' })
8
+ expect(parseNumeric('1ul')).toEqual({ value: 1, unit: 'ul', raw: '1ul' })
9
+ })
10
+
11
+ it('decodes the dot-encoded decimal point', () => {
12
+ expect(parseNumeric('0dot5ul')).toEqual({ value: 0.5, unit: 'ul', raw: '0dot5ul' })
13
+ })
14
+
15
+ it('parses ratios via the dedicated branch', () => {
16
+ expect(parseNumeric('1to100')).toEqual({ value: 0.01, unit: 'ratio', raw: '1to100' })
17
+ expect(parseNumeric('1to10')).toEqual({ value: 0.1, unit: 'ratio', raw: '1to10' })
18
+ })
19
+
20
+ it('rejects bare IDs, dates, and pure numbers', () => {
21
+ expect(parseNumeric('M10')).toBeNull()
22
+ expect(parseNumeric('exp281')).toBeNull()
23
+ expect(parseNumeric('260415')).toBeNull()
24
+ expect(parseNumeric('123')).toBeNull()
25
+ })
26
+ })
27
+
28
+ describe('inferRole', () => {
29
+ const make = (values: string[]) => ({
30
+ values,
31
+ position: 0,
32
+ totalPositions: 1,
33
+ classSize: values.length,
34
+ })
35
+
36
+ it('returns constant when every value is the same', () => {
37
+ expect(inferRole(make(['exp281', 'exp281', 'exp281']))).toBe('constant')
38
+ })
39
+
40
+ it('returns run-order when trailing position has high-card numeric values', () => {
41
+ expect(inferRole({
42
+ values: ['001', '002', '003', '004', '005'],
43
+ position: 4,
44
+ totalPositions: 5,
45
+ classSize: 5,
46
+ })).toBe('run-order')
47
+ })
48
+
49
+ it('returns numeric when many distinct value+unit values are present', () => {
50
+ // 6+ distinct numeric levels read as numeric (binning candidates).
51
+ expect(inferRole(make(['1nM', '5nM', '10nM', '20nM', '50nM', '100nM']))).toBe('numeric')
52
+ })
53
+
54
+ it('returns factor for low-cardinality numerics (<=5 unique levels)', () => {
55
+ // 5min / 10min and similar small dose ladders read as factors by default;
56
+ // users opt into binning via the column popover for the cases that want it.
57
+ expect(inferRole(make(['5min', '10min']))).toBe('factor')
58
+ expect(inferRole(make(['5nM', '10nM', '20nM']))).toBe('factor')
59
+ })
60
+
61
+ it('returns replicate ONLY for explicit replicate-named tokens (rep1, Rep2, replicate3, R1)', () => {
62
+ // The replicate pre-grouping pass collapses real replicates by name
63
+ // before tokenisation, so the cardinality-based ID_PATTERN heuristic
64
+ // (M1, M2, …) was removed — those reach inferRole now only when they
65
+ // represent distinct conditions, and should be treated as factors.
66
+ expect(inferRole({
67
+ values: ['M1', 'M2', 'M3', 'M4', 'M5'],
68
+ position: 1,
69
+ totalPositions: 3,
70
+ classSize: 5,
71
+ })).toBe('factor')
72
+ expect(inferRole({
73
+ values: ['rep1', 'rep2', 'rep3'],
74
+ position: 1,
75
+ totalPositions: 3,
76
+ classSize: 3,
77
+ })).toBe('replicate')
78
+ expect(inferRole({
79
+ values: ['Replicate1', 'Replicate2'],
80
+ position: 1,
81
+ totalPositions: 3,
82
+ classSize: 2,
83
+ })).toBe('replicate')
84
+ })
85
+
86
+ it('returns factor for low-cardinality categoricals', () => {
87
+ expect(inferRole(make(['Kidney', 'Liver', 'Tumor', 'Kidney', 'Liver']))).toBe('factor')
88
+ })
89
+
90
+ it('returns ignore for very-high-cardinality opaque values', () => {
91
+ // Real-world LC-MS batches routinely have 30–50 distinct conditions, so the
92
+ // factor cap was raised to 50 (was 20). 80 unique values still reads as
93
+ // ignore — likely a sample-ID column rather than a meaningful factor.
94
+ const values = Array.from({ length: 80 }, (_, i) => `opaque-${i}`)
95
+ expect(inferRole({ values, position: 0, totalPositions: 1, classSize: 80 })).toBe('ignore')
96
+ })
97
+ })
98
+
99
+ describe('buildClassSchema', () => {
100
+ it('builds one ColumnInfo per position, constant class-tag positions get class-tag role', () => {
101
+ const schema = buildClassSchema(
102
+ [
103
+ ['exp281', 'SAM_SAH', 'Tissues', 'Kidney', 'M10', '085'],
104
+ ['exp281', 'SAM_SAH', 'Tissues', 'Liver', 'M10', '047'],
105
+ ],
106
+ 'biological',
107
+ 'Tissues',
108
+ [2, 3], // positions flagged as class-related tags
109
+ )
110
+ expect(schema.classKind).toBe('biological')
111
+ expect(schema.subKind).toBe('Tissues')
112
+ // Position 2 ('Tissues') is constant → class-tag.
113
+ // Position 3 ('Kidney'/'Liver') is multi-valued → falls through to factor inference.
114
+ const tagCols = schema.columns.filter(c => c.role === 'class-tag')
115
+ expect(tagCols.length).toBe(1)
116
+ expect(tagCols[0].index).toBe(2)
117
+ const factorCols = schema.columns.filter(c => c.role === 'factor')
118
+ expect(factorCols.some(c => c.index === 3)).toBe(true)
119
+ })
120
+
121
+ it('default groupBy is every Factor column', () => {
122
+ const schema = buildClassSchema(
123
+ [
124
+ ['exp', 'Kidney', '1'],
125
+ ['exp', 'Liver', '2'],
126
+ ['exp', 'Tumor', '3'],
127
+ ],
128
+ 'biological',
129
+ 'Tissues',
130
+ [],
131
+ )
132
+ const factorIndices = schema.columns.filter(c => c.role === 'factor').map(c => c.index)
133
+ expect(schema.groupBy).toEqual(factorIndices)
134
+ })
135
+ })
@@ -0,0 +1,227 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { composeGroups } from '../../../composables/autoGroup/compose'
3
+ import type { ClassSchema } from '../../../types/auto-group'
4
+
5
+ const tissueSchema: ClassSchema = {
6
+ classKind: 'biological',
7
+ subKind: 'Tissues',
8
+ columns: [
9
+ { index: 0, name: 'Tissue', sourceIndices: [0], uniqueValues: ['Kidney', 'Liver'], cardinality: 2, role: 'factor' },
10
+ { index: 1, name: 'Animal', sourceIndices: [1], uniqueValues: ['M1', 'M2', 'M3'], cardinality: 3, role: 'replicate' },
11
+ ],
12
+ groupBy: [0],
13
+ }
14
+
15
+ const iqcSchema: ClassSchema = {
16
+ classKind: 'iqc',
17
+ columns: [
18
+ { index: 0, name: 'IQC', sourceIndices: [0], uniqueValues: ['IQC'], cardinality: 1, role: 'class-tag' },
19
+ { index: 1, name: 'Run', sourceIndices: [1], uniqueValues: ['001', '002'], cardinality: 2, role: 'run-order' },
20
+ ],
21
+ groupBy: [],
22
+ }
23
+
24
+ describe('composeGroups', () => {
25
+ it('builds groups by enabled factor columns, ignoring replicate and run-order roles', () => {
26
+ const result = composeGroups({
27
+ tokenizedSamples: [
28
+ ['Kidney', 'M1'],
29
+ ['Kidney', 'M2'],
30
+ ['Liver', 'M1'],
31
+ ],
32
+ sampleNames: ['s1', 's2', 's3'],
33
+ schemas: { 'biological:Tissues': tissueSchema },
34
+ classes: [
35
+ {
36
+ kind: 'biological',
37
+ subKind: 'Tissues',
38
+ label: 'Biological / Tissues',
39
+ members: [0, 1, 2],
40
+ classTagPositions: [],
41
+ disposition: 'group',
42
+ },
43
+ ],
44
+ })
45
+ // Group names carry class context: "Biological / Tissues / <factor>".
46
+ // The class label is prepended so labels from different classes don't
47
+ // collide when they share a factor value.
48
+ expect(result.experimentalGroups!.map(g => g.name).sort()).toEqual([
49
+ 'Biological / Tissues / Kidney',
50
+ 'Biological / Tissues / Liver',
51
+ ])
52
+ expect(
53
+ result.experimentalGroups!.find(g => g.name === 'Biological / Tissues / Kidney')!.samples,
54
+ ).toEqual(['s1', 's2'])
55
+ })
56
+
57
+ it('routes overlay classes to qcGroups (separate from experimental)', () => {
58
+ const result = composeGroups({
59
+ tokenizedSamples: [
60
+ ['Kidney', 'M1'],
61
+ ['IQC', '001'],
62
+ ['IQC', '002'],
63
+ ],
64
+ sampleNames: ['bio1', 'iqc1', 'iqc2'],
65
+ schemas: { 'biological:Tissues': tissueSchema, iqc: iqcSchema },
66
+ classes: [
67
+ {
68
+ kind: 'biological',
69
+ subKind: 'Tissues',
70
+ label: 'Biological / Tissues',
71
+ members: [0],
72
+ classTagPositions: [],
73
+ disposition: 'group',
74
+ },
75
+ {
76
+ kind: 'iqc',
77
+ label: 'IQC',
78
+ members: [1, 2],
79
+ classTagPositions: [0],
80
+ disposition: 'overlay',
81
+ },
82
+ ],
83
+ })
84
+ expect(result.qcGroups!.length).toBe(1)
85
+ expect(result.qcGroups![0].name).toBe('IQC')
86
+ expect(result.qcGroups![0].samples).toEqual(['iqc1', 'iqc2'])
87
+ expect(result.experimentalGroups!.length).toBe(1)
88
+ })
89
+
90
+ it('routes excluded-disposition classes to excludedSamples', () => {
91
+ const result = composeGroups({
92
+ tokenizedSamples: [
93
+ ['Kidney', 'M1'],
94
+ ['IQC', '001'],
95
+ ],
96
+ sampleNames: ['bio1', 'iqc1'],
97
+ schemas: { 'biological:Tissues': tissueSchema, iqc: iqcSchema },
98
+ classes: [
99
+ {
100
+ kind: 'biological',
101
+ subKind: 'Tissues',
102
+ label: 'Biological / Tissues',
103
+ members: [0],
104
+ classTagPositions: [],
105
+ disposition: 'group',
106
+ },
107
+ {
108
+ kind: 'iqc',
109
+ label: 'IQC',
110
+ members: [1],
111
+ classTagPositions: [0],
112
+ disposition: 'exclude',
113
+ },
114
+ ],
115
+ })
116
+ expect(result.excludedSamples).toEqual(['iqc1'])
117
+ expect(result.qcGroups!.length).toBe(0)
118
+ })
119
+
120
+ it('legacy groups field equals experimentalGroups concat qcGroups', () => {
121
+ const result = composeGroups({
122
+ tokenizedSamples: [
123
+ ['Kidney', 'M1'],
124
+ ['IQC', '001'],
125
+ ],
126
+ sampleNames: ['bio1', 'iqc1'],
127
+ schemas: { 'biological:Tissues': tissueSchema, iqc: iqcSchema },
128
+ classes: [
129
+ {
130
+ kind: 'biological',
131
+ subKind: 'Tissues',
132
+ label: 'Biological / Tissues',
133
+ members: [0],
134
+ classTagPositions: [],
135
+ disposition: 'group',
136
+ },
137
+ {
138
+ kind: 'iqc',
139
+ label: 'IQC',
140
+ members: [1],
141
+ classTagPositions: [0],
142
+ disposition: 'overlay',
143
+ },
144
+ ],
145
+ })
146
+ const concatNames = [
147
+ ...result.experimentalGroups!.map(g => g.name),
148
+ ...result.qcGroups!.map(g => g.name),
149
+ ]
150
+ expect(result.groups.map(g => g.name)).toEqual(concatNames)
151
+ })
152
+
153
+ it('applies alias before exclude before bin in the operation pipeline', () => {
154
+ const schema: ClassSchema = {
155
+ classKind: 'biological',
156
+ columns: [
157
+ {
158
+ index: 0,
159
+ name: 'Treatment',
160
+ sourceIndices: [0],
161
+ uniqueValues: ['Drug-A', 'drug-A', 'CTL', 'CTL'],
162
+ cardinality: 3,
163
+ role: 'factor',
164
+ ops: {
165
+ alias: { 'drug-A': 'Drug-A' },
166
+ exclude: ['CTL'],
167
+ },
168
+ },
169
+ ],
170
+ groupBy: [0],
171
+ }
172
+ const result = composeGroups({
173
+ tokenizedSamples: [['Drug-A'], ['drug-A'], ['CTL']],
174
+ sampleNames: ['s1', 's2', 's3'],
175
+ schemas: { biological: schema },
176
+ classes: [
177
+ {
178
+ kind: 'biological',
179
+ label: 'Biological',
180
+ members: [0, 1, 2],
181
+ classTagPositions: [],
182
+ disposition: 'group',
183
+ },
184
+ ],
185
+ })
186
+ expect(result.experimentalGroups!.length).toBe(1)
187
+ // Class-prefixed group name (cls.label === 'Biological' → "Biological / Drug-A").
188
+ expect(result.experimentalGroups![0].name).toBe('Biological / Drug-A')
189
+ expect(result.experimentalGroups![0].samples).toEqual(['s1', 's2'])
190
+ expect(result.excludedSamples).toEqual(['s3'])
191
+ })
192
+
193
+ it('stores post-alias / post-bin display values in MetadataRow.fields', () => {
194
+ const schema: ClassSchema = {
195
+ classKind: 'biological',
196
+ columns: [
197
+ {
198
+ index: 0,
199
+ name: 'Tissue',
200
+ sourceIndices: [0],
201
+ uniqueValues: ['Kidney', 'Liver'],
202
+ cardinality: 2,
203
+ role: 'factor',
204
+ ops: { alias: { Kidney: 'kidney_panel' } },
205
+ },
206
+ ],
207
+ groupBy: [0],
208
+ }
209
+ const result = composeGroups({
210
+ tokenizedSamples: [['Kidney']],
211
+ sampleNames: ['s1'],
212
+ schemas: { biological: schema },
213
+ classes: [
214
+ {
215
+ kind: 'biological',
216
+ label: 'Biological',
217
+ members: [0],
218
+ classTagPositions: [],
219
+ disposition: 'group',
220
+ },
221
+ ],
222
+ })
223
+ expect(result.metadata[0].fields.Tissue).toBe('kidney_panel')
224
+ // Group label is class-prefixed: cls.label ("Biological") + post-alias value.
225
+ expect(result.metadata[0].group).toBe('Biological / kidney_panel')
226
+ })
227
+ })
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { findMerges } from '../../../composables/autoGroup/cooccurrence'
3
+ import type { ClassSchema } from '../../../types/auto-group'
4
+
5
+ const schema = (overrides: Partial<ClassSchema> = {}): ClassSchema => ({
6
+ classKind: 'biological',
7
+ columns: [
8
+ { index: 0, name: 'A', sourceIndices: [0], uniqueValues: ['SAM'], cardinality: 1, role: 'factor' },
9
+ { index: 1, name: 'B', sourceIndices: [1], uniqueValues: ['SAH'], cardinality: 1, role: 'factor' },
10
+ ],
11
+ groupBy: [],
12
+ ...overrides,
13
+ })
14
+
15
+ describe('findMerges', () => {
16
+ it('suggests merging SAM + SAH when both columns are factors and mutually paired', () => {
17
+ const s = schema({
18
+ columns: [
19
+ { index: 0, name: 'A', sourceIndices: [0], uniqueValues: ['SAM', 'WT'], cardinality: 2, role: 'factor' },
20
+ { index: 1, name: 'B', sourceIndices: [1], uniqueValues: ['SAH', 'CTL'], cardinality: 2, role: 'factor' },
21
+ ],
22
+ })
23
+ const rows = [
24
+ ['SAM', 'SAH'],
25
+ ['SAM', 'SAH'],
26
+ ['SAM', 'SAH'],
27
+ ['SAM', 'SAH'],
28
+ ['WT', 'CTL'],
29
+ ['WT', 'CTL'],
30
+ ['WT', 'CTL'],
31
+ ['WT', 'CTL'],
32
+ ]
33
+ const out = findMerges(s, rows)
34
+ expect(out.length).toBe(1)
35
+ expect(out[0].columnIndices).toEqual([0, 1])
36
+ expect(out[0].confidence).toBeGreaterThanOrEqual(0.95)
37
+ })
38
+
39
+ it('rejects merge between two adjacent constants (no factor among them)', () => {
40
+ const s = schema({
41
+ columns: [
42
+ { index: 0, name: 'A', sourceIndices: [0], uniqueValues: ['exp281'], cardinality: 1, role: 'constant' },
43
+ { index: 1, name: 'B', sourceIndices: [1], uniqueValues: ['260415'], cardinality: 1, role: 'constant' },
44
+ ],
45
+ })
46
+ const rows = Array.from({ length: 10 }, () => ['exp281', '260415'])
47
+ expect(findMerges(s, rows)).toEqual([])
48
+ })
49
+
50
+ it('rejects asymmetric pairing (forward functional but not backward)', () => {
51
+ const s = schema({
52
+ columns: [
53
+ { index: 0, name: 'A', sourceIndices: [0], uniqueValues: ['a', 'b'], cardinality: 2, role: 'factor' },
54
+ { index: 1, name: 'B', sourceIndices: [1], uniqueValues: ['x'], cardinality: 1, role: 'constant' },
55
+ ],
56
+ })
57
+ const rows = [
58
+ ['a', 'x'],
59
+ ['a', 'x'],
60
+ ['b', 'x'],
61
+ ['b', 'x'],
62
+ ]
63
+ expect(findMerges(s, rows)).toEqual([])
64
+ })
65
+
66
+ it('rejects suggestions when class size < 4', () => {
67
+ const s = schema({
68
+ columns: [
69
+ { index: 0, name: 'A', sourceIndices: [0], uniqueValues: ['a', 'b'], cardinality: 2, role: 'factor' },
70
+ { index: 1, name: 'B', sourceIndices: [1], uniqueValues: ['x', 'y'], cardinality: 2, role: 'factor' },
71
+ ],
72
+ })
73
+ const rows = [
74
+ ['a', 'x'],
75
+ ['b', 'y'],
76
+ ['a', 'x'],
77
+ ]
78
+ expect(findMerges(s, rows)).toEqual([])
79
+ })
80
+
81
+ it('rejects merge that would collapse two adjacent factors into a single constant', () => {
82
+ const s = schema({
83
+ columns: [
84
+ { index: 0, name: 'A', sourceIndices: [0], uniqueValues: ['x'], cardinality: 1, role: 'factor' },
85
+ { index: 1, name: 'B', sourceIndices: [1], uniqueValues: ['y'], cardinality: 1, role: 'factor' },
86
+ ],
87
+ })
88
+ const rows = Array.from({ length: 6 }, () => ['x', 'y'])
89
+ expect(findMerges(s, rows)).toEqual([])
90
+ })
91
+ })
@@ -0,0 +1,50 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { serializeFingerprint, restoreFingerprint } from '../../../composables/autoGroup/fingerprint'
3
+ import type { ClassSchema } from '../../../types/auto-group'
4
+
5
+ const schemas: ClassSchema[] = [
6
+ {
7
+ classKind: 'biological',
8
+ subKind: 'Tissues',
9
+ columns: [
10
+ {
11
+ index: 0,
12
+ name: 'Tissue',
13
+ sourceIndices: [0],
14
+ uniqueValues: ['Kidney', 'Liver'],
15
+ cardinality: 2,
16
+ role: 'factor',
17
+ ops: { alias: { Kidney: 'kidney_panel' } },
18
+ },
19
+ ],
20
+ groupBy: [0],
21
+ },
22
+ ]
23
+
24
+ describe('fingerprint round-trip', () => {
25
+ it('serializes to a JSON-stringify-safe object (no Maps)', () => {
26
+ const fp = serializeFingerprint(schemas)
27
+ const stringified = JSON.stringify(fp)
28
+ const reparsed = JSON.parse(stringified)
29
+ expect(reparsed.version).toBe(1)
30
+ expect(reparsed.classes[0].columns[0].ops.alias.Kidney).toBe('kidney_panel')
31
+ })
32
+
33
+ it('round-trips through restore preserving group-by and ops', () => {
34
+ const fp = serializeFingerprint(schemas)
35
+ const restored = restoreFingerprint(fp, schemas)
36
+ expect(restored[0].groupBy).toEqual([0])
37
+ expect(restored[0].columns[0].ops?.alias?.Kidney).toBe('kidney_panel')
38
+ })
39
+
40
+ it('reports a clear error when current schema column count differs', () => {
41
+ const fp = serializeFingerprint(schemas)
42
+ const mismatched: ClassSchema[] = [
43
+ {
44
+ ...schemas[0],
45
+ columns: [], // empty
46
+ },
47
+ ]
48
+ expect(() => restoreFingerprint(fp, mismatched)).toThrow(/column count/i)
49
+ })
50
+ })
@@ -0,0 +1,79 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { readFileSync } from 'node:fs'
3
+ import { resolve } from 'node:path'
4
+ import {
5
+ pickPrimaryDelimiter,
6
+ splitMulti,
7
+ detectClass,
8
+ buildClassSchema,
9
+ composeGroups,
10
+ } from '../../../composables/autoGroup'
11
+
12
+ const FIXTURE = resolve(__dirname, '../../fixtures/auto-group/mixed-lc-ms-batch.txt')
13
+
14
+ describe('mixed LC-MS batch integration', () => {
15
+ const samples = readFileSync(FIXTURE, 'utf-8').trim().split('\n')
16
+ const delim = pickPrimaryDelimiter(samples)
17
+ const tokenized = samples.map(s => splitMulti(s, delim))
18
+ const classes = detectClass(tokenized, {})
19
+
20
+ it('detects exactly 81 Biological/Tissues samples', () => {
21
+ const c = classes.find(c => c.kind === 'biological' && c.subKind === 'Tissues')
22
+ expect(c?.members.length).toBe(81)
23
+ })
24
+
25
+ it('detects exactly 81 Biological/Plasma samples', () => {
26
+ const c = classes.find(c => c.kind === 'biological' && c.subKind === 'Plasma')
27
+ expect(c?.members.length).toBe(81)
28
+ })
29
+
30
+ it('detects exactly 3 IQC and 3 EQC samples', () => {
31
+ const iqc = classes.find(c => c.kind === 'iqc')
32
+ const eqc = classes.find(c => c.kind === 'eqc')
33
+ expect(iqc?.members.length).toBe(3)
34
+ expect(eqc?.members.length).toBe(3)
35
+ })
36
+
37
+ it('totals biological + QC overlay + excluded === 187', () => {
38
+ const tissues = classes.find(c => c.kind === 'biological' && c.subKind === 'Tissues')
39
+ const plasma = classes.find(c => c.kind === 'biological' && c.subKind === 'Plasma')
40
+ const overlayKinds = ['iqc', 'eqc', 'standard', 'spike-in', 'stock', 'blank']
41
+ const overlayCount = classes
42
+ .filter(c => overlayKinds.includes(c.kind))
43
+ .reduce((acc, c) => acc + c.members.length, 0)
44
+ const unknownCount = classes
45
+ .filter(c => c.kind === 'unknown')
46
+ .reduce((acc, c) => acc + c.members.length, 0)
47
+ expect((tissues?.members.length ?? 0) + (plasma?.members.length ?? 0) + overlayCount + unknownCount).toBe(187)
48
+ })
49
+
50
+ it('default workspace produces 3 Kidney/Liver/Tumor groups of 27 each', () => {
51
+ const tissuesClass = classes.find(c => c.kind === 'biological' && c.subKind === 'Tissues')!
52
+ const tissuesTokens = tissuesClass.members.map(i => tokenized[i])
53
+ const schema = buildClassSchema(
54
+ tissuesTokens,
55
+ 'biological',
56
+ 'Tissues',
57
+ tissuesClass.classTagPositions,
58
+ )
59
+ // The Tissue column should be a factor with 3 unique values.
60
+ const tissueCol = schema.columns.find(c => c.uniqueValues.length === 3 && c.role === 'factor')
61
+ expect(tissueCol).toBeDefined()
62
+ const result = composeGroups({
63
+ tokenizedSamples: tokenized,
64
+ sampleNames: samples,
65
+ schemas: { 'biological:Tissues': { ...schema, groupBy: [tissueCol!.index] } },
66
+ classes: [tissuesClass],
67
+ })
68
+ expect(result.experimentalGroups!.length).toBe(3)
69
+ expect(result.experimentalGroups!.every(g => g.samples.length === 27)).toBe(true)
70
+ const names = result.experimentalGroups!.map(g => g.name).sort()
71
+ // Class-prefixed: "Biological / Tissues / <organ>" disambiguates from
72
+ // other classes that might also produce a Kidney/Liver/Tumor factor.
73
+ expect(names).toEqual([
74
+ 'Biological / Tissues / Kidney',
75
+ 'Biological / Tissues / Liver',
76
+ 'Biological / Tissues / Tumor',
77
+ ])
78
+ })
79
+ })
@@ -0,0 +1,70 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { composeTemplate } from '../../../composables/autoGroup/template'
3
+ import type { ClassSchema, SampleClass } from '../../../types/auto-group'
4
+
5
+ describe('composeTemplate', () => {
6
+ const samples = ['s_1', 's_2,with_comma', 's_3']
7
+ const classes: SampleClass[] = [
8
+ {
9
+ kind: 'biological',
10
+ subKind: 'Tissues',
11
+ label: 'Biological / Tissues',
12
+ members: [0, 1, 2],
13
+ classTagPositions: [],
14
+ disposition: 'group',
15
+ },
16
+ ]
17
+ const schema: ClassSchema = {
18
+ classKind: 'biological',
19
+ subKind: 'Tissues',
20
+ columns: [
21
+ { index: 0, name: 'Tissue', sourceIndices: [0], uniqueValues: ['Kidney', 'Liver'], cardinality: 2, role: 'factor' },
22
+ ],
23
+ groupBy: [0],
24
+ }
25
+
26
+ it('blank mode emits one row per sample with sample_name and header-only suggested columns', () => {
27
+ const { content } = composeTemplate(samples, null, null, { mode: 'blank', format: 'csv' })
28
+ const lines = content.split('\n')
29
+ expect(lines[0]).toBe('sample_name,group,class,replicate,notes')
30
+ expect(lines[1].startsWith('s_1,')).toBe(true)
31
+ expect(lines.length).toBe(samples.length + 1)
32
+ })
33
+
34
+ it('quotes values containing commas per RFC 4180', () => {
35
+ const { content } = composeTemplate(samples, null, null, { mode: 'blank', format: 'csv' })
36
+ expect(content).toContain('"s_2,with_comma"')
37
+ })
38
+
39
+ it('prefilled mode includes class label and schema columns', () => {
40
+ const tokenized = [['Kidney'], ['Kidney'], ['Liver']]
41
+ const { content } = composeTemplate(samples, [schema], classes, {
42
+ mode: 'prefilled',
43
+ format: 'csv',
44
+ tokenizedSamples: tokenized,
45
+ })
46
+ const lines = content.split('\n')
47
+ expect(lines[0]).toBe('sample_name,class,Tissue,group,notes')
48
+ expect(lines[1]).toContain('Biological / Tissues')
49
+ expect(lines[1]).toContain('Kidney')
50
+ })
51
+
52
+ it('TSV format uses tab separator and skips quoting commas', () => {
53
+ const { content } = composeTemplate(samples, null, null, { mode: 'blank', format: 'tsv' })
54
+ expect(content.split('\n')[2].split('\t')[0]).toBe('s_2,with_comma')
55
+ })
56
+
57
+ it('uses experiment code in filename when provided', () => {
58
+ const { filename } = composeTemplate(samples, null, null, {
59
+ mode: 'blank',
60
+ format: 'csv',
61
+ experimentCode: 'EXP-042',
62
+ })
63
+ expect(filename).toMatch(/^EXP-042_metadata_\d{4}-\d{2}-\d{2}\.csv$/)
64
+ })
65
+
66
+ it('falls back to default filename without experiment code', () => {
67
+ const { filename } = composeTemplate(samples, null, null, { mode: 'blank', format: 'csv' })
68
+ expect(filename).toMatch(/^sample_metadata_\d{4}-\d{2}-\d{2}\.csv$/)
69
+ })
70
+ })
@@ -0,0 +1,33 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { splitMulti, pickPrimaryDelimiter } from '../../../composables/autoGroup/tokenize'
3
+
4
+ describe('pickPrimaryDelimiter', () => {
5
+ it('should pick underscore when underscore field counts are most consistent', () => {
6
+ const lines = ['a_b_c', 'd_e_f', 'g_h_i']
7
+ expect(pickPrimaryDelimiter(lines)).toBe('_')
8
+ })
9
+
10
+ it('should pick hyphen when underscore is absent but hyphen is consistent', () => {
11
+ const lines = ['a-b-c', 'd-e-f']
12
+ expect(pickPrimaryDelimiter(lines)).toBe('-')
13
+ })
14
+
15
+ it('should fall back to underscore when no delimiter splits anything', () => {
16
+ const lines = ['abc', 'def']
17
+ expect(pickPrimaryDelimiter(lines)).toBe('_')
18
+ })
19
+ })
20
+
21
+ describe('splitMulti', () => {
22
+ it('should split a single sample on the primary delimiter', () => {
23
+ expect(splitMulti('a_b_c', '_')).toEqual(['a', 'b', 'c'])
24
+ })
25
+
26
+ it('should not re-split on secondary delimiters', () => {
27
+ expect(splitMulti('a-b_c-d', '_')).toEqual(['a-b', 'c-d'])
28
+ })
29
+
30
+ it('should handle empty string as a single empty token', () => {
31
+ expect(splitMulti('', '_')).toEqual([''])
32
+ })
33
+ })