@morscherlab/mint-sdk 1.0.0-rc.2 → 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.
- package/dist/__tests__/components/AppTopBar.navigation.test.d.ts +1 -0
- package/dist/__tests__/components/DoseCalculatorVolumeField.test.d.ts +1 -0
- package/dist/__tests__/components/PlateMapEditorToolbarInternal.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.controls.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.navigation.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.shell.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStep.presentation.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStepEditor.state.test.d.ts +1 -0
- package/dist/__tests__/components/ProtocolStepParameterField.test.d.ts +1 -0
- package/dist/__tests__/components/ReagentList.presentation.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.colors.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.drag.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.groups.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelector.selection.test.d.ts +1 -0
- package/dist/__tests__/components/SampleSelectorSampleRow.test.d.ts +1 -0
- package/dist/__tests__/components/ScheduleCalendar.test.d.ts +1 -0
- package/dist/__tests__/components/SettingsModal.schema.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.colors.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.conditions.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.geometry.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.interaction.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.legend.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.rendering.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.sampleDrop.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/classify.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/columns.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/compose.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/cooccurrence.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/fingerprint.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/integration.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/template.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/tokenize.test.d.ts +1 -0
- package/dist/__tests__/composables/useAutoGroupInputSources.test.d.ts +1 -0
- package/dist/__tests__/composables/useScheduleCalendarLayout.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsComponents.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsExports.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsParsing.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsTemplates.test.d.ts +1 -0
- package/dist/__tests__/docs/extractDocsTheme.test.d.ts +1 -0
- package/dist/components/AppTopBar.navigation.d.ts +11 -0
- package/dist/components/BaseButton.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseInput.vue.d.ts +2 -2
- package/dist/components/BasePill.vue.d.ts +1 -1
- package/dist/components/BaseRadioGroup.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseSlider.vue.d.ts +2 -2
- package/dist/components/BaseTextarea.vue.d.ts +2 -2
- package/dist/components/BaseToggle.vue.d.ts +1 -1
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
- package/dist/components/ColorSlider.vue.d.ts +2 -2
- package/dist/components/ConcentrationInput.vue.d.ts +2 -2
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +2 -2
- package/dist/components/DoseCalculatorVolumeField.vue.d.ts +15 -0
- package/dist/components/DropdownButton.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/FormulaInput.vue.d.ts +2 -2
- package/dist/components/IconButton.vue.d.ts +1 -1
- package/dist/components/LoadingSpinner.vue.d.ts +1 -1
- package/dist/components/MoleculeInput.vue.d.ts +2 -2
- package/dist/components/MultiSelect.vue.d.ts +1 -1
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/PlateMapEditor.vue.d.ts +6 -6
- package/dist/components/PluginWorkspaceView.controls.d.ts +28 -0
- package/dist/components/PluginWorkspaceView.navigation.d.ts +29 -0
- package/dist/components/PluginWorkspaceView.props.d.ts +151 -0
- package/dist/components/PluginWorkspaceView.shell.d.ts +19 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +46 -195
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStep.presentation.d.ts +4 -0
- package/dist/components/ProtocolStepEditor.state.d.ts +18 -0
- package/dist/components/ProtocolStepParameterField.vue.d.ts +12 -0
- package/dist/components/ReagentList.presentation.d.ts +16 -0
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SampleSelector.colors.d.ts +13 -0
- package/dist/components/SampleSelector.drag.d.ts +24 -0
- package/dist/components/SampleSelector.groups.d.ts +15 -0
- package/dist/components/SampleSelector.selection.d.ts +26 -0
- package/dist/components/SampleSelector.vue.d.ts +4 -1
- package/dist/components/SampleSelectorSampleRow.vue.d.ts +21 -0
- package/dist/components/SegmentedControl.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +2 -2
- package/dist/components/SequenceProgressBar.vue.d.ts +1 -1
- package/dist/components/SettingsModal.schema.d.ts +9 -0
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +2 -2
- package/dist/components/TimePicker.vue.d.ts +2 -2
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +2 -2
- package/dist/components/WellPlate.colors.d.ts +9 -0
- package/dist/components/WellPlate.conditions.d.ts +26 -0
- package/dist/components/WellPlate.geometry.d.ts +23 -0
- package/dist/components/WellPlate.interaction.d.ts +71 -0
- package/dist/components/WellPlate.legend.d.ts +2 -0
- package/dist/components/WellPlate.rendering.d.ts +24 -0
- package/dist/components/WellPlate.sampleDrop.d.ts +8 -0
- package/dist/components/WellPlate.vue.d.ts +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/internal/ActionItemInternal.vue.d.ts +1 -1
- package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +28 -0
- package/dist/{components-BhK-dW99.js → components-DtHA2bgp.js} +3754 -2991
- package/dist/components-DtHA2bgp.js.map +1 -0
- package/dist/composables/autoGroup/classKey.d.ts +4 -0
- package/dist/composables/autoGroup/classify.d.ts +28 -0
- package/dist/composables/autoGroup/colors.d.ts +2 -0
- package/dist/composables/autoGroup/columns.d.ts +10 -0
- package/dist/composables/autoGroup/compose.d.ts +8 -0
- package/dist/composables/autoGroup/cooccurrence.d.ts +2 -0
- package/dist/composables/autoGroup/csv-shim.d.ts +2 -0
- package/dist/composables/autoGroup/fingerprint.d.ts +3 -0
- package/dist/composables/autoGroup/index.d.ts +16 -0
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +38 -0
- package/dist/composables/autoGroup/template.d.ts +15 -0
- package/dist/composables/autoGroup/tokenize.d.ts +8 -0
- package/dist/composables/autoGroupConstants.d.ts +1 -0
- package/dist/composables/autoGroupGrouping.d.ts +3 -0
- package/dist/composables/controlComponentBindings.d.ts +7 -0
- package/dist/composables/controlSchemaAdapters.d.ts +20 -0
- package/dist/composables/controlSchemaDoseDesign.d.ts +11 -0
- package/dist/composables/controlSchemaFormFields.d.ts +3 -0
- package/dist/composables/controlSchemaLayout.d.ts +7 -0
- package/dist/composables/controlSchemaModel.d.ts +5 -0
- package/dist/composables/controlSchemaNormalize.d.ts +15 -0
- package/dist/composables/controlSchemaTypes.d.ts +305 -0
- package/dist/composables/controlSchemaUtils.d.ts +9 -0
- package/dist/composables/controlWorkspaceOptions.d.ts +2 -0
- package/dist/composables/formBuilderSchema.d.ts +18 -0
- package/dist/composables/index.js +3 -3
- package/dist/composables/pluginEndpointBuilder.d.ts +13 -0
- package/dist/composables/protocolTemplateCatalog.d.ts +26 -0
- package/dist/composables/useAutoGroup.d.ts +61 -74
- package/dist/composables/useAutoGroupInputSources.d.ts +32 -0
- package/dist/composables/useBioTemplateControls.d.ts +1 -1
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +1 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +1 -1
- package/dist/composables/useControlSchema.d.ts +8 -346
- package/dist/composables/useControlWorkspace.d.ts +5 -0
- package/dist/composables/useForm.d.ts +2 -33
- package/dist/composables/useFormBuilder.d.ts +2 -9
- package/dist/composables/useFormValidation.d.ts +34 -0
- package/dist/composables/usePluginClient.d.ts +1 -4
- package/dist/composables/useProtocolTemplates.d.ts +2 -24
- package/dist/composables/useScheduleCalendarLayout.d.ts +49 -0
- package/dist/{composables-Bg7CFuNz.js → composables-Dlg8jenH.js} +33 -31
- package/dist/composables-Dlg8jenH.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/install.js +2 -2
- package/dist/styles.css +547 -516
- package/dist/templates/adapters.d.ts +14 -47
- package/dist/templates/assayLookups.d.ts +3 -0
- package/dist/templates/assayMatrixAdapters.d.ts +6 -0
- package/dist/templates/assayMatrixBuilder.d.ts +2 -0
- package/dist/templates/assayNormalizers.d.ts +4 -0
- package/dist/templates/builderDefaults.d.ts +1 -0
- package/dist/templates/builderIdUtils.d.ts +4 -0
- package/dist/templates/builderPresetControls.d.ts +10 -0
- package/dist/templates/builderReadUtils.d.ts +8 -0
- package/dist/templates/builders.d.ts +26 -67
- package/dist/templates/calibrationCurveAdapters.d.ts +4 -0
- package/dist/templates/calibrationCurveBuilder.d.ts +2 -0
- package/dist/templates/calibrationNormalizers.d.ts +5 -0
- package/dist/templates/componentBindingCatalog.d.ts +25 -0
- package/dist/templates/componentBindingHelpers.d.ts +17 -0
- package/dist/templates/componentDoseResponseProps.d.ts +3 -0
- package/dist/templates/componentGenericProps.d.ts +8 -0
- package/dist/templates/componentPlateHelpers.d.ts +5 -0
- package/dist/templates/componentPlateMapProps.d.ts +3 -0
- package/dist/templates/componentPropsFactory.d.ts +3 -0
- package/dist/templates/componentQpcrPlateProps.d.ts +3 -0
- package/dist/templates/componentTargetResolvers.d.ts +5 -0
- package/dist/templates/componentTemplateProps.d.ts +3 -0
- package/dist/templates/controlSchemaClone.d.ts +4 -0
- package/dist/templates/controlSchemaConstants.d.ts +10 -0
- package/dist/templates/controlSchemaMerge.d.ts +3 -0
- package/dist/templates/controlSchemaTargets.d.ts +17 -0
- package/dist/templates/controlSchemaTypes.d.ts +4 -0
- package/dist/templates/controlSchemas.d.ts +4 -4
- package/dist/templates/defaultBioTemplateBuilder.d.ts +2 -0
- package/dist/templates/doseResponseAdapters.d.ts +4 -0
- package/dist/templates/doseResponseBuilder.d.ts +2 -0
- package/dist/templates/elisaAssayCollectionBuilder.d.ts +2 -0
- package/dist/templates/flowCytometryAssayCollectionBuilder.d.ts +2 -0
- package/dist/templates/flowCytometryPanelBuilder.d.ts +2 -0
- package/dist/templates/flowNormalizers.d.ts +8 -0
- package/dist/templates/flowPanelAdapters.d.ts +4 -0
- package/dist/templates/index.js +1 -1
- package/dist/templates/instrumentRunAdapterHelpers.d.ts +8 -0
- package/dist/templates/instrumentRunAdapters.d.ts +8 -0
- package/dist/templates/instrumentRunBuilder.d.ts +2 -0
- package/dist/templates/lcmsBatchCollectionBuilder.d.ts +2 -0
- package/dist/templates/plateGeometry.d.ts +4 -0
- package/dist/templates/plateMapAdapters.d.ts +3 -0
- package/dist/templates/plateMapBuilder.d.ts +2 -0
- package/dist/templates/presetControlSchemas.d.ts +534 -0
- package/dist/templates/protocolAdapters.d.ts +5 -0
- package/dist/templates/protocolNormalizers.d.ts +6 -0
- package/dist/templates/protocolStepsBuilder.d.ts +2 -0
- package/dist/templates/qpcrAdapters.d.ts +5 -0
- package/dist/templates/qpcrExpressionCollectionBuilder.d.ts +2 -0
- package/dist/templates/qpcrPlateBuilder.d.ts +2 -0
- package/dist/templates/reagentAdapters.d.ts +5 -0
- package/dist/templates/reagentListBuilder.d.ts +2 -0
- package/dist/templates/runNormalizers.d.ts +10 -0
- package/dist/templates/sampleNormalizers.d.ts +4 -0
- package/dist/templates/samplePrepAdapters.d.ts +4 -0
- package/dist/templates/samplePrepBuilder.d.ts +2 -0
- package/dist/templates/sampleSheetAdapters.d.ts +5 -0
- package/dist/templates/sampleSheetBuilder.d.ts +2 -0
- package/dist/templates/targetedMetabolomicsCollectionBuilder.d.ts +2 -0
- package/dist/templates/targetedMetabolomicsHelpers.d.ts +5 -0
- package/dist/templates/templateAdapterTypes.d.ts +48 -0
- package/dist/templates/templateControlSchemas.d.ts +400 -0
- package/dist/templates/templateCreateOptions.d.ts +165 -0
- package/dist/templates/templateEnvelopes.d.ts +9 -0
- package/dist/templates/templatePackCollectionBuilder.d.ts +2 -0
- package/dist/templates/templatePresetCollectionBuilder.d.ts +18 -0
- package/dist/templates/templateQpcrTypes.d.ts +42 -0
- package/dist/templates/templateValidators.d.ts +13 -0
- package/dist/templates/timeCourseAdapters.d.ts +5 -0
- package/dist/templates/timeCourseBuilder.d.ts +2 -0
- package/dist/templates/types.d.ts +5 -250
- package/dist/templates/wellPlateScreenCollectionBuilder.d.ts +2 -0
- package/dist/templates/westernBlotAssayCollectionBuilder.d.ts +2 -0
- package/dist/{templates-BorLR_7p.js → templates-DtdUvJ4c.js} +3565 -3411
- package/dist/templates-DtdUvJ4c.js.map +1 -0
- package/dist/types/auto-group.d.ts +79 -9
- package/dist/types/componentLabTypes.d.ts +161 -0
- package/dist/types/componentWorkflowTypes.d.ts +150 -0
- package/dist/types/components.d.ts +2 -311
- package/dist/{useProtocolTemplates-n6AJqSqv.js → useProtocolTemplates-Bm5vyH4_.js} +1220 -454
- package/dist/useProtocolTemplates-Bm5vyH4_.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.navigation.test.ts +70 -0
- package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +53 -0
- package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +54 -0
- package/src/__tests__/components/PluginWorkspaceView.controls.test.ts +156 -0
- package/src/__tests__/components/PluginWorkspaceView.navigation.test.ts +102 -0
- package/src/__tests__/components/PluginWorkspaceView.shell.test.ts +41 -0
- package/src/__tests__/components/ProtocolStep.presentation.test.ts +31 -0
- package/src/__tests__/components/ProtocolStepEditor.state.test.ts +165 -0
- package/src/__tests__/components/ProtocolStepParameterField.test.ts +44 -0
- package/src/__tests__/components/ReagentList.presentation.test.ts +68 -0
- package/src/__tests__/components/SampleSelector.colors.test.ts +49 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +100 -0
- package/src/__tests__/components/SampleSelector.groups.test.ts +81 -0
- package/src/__tests__/components/SampleSelector.selection.test.ts +70 -0
- package/src/__tests__/components/SampleSelector.test.ts +32 -0
- package/src/__tests__/components/SampleSelectorSampleRow.test.ts +37 -0
- package/src/__tests__/components/ScheduleCalendar.test.ts +44 -0
- package/src/__tests__/components/SettingsModal.schema.test.ts +97 -0
- package/src/__tests__/components/WellPlate.colors.test.ts +28 -0
- package/src/__tests__/components/WellPlate.conditions.test.ts +68 -0
- package/src/__tests__/components/WellPlate.geometry.test.ts +54 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +171 -0
- package/src/__tests__/components/WellPlate.legend.test.ts +13 -0
- package/src/__tests__/components/WellPlate.rendering.test.ts +122 -0
- package/src/__tests__/components/WellPlate.sampleDrop.test.ts +70 -0
- package/src/__tests__/composables/autoGroup/classify.test.ts +107 -0
- package/src/__tests__/composables/autoGroup/columns.test.ts +135 -0
- package/src/__tests__/composables/autoGroup/compose.test.ts +227 -0
- package/src/__tests__/composables/autoGroup/cooccurrence.test.ts +91 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +50 -0
- package/src/__tests__/composables/autoGroup/integration.test.ts +79 -0
- package/src/__tests__/composables/autoGroup/template.test.ts +70 -0
- package/src/__tests__/composables/autoGroup/tokenize.test.ts +33 -0
- package/src/__tests__/composables/useAutoGroup.test.ts +129 -625
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +107 -0
- package/src/__tests__/composables/useControlSchema.test.ts +23 -0
- package/src/__tests__/composables/useScheduleCalendarLayout.test.ts +89 -0
- package/src/__tests__/docs/extractDocsComponents.test.ts +142 -0
- package/src/__tests__/docs/extractDocsExports.test.ts +77 -0
- package/src/__tests__/docs/extractDocsParsing.test.ts +69 -0
- package/src/__tests__/docs/extractDocsTemplates.test.ts +54 -0
- package/src/__tests__/docs/extractDocsTheme.test.ts +89 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +1 -1
- package/src/__tests__/fixtures/auto-group/mixed-lc-ms-batch.txt +187 -0
- package/src/components/AppSidebar.vue +2 -6
- package/src/components/AppTopBar.navigation.ts +62 -0
- package/src/components/AppTopBar.vue +17 -44
- package/src/components/AutoGroupModal.story.vue +50 -0
- package/src/components/AutoGroupModal.vue +441 -158
- package/src/components/ControlWorkspaceView.vue +2 -6
- package/src/components/DoseCalculator.vue +13 -73
- package/src/components/DoseCalculatorVolumeField.vue +61 -0
- package/src/components/ExperimentTimeline.vue +6 -31
- package/src/components/FormBuilder.vue +2 -7
- package/src/components/PlateMapEditor.vue +32 -106
- package/src/components/PluginWorkspaceView.controls.ts +182 -0
- package/src/components/PluginWorkspaceView.navigation.ts +106 -0
- package/src/components/PluginWorkspaceView.props.ts +174 -0
- package/src/components/PluginWorkspaceView.shell.ts +66 -0
- package/src/components/PluginWorkspaceView.vue +85 -404
- package/src/components/ProtocolStep.presentation.ts +31 -0
- package/src/components/ProtocolStepEditor.state.ts +104 -0
- package/src/components/ProtocolStepEditor.vue +48 -179
- package/src/components/ProtocolStepParameterField.vue +134 -0
- package/src/components/ReagentList.presentation.ts +105 -0
- package/src/components/ReagentList.vue +16 -79
- package/src/components/SampleSelector.colors.ts +43 -0
- package/src/components/SampleSelector.drag.ts +164 -0
- package/src/components/SampleSelector.groups.ts +109 -0
- package/src/components/SampleSelector.selection.ts +103 -0
- package/src/components/SampleSelector.vue +82 -349
- package/src/components/SampleSelectorSampleRow.vue +64 -0
- package/src/components/ScheduleCalendar.vue +44 -199
- package/src/components/SettingsModal.schema.ts +71 -0
- package/src/components/SettingsModal.vue +16 -46
- package/src/components/WellPlate.colors.ts +56 -0
- package/src/components/WellPlate.conditions.ts +100 -0
- package/src/components/WellPlate.geometry.ts +91 -0
- package/src/components/WellPlate.interaction.ts +272 -0
- package/src/components/WellPlate.legend.ts +8 -0
- package/src/components/WellPlate.rendering.ts +105 -0
- package/src/components/WellPlate.sampleDrop.ts +73 -0
- package/src/components/WellPlate.vue +102 -550
- package/src/components/internal/PlateMapEditorToolbarInternal.vue +128 -0
- package/src/composables/autoGroup/classKey.ts +5 -0
- package/src/composables/autoGroup/classify.ts +205 -0
- package/src/composables/autoGroup/colors.ts +6 -0
- package/src/composables/autoGroup/columns.ts +226 -0
- package/src/composables/autoGroup/compose.ts +156 -0
- package/src/composables/autoGroup/cooccurrence.ts +46 -0
- package/src/composables/autoGroup/csv-shim.ts +44 -0
- package/src/composables/autoGroup/fingerprint.ts +49 -0
- package/src/composables/autoGroup/index.ts +20 -0
- package/src/composables/autoGroup/replicatePreGroup.ts +90 -0
- package/src/composables/autoGroup/template.ts +126 -0
- package/src/composables/autoGroup/tokenize.ts +41 -0
- package/src/composables/autoGroup/vocab.json +67 -0
- package/src/composables/autoGroupConstants.ts +4 -0
- package/src/composables/autoGroupGrouping.ts +148 -0
- package/src/composables/controlComponentBindings.ts +80 -0
- package/src/composables/controlSchemaAdapters.ts +196 -0
- package/src/composables/controlSchemaDoseDesign.ts +215 -0
- package/src/composables/controlSchemaFormFields.ts +61 -0
- package/src/composables/controlSchemaLayout.ts +59 -0
- package/src/composables/controlSchemaModel.ts +163 -0
- package/src/composables/controlSchemaNormalize.ts +101 -0
- package/src/composables/controlSchemaTypes.ts +364 -0
- package/src/composables/controlSchemaUtils.ts +36 -0
- package/src/composables/controlWorkspaceOptions.ts +21 -0
- package/src/composables/formBuilderSchema.ts +153 -0
- package/src/composables/pluginEndpointBuilder.ts +203 -0
- package/src/composables/protocolTemplateCatalog.ts +325 -0
- package/src/composables/useAutoGroup.ts +395 -549
- package/src/composables/useAutoGroupInputSources.ts +147 -0
- package/src/composables/useBioTemplateControls.ts +1 -1
- package/src/composables/useBioTemplatePresetWorkspace.ts +1 -1
- package/src/composables/useBioTemplateWorkspace.ts +1 -1
- package/src/composables/useControlSchema.ts +64 -1312
- package/src/composables/useControlWorkspace.ts +201 -0
- package/src/composables/useForm.ts +5 -187
- package/src/composables/useFormBuilder.ts +11 -153
- package/src/composables/useFormValidation.ts +154 -0
- package/src/composables/usePluginClient.ts +10 -193
- package/src/composables/useProtocolTemplates.ts +10 -328
- package/src/composables/useScheduleCalendarLayout.ts +287 -0
- package/src/styles/components/auto-group-modal.css +248 -310
- package/src/templates/adapters.ts +89 -930
- package/src/templates/assayLookups.ts +33 -0
- package/src/templates/assayMatrixAdapters.ts +78 -0
- package/src/templates/assayMatrixBuilder.ts +59 -0
- package/src/templates/assayNormalizers.ts +34 -0
- package/src/templates/builderDefaults.ts +11 -0
- package/src/templates/builderIdUtils.ts +20 -0
- package/src/templates/builderPresetControls.ts +165 -0
- package/src/templates/builderReadUtils.ts +57 -0
- package/src/templates/builders.ts +122 -2350
- package/src/templates/calibrationCurveAdapters.ts +59 -0
- package/src/templates/calibrationCurveBuilder.ts +99 -0
- package/src/templates/calibrationNormalizers.ts +60 -0
- package/src/templates/componentBindingCatalog.ts +90 -0
- package/src/templates/componentBindingHelpers.ts +93 -0
- package/src/templates/componentBindings.ts +12 -461
- package/src/templates/componentDoseResponseProps.ts +42 -0
- package/src/templates/componentGenericProps.ts +77 -0
- package/src/templates/componentPlateHelpers.ts +29 -0
- package/src/templates/componentPlateMapProps.ts +32 -0
- package/src/templates/componentPropsFactory.ts +21 -0
- package/src/templates/componentQpcrPlateProps.ts +28 -0
- package/src/templates/componentTargetResolvers.ts +69 -0
- package/src/templates/componentTemplateProps.ts +78 -0
- package/src/templates/controlSchemaClone.ts +32 -0
- package/src/templates/controlSchemaConstants.ts +11 -0
- package/src/templates/controlSchemaMerge.ts +40 -0
- package/src/templates/controlSchemaTargets.ts +87 -0
- package/src/templates/controlSchemaTypes.ts +20 -0
- package/src/templates/controlSchemas.ts +22 -704
- package/src/templates/defaultBioTemplateBuilder.ts +124 -0
- package/src/templates/doseResponseAdapters.ts +45 -0
- package/src/templates/doseResponseBuilder.ts +44 -0
- package/src/templates/elisaAssayCollectionBuilder.ts +62 -0
- package/src/templates/flowCytometryAssayCollectionBuilder.ts +41 -0
- package/src/templates/flowCytometryPanelBuilder.ts +53 -0
- package/src/templates/flowNormalizers.ts +58 -0
- package/src/templates/flowPanelAdapters.ts +58 -0
- package/src/templates/instrumentRunAdapterHelpers.ts +94 -0
- package/src/templates/instrumentRunAdapters.ts +163 -0
- package/src/templates/instrumentRunBuilder.ts +97 -0
- package/src/templates/lcmsBatchCollectionBuilder.ts +38 -0
- package/src/templates/plateGeometry.ts +62 -0
- package/src/templates/plateMapAdapters.ts +36 -0
- package/src/templates/plateMapBuilder.ts +43 -0
- package/src/templates/presetControlSchemas.ts +258 -0
- package/src/templates/protocolAdapters.ts +69 -0
- package/src/templates/protocolNormalizers.ts +37 -0
- package/src/templates/protocolStepsBuilder.ts +36 -0
- package/src/templates/qpcrAdapters.ts +104 -0
- package/src/templates/qpcrExpressionCollectionBuilder.ts +33 -0
- package/src/templates/qpcrPlateBuilder.ts +96 -0
- package/src/templates/reagentAdapters.ts +77 -0
- package/src/templates/reagentListBuilder.ts +30 -0
- package/src/templates/runNormalizers.ts +63 -0
- package/src/templates/sampleNormalizers.ts +58 -0
- package/src/templates/samplePrepAdapters.ts +63 -0
- package/src/templates/samplePrepBuilder.ts +51 -0
- package/src/templates/sampleSheetAdapters.ts +75 -0
- package/src/templates/sampleSheetBuilder.ts +23 -0
- package/src/templates/targetedMetabolomicsCollectionBuilder.ts +79 -0
- package/src/templates/targetedMetabolomicsHelpers.ts +102 -0
- package/src/templates/templateAdapterTypes.ts +58 -0
- package/src/templates/templateControlSchemas.ts +320 -0
- package/src/templates/templateCreateOptions.ts +208 -0
- package/src/templates/templateEnvelopes.ts +137 -0
- package/src/templates/templatePackCollectionBuilder.ts +23 -0
- package/src/templates/templatePresetCollectionBuilder.ts +139 -0
- package/src/templates/templateQpcrTypes.ts +48 -0
- package/src/templates/templateValidators.ts +414 -0
- package/src/templates/timeCourseAdapters.ts +73 -0
- package/src/templates/timeCourseBuilder.ts +64 -0
- package/src/templates/types.ts +79 -275
- package/src/templates/wellPlateScreenCollectionBuilder.ts +36 -0
- package/src/templates/westernBlotAssayCollectionBuilder.ts +68 -0
- package/src/types/auto-group.ts +107 -9
- package/src/types/componentLabTypes.ts +235 -0
- package/src/types/componentWorkflowTypes.ts +190 -0
- package/src/types/components.ts +74 -424
- package/dist/components-BhK-dW99.js.map +0 -1
- package/dist/composables-Bg7CFuNz.js.map +0 -1
- package/dist/templates-BorLR_7p.js.map +0 -1
- package/dist/useProtocolTemplates-n6AJqSqv.js.map +0 -1
|
@@ -1,229 +1,9 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest'
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
-
analyzeDelimiter,
|
|
4
|
-
detectOutliers,
|
|
5
|
-
classifyOutlierAction,
|
|
6
|
-
extractColumns,
|
|
7
3
|
parseCSV,
|
|
8
|
-
parseCSVLine,
|
|
9
|
-
computeGroups,
|
|
10
|
-
computeGroupsFromCsv,
|
|
11
4
|
extractSamplesFromDesignData,
|
|
12
|
-
DEFAULT_COLORS,
|
|
13
5
|
useAutoGroup,
|
|
14
6
|
} from '../../composables/useAutoGroup'
|
|
15
|
-
import type { OutlierAction } from '../../types/auto-group'
|
|
16
|
-
|
|
17
|
-
describe('analyzeDelimiter', () => {
|
|
18
|
-
it('should detect underscore delimiter', () => {
|
|
19
|
-
const lines = ['Ctrl_WT_1', 'Ctrl_WT_2', 'Treat_KO_1', 'Treat_KO_2']
|
|
20
|
-
const result = analyzeDelimiter(lines)
|
|
21
|
-
expect(result.delimiter).toBe('_')
|
|
22
|
-
expect(result.dominantFieldCount).toBe(3)
|
|
23
|
-
expect(result.consistency).toBe(1)
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
it('should detect hyphen delimiter', () => {
|
|
27
|
-
const lines = ['Ctrl-WT-1', 'Ctrl-WT-2', 'Treat-KO-1']
|
|
28
|
-
const result = analyzeDelimiter(lines)
|
|
29
|
-
expect(result.delimiter).toBe('-')
|
|
30
|
-
expect(result.dominantFieldCount).toBe(3)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
it('should detect dot delimiter', () => {
|
|
34
|
-
const lines = ['Ctrl.WT.1', 'Ctrl.WT.2', 'Treat.KO.1']
|
|
35
|
-
const result = analyzeDelimiter(lines)
|
|
36
|
-
expect(result.delimiter).toBe('.')
|
|
37
|
-
expect(result.dominantFieldCount).toBe(3)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('should pick most consistent delimiter with mixed usage', () => {
|
|
41
|
-
// Underscores used consistently (3 fields), hyphens appear but less consistently
|
|
42
|
-
const lines = ['Ctrl_WT_1', 'Ctrl_WT_2', 'Treat_KO-1', 'Treat_KO_2']
|
|
43
|
-
const result = analyzeDelimiter(lines)
|
|
44
|
-
expect(result.delimiter).toBe('_')
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('should prefer underscore when tied', () => {
|
|
48
|
-
// Both _ and - produce identical consistency
|
|
49
|
-
const lines = ['A_B-C']
|
|
50
|
-
const result = analyzeDelimiter(lines)
|
|
51
|
-
expect(result.delimiter).toBe('_')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('should handle single-segment samples (no delimiter)', () => {
|
|
55
|
-
const lines = ['SampleA', 'SampleB', 'SampleC']
|
|
56
|
-
const result = analyzeDelimiter(lines)
|
|
57
|
-
expect(result.dominantFieldCount).toBe(1)
|
|
58
|
-
expect(result.minFieldCount).toBe(1)
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
it('should handle empty input', () => {
|
|
62
|
-
const result = analyzeDelimiter([])
|
|
63
|
-
expect(result.delimiter).toBe('_')
|
|
64
|
-
expect(result.dominantFieldCount).toBe(1)
|
|
65
|
-
expect(result.minFieldCount).toBe(1)
|
|
66
|
-
expect(result.consistency).toBe(0)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('should compute correct consistency ratio', () => {
|
|
70
|
-
// 3 out of 4 samples have 3 fields with underscore
|
|
71
|
-
const lines = ['A_B_C', 'D_E_F', 'G_H_I', 'JK']
|
|
72
|
-
const result = analyzeDelimiter(lines)
|
|
73
|
-
expect(result.delimiter).toBe('_')
|
|
74
|
-
expect(result.dominantFieldCount).toBe(3)
|
|
75
|
-
expect(result.consistency).toBe(0.75)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('should return correct minFieldCount with mixed field counts', () => {
|
|
79
|
-
const lines = ['Control_Rep1', 'Treatment_Low_Rep1', 'Vehicle_Rep1']
|
|
80
|
-
const result = analyzeDelimiter(lines)
|
|
81
|
-
expect(result.delimiter).toBe('_')
|
|
82
|
-
expect(result.minFieldCount).toBe(2)
|
|
83
|
-
expect(result.dominantFieldCount).toBe(2)
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should return minFieldCount different from dominantFieldCount when needed', () => {
|
|
87
|
-
const lines = [
|
|
88
|
-
'Control_Rep1', 'Control_Rep2',
|
|
89
|
-
'Treatment_Low_Rep1', 'Treatment_Low_Rep2', 'Treatment_Low_Rep3',
|
|
90
|
-
'Treatment_High_Rep1', 'Treatment_High_Rep2', 'Treatment_High_Rep3',
|
|
91
|
-
]
|
|
92
|
-
const result = analyzeDelimiter(lines)
|
|
93
|
-
expect(result.delimiter).toBe('_')
|
|
94
|
-
// Mode is 3 (6 samples have 3 fields vs 2 samples with 2 fields)
|
|
95
|
-
expect(result.dominantFieldCount).toBe(3)
|
|
96
|
-
// Min of multi-field counts is 2
|
|
97
|
-
expect(result.minFieldCount).toBe(2)
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe('detectOutliers', () => {
|
|
102
|
-
it('should flag samples with fewer fields than minFieldCount', () => {
|
|
103
|
-
const lines = ['Ctrl_WT_1', 'Ctrl_WT_2', 'QC_Pool', 'Treat_KO_1']
|
|
104
|
-
const outliers = detectOutliers(lines, '_', 3)
|
|
105
|
-
expect(outliers).toHaveLength(1)
|
|
106
|
-
expect(outliers[0].sample).toBe('QC_Pool')
|
|
107
|
-
expect(outliers[0].index).toBe(2)
|
|
108
|
-
expect(outliers[0].fieldCount).toBe(2)
|
|
109
|
-
expect(outliers[0].action).toBe('include')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('should return empty array when all meet minFieldCount', () => {
|
|
113
|
-
const lines = ['A_B_C', 'D_E_F', 'G_H_I']
|
|
114
|
-
const outliers = detectOutliers(lines, '_', 3)
|
|
115
|
-
expect(outliers).toHaveLength(0)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('should preserve original line index', () => {
|
|
119
|
-
const lines = ['A_B', 'C', 'D_E', 'F']
|
|
120
|
-
const outliers = detectOutliers(lines, '_', 2)
|
|
121
|
-
expect(outliers).toHaveLength(2)
|
|
122
|
-
expect(outliers[0]).toEqual({ sample: 'C', index: 1, fieldCount: 1, action: 'include' })
|
|
123
|
-
expect(outliers[1]).toEqual({ sample: 'F', index: 3, fieldCount: 1, action: 'include' })
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('should NOT flag samples with more fields than minFieldCount', () => {
|
|
127
|
-
const lines = ['Ctrl_Rep1', 'Treat_Low_Rep1']
|
|
128
|
-
const outliers = detectOutliers(lines, '_', 2)
|
|
129
|
-
expect(outliers).toHaveLength(0)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should flag single-segment samples when minFieldCount requires delimiter', () => {
|
|
133
|
-
const lines = ['Ctrl_Rep1', 'QCPool']
|
|
134
|
-
const outliers = detectOutliers(lines, '_', 2)
|
|
135
|
-
expect(outliers).toHaveLength(1)
|
|
136
|
-
expect(outliers[0].sample).toBe('QCPool')
|
|
137
|
-
expect(outliers[0].fieldCount).toBe(1)
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
it('should not flag any sample when minFieldCount is 1', () => {
|
|
141
|
-
const lines = ['ABC', 'DEF', 'G_H_I']
|
|
142
|
-
const outliers = detectOutliers(lines, '_', 1)
|
|
143
|
-
expect(outliers).toHaveLength(0)
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
describe('extractColumns', () => {
|
|
148
|
-
it('should produce correct column count and unique values via right alignment', () => {
|
|
149
|
-
const samples = ['Ctrl_Liver_1', 'Ctrl_Brain_2', 'Treat_Liver_1']
|
|
150
|
-
const columns = extractColumns(samples, '_', 3)
|
|
151
|
-
expect(columns).toHaveLength(3)
|
|
152
|
-
expect(columns[0].uniqueValues).toEqual(['Ctrl', 'Treat'])
|
|
153
|
-
expect(columns[1].uniqueValues).toEqual(['Liver', 'Brain'])
|
|
154
|
-
expect(columns[2].uniqueValues).toEqual(['1', '2'])
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
it('should compute correct cardinality', () => {
|
|
158
|
-
const samples = ['A_X_1', 'A_Y_2', 'B_X_1', 'B_Y_2']
|
|
159
|
-
const columns = extractColumns(samples, '_', 3)
|
|
160
|
-
expect(columns[0].cardinality).toBe(2) // A, B
|
|
161
|
-
expect(columns[1].cardinality).toBe(2) // X, Y
|
|
162
|
-
expect(columns[2].cardinality).toBe(2) // 1, 2
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('should assign Condition as first column name and Field N for rest', () => {
|
|
166
|
-
const samples = ['A_B_C']
|
|
167
|
-
const columns = extractColumns(samples, '_', 3)
|
|
168
|
-
expect(columns[0].name).toBe('Condition')
|
|
169
|
-
expect(columns[1].name).toBe('Field 2')
|
|
170
|
-
expect(columns[2].name).toBe('Field 3')
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('should set correct column indices', () => {
|
|
174
|
-
const samples = ['A_B_C_D']
|
|
175
|
-
const columns = extractColumns(samples, '_', 4)
|
|
176
|
-
expect(columns.map(c => c.index)).toEqual([0, 1, 2, 3])
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('should handle empty input', () => {
|
|
180
|
-
const columns = extractColumns([], '_', 2)
|
|
181
|
-
expect(columns).toHaveLength(0)
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('should handle variable-length prefixes via right alignment', () => {
|
|
185
|
-
const samples = ['Control_Rep1', 'Treatment_Low_Rep1', 'Vehicle_Rep1']
|
|
186
|
-
const columns = extractColumns(samples, '_', 2)
|
|
187
|
-
expect(columns).toHaveLength(2)
|
|
188
|
-
expect(columns[0].uniqueValues).toEqual(['Control', 'Treatment_Low', 'Vehicle'])
|
|
189
|
-
expect(columns[1].uniqueValues).toEqual(['Rep1'])
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('should right-align with 3+ suffix fields', () => {
|
|
193
|
-
const samples = ['A_X_1', 'B_C_X_1']
|
|
194
|
-
const columns = extractColumns(samples, '_', 3)
|
|
195
|
-
expect(columns).toHaveLength(3)
|
|
196
|
-
expect(columns[0].uniqueValues).toEqual(['A', 'B_C'])
|
|
197
|
-
expect(columns[1].uniqueValues).toEqual(['X'])
|
|
198
|
-
expect(columns[2].uniqueValues).toEqual(['1'])
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
it('should mark column types (prefix vs suffix)', () => {
|
|
202
|
-
const samples = ['A_B_C']
|
|
203
|
-
const columns = extractColumns(samples, '_', 3)
|
|
204
|
-
expect(columns[0].type).toBe('prefix')
|
|
205
|
-
expect(columns[1].type).toBe('suffix')
|
|
206
|
-
expect(columns[2].type).toBe('suffix')
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe('parseCSVLine', () => {
|
|
211
|
-
it('should parse simple comma-separated line', () => {
|
|
212
|
-
expect(parseCSVLine('A,B,C')).toEqual(['A', 'B', 'C'])
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
it('should handle quoted fields with commas', () => {
|
|
216
|
-
expect(parseCSVLine('A,"B,C",D')).toEqual(['A', 'B,C', 'D'])
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('should trim whitespace from fields', () => {
|
|
220
|
-
expect(parseCSVLine(' A , B , C ')).toEqual(['A', 'B', 'C'])
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('should parse tab-separated line when delimiter is tab', () => {
|
|
224
|
-
expect(parseCSVLine('A\tB\tC', '\t')).toEqual(['A', 'B', 'C'])
|
|
225
|
-
})
|
|
226
|
-
})
|
|
227
7
|
|
|
228
8
|
describe('parseCSV', () => {
|
|
229
9
|
it('should parse simple CSV with headers', () => {
|
|
@@ -285,356 +65,6 @@ describe('parseCSV', () => {
|
|
|
285
65
|
})
|
|
286
66
|
})
|
|
287
67
|
|
|
288
|
-
describe('computeGroups', () => {
|
|
289
|
-
it('should group by single column', () => {
|
|
290
|
-
const samples = ['Ctrl_Liver_1', 'Ctrl_Brain_2', 'Treat_Liver_1']
|
|
291
|
-
const columns = extractColumns(samples, '_', 3)
|
|
292
|
-
const enabledFields = new Set([0]) // group by first column only
|
|
293
|
-
|
|
294
|
-
const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 3)
|
|
295
|
-
expect(result.groups).toHaveLength(2)
|
|
296
|
-
|
|
297
|
-
const ctrlGroup = result.groups.find(g => g.name === 'Ctrl')
|
|
298
|
-
const treatGroup = result.groups.find(g => g.name === 'Treat')
|
|
299
|
-
expect(ctrlGroup?.samples).toEqual(['Ctrl_Liver_1', 'Ctrl_Brain_2'])
|
|
300
|
-
expect(treatGroup?.samples).toEqual(['Treat_Liver_1'])
|
|
301
|
-
})
|
|
302
|
-
|
|
303
|
-
it('should group by multiple columns joined with " / "', () => {
|
|
304
|
-
const samples = ['Ctrl_Liver_1', 'Ctrl_Brain_2', 'Treat_Liver_1']
|
|
305
|
-
const columns = extractColumns(samples, '_', 3)
|
|
306
|
-
const enabledFields = new Set([0, 1])
|
|
307
|
-
|
|
308
|
-
const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 3)
|
|
309
|
-
expect(result.groups).toHaveLength(3)
|
|
310
|
-
|
|
311
|
-
const names = result.groups.map(g => g.name).sort()
|
|
312
|
-
expect(names).toEqual(['Ctrl / Brain', 'Ctrl / Liver', 'Treat / Liver'])
|
|
313
|
-
})
|
|
314
|
-
|
|
315
|
-
it('should cycle colors from DEFAULT_COLORS', () => {
|
|
316
|
-
const samples = Array.from({ length: 12 }, (_, i) => `G${i}_X`)
|
|
317
|
-
const columns = extractColumns(samples, '_', 2)
|
|
318
|
-
const enabledFields = new Set([0])
|
|
319
|
-
|
|
320
|
-
const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 2)
|
|
321
|
-
// 12 unique groups should cycle through 10 colors
|
|
322
|
-
expect(result.groups[0].color).toBe(DEFAULT_COLORS[0])
|
|
323
|
-
expect(result.groups[10].color).toBe(DEFAULT_COLORS[0])
|
|
324
|
-
expect(result.groups[11].color).toBe(DEFAULT_COLORS[1])
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
it('should exclude outliers marked as "exclude"', () => {
|
|
328
|
-
const samples = ['Ctrl_WT_1', 'Ctrl_WT_2', 'QC_Pool']
|
|
329
|
-
const columns = extractColumns(['Ctrl_WT_1', 'Ctrl_WT_2'], '_', 3)
|
|
330
|
-
const enabledFields = new Set([0])
|
|
331
|
-
const outlierActions = new Map<number, OutlierAction>([[2, 'exclude']])
|
|
332
|
-
|
|
333
|
-
const result = computeGroups(samples, columns, enabledFields, outlierActions, '_', 3)
|
|
334
|
-
expect(result.excludedSamples).toContain('QC_Pool')
|
|
335
|
-
const allGroupedSamples = result.groups.flatMap(g => g.samples)
|
|
336
|
-
expect(allGroupedSamples).not.toContain('QC_Pool')
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
it('should put QC-marked outliers in a QC group', () => {
|
|
340
|
-
const samples = ['Ctrl_WT_1', 'Ctrl_WT_2', 'QC_Pool']
|
|
341
|
-
const columns = extractColumns(['Ctrl_WT_1', 'Ctrl_WT_2'], '_', 3)
|
|
342
|
-
const enabledFields = new Set([0])
|
|
343
|
-
const outlierActions = new Map<number, OutlierAction>([[2, 'qc']])
|
|
344
|
-
|
|
345
|
-
const result = computeGroups(samples, columns, enabledFields, outlierActions, '_', 3)
|
|
346
|
-
const qcGroup = result.groups.find(g => g.name === 'QC')
|
|
347
|
-
expect(qcGroup).toBeDefined()
|
|
348
|
-
expect(qcGroup?.samples).toContain('QC_Pool')
|
|
349
|
-
expect(qcGroup?.color).toBe('#6B7280')
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
it('should generate metadata rows', () => {
|
|
353
|
-
const samples = ['Ctrl_Liver_1', 'Treat_Brain_2']
|
|
354
|
-
const columns = extractColumns(samples, '_', 3)
|
|
355
|
-
// Rename columns for metadata
|
|
356
|
-
columns[0].name = 'Condition'
|
|
357
|
-
columns[1].name = 'Tissue'
|
|
358
|
-
columns[2].name = 'Replicate'
|
|
359
|
-
const enabledFields = new Set([0, 1])
|
|
360
|
-
|
|
361
|
-
const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 3)
|
|
362
|
-
expect(result.metadata).toHaveLength(2)
|
|
363
|
-
expect(result.metadata[0]).toEqual({
|
|
364
|
-
sampleName: 'Ctrl_Liver_1',
|
|
365
|
-
fields: { Condition: 'Ctrl', Tissue: 'Liver', Replicate: '1' },
|
|
366
|
-
group: 'Ctrl / Liver',
|
|
367
|
-
})
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
it('should handle single column without separator in group name', () => {
|
|
371
|
-
const samples = ['A_1', 'B_2', 'A_3']
|
|
372
|
-
const columns = extractColumns(samples, '_', 2)
|
|
373
|
-
const enabledFields = new Set([0])
|
|
374
|
-
|
|
375
|
-
const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 2)
|
|
376
|
-
const names = result.groups.map(g => g.name)
|
|
377
|
-
expect(names).toContain('A')
|
|
378
|
-
expect(names).toContain('B')
|
|
379
|
-
// Should NOT contain " / " separator
|
|
380
|
-
expect(names.every(n => !n.includes(' / '))).toBe(true)
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
it('should correctly group variable-length prefixes', () => {
|
|
384
|
-
const samples = [
|
|
385
|
-
'Control_Rep1', 'Control_Rep2', 'Control_Rep3',
|
|
386
|
-
'Treatment_Low_Rep1', 'Treatment_Low_Rep2', 'Treatment_Low_Rep3',
|
|
387
|
-
]
|
|
388
|
-
const columns = extractColumns(samples, '_', 2)
|
|
389
|
-
const enabledFields = new Set([0])
|
|
390
|
-
|
|
391
|
-
const result = computeGroups(samples, columns, enabledFields, new Map(), '_', 2)
|
|
392
|
-
expect(result.groups).toHaveLength(2)
|
|
393
|
-
|
|
394
|
-
const ctrlGroup = result.groups.find(g => g.name === 'Control')
|
|
395
|
-
const treatGroup = result.groups.find(g => g.name === 'Treatment_Low')
|
|
396
|
-
expect(ctrlGroup?.samples).toHaveLength(3)
|
|
397
|
-
expect(treatGroup?.samples).toHaveLength(3)
|
|
398
|
-
})
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
describe('classifyOutlierAction', () => {
|
|
402
|
-
it('should return qc for samples containing QC keywords', () => {
|
|
403
|
-
expect(classifyOutlierAction('LT_13102025_EQC_Jurkat_1_2', '_')).toBe('qc')
|
|
404
|
-
expect(classifyOutlierAction('LT_13102025_IQC_Pool_1', '_')).toBe('qc')
|
|
405
|
-
expect(classifyOutlierAction('Blank_001', '_')).toBe('qc')
|
|
406
|
-
expect(classifyOutlierAction('STD_Low_1', '_')).toBe('qc')
|
|
407
|
-
expect(classifyOutlierAction('test_EQC_Jurkat_02', '_')).toBe('qc')
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
it('should match case-insensitively', () => {
|
|
411
|
-
expect(classifyOutlierAction('LT_eqc_Pool', '_')).toBe('qc')
|
|
412
|
-
expect(classifyOutlierAction('LT_EQC_Pool', '_')).toBe('qc')
|
|
413
|
-
expect(classifyOutlierAction('LT_Eqc_Pool', '_')).toBe('qc')
|
|
414
|
-
expect(classifyOutlierAction('BLANK_001', '_')).toBe('qc')
|
|
415
|
-
expect(classifyOutlierAction('Test_Sample', '_')).toBe('qc')
|
|
416
|
-
})
|
|
417
|
-
|
|
418
|
-
it('should match against individual segments only', () => {
|
|
419
|
-
// "eqc" embedded inside a segment should NOT match
|
|
420
|
-
expect(classifyOutlierAction('LT_SEQC123_Pool', '_')).toBe('include')
|
|
421
|
-
// "std" as standalone segment should match
|
|
422
|
-
expect(classifyOutlierAction('LT_std_Pool', '_')).toBe('qc')
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
it('should return include for regular experimental samples', () => {
|
|
426
|
-
expect(classifyOutlierAction('LT_13102025_212_WT_Glu', '_')).toBe('include')
|
|
427
|
-
expect(classifyOutlierAction('Control_Rep1', '_')).toBe('include')
|
|
428
|
-
expect(classifyOutlierAction('Treatment_High_Rep3', '_')).toBe('include')
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
it('should work with different delimiters', () => {
|
|
432
|
-
expect(classifyOutlierAction('LT-EQC-Pool', '-')).toBe('qc')
|
|
433
|
-
expect(classifyOutlierAction('blank.001', '.')).toBe('qc')
|
|
434
|
-
expect(classifyOutlierAction('Control-Rep1', '-')).toBe('include')
|
|
435
|
-
})
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
describe('computeGroupsFromCsv', () => {
|
|
439
|
-
it('should group samples by CSV column values', () => {
|
|
440
|
-
const csvData = parseCSV(
|
|
441
|
-
'File Name\tTreatment Group\n' +
|
|
442
|
-
'Sample_001\tCtrl\n' +
|
|
443
|
-
'Sample_002\tCtrl\n' +
|
|
444
|
-
'Sample_003\tTreat\n' +
|
|
445
|
-
'Sample_004\tTreat\n'
|
|
446
|
-
)
|
|
447
|
-
const columns = [
|
|
448
|
-
{ index: 0, name: 'Treatment Group', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
|
|
449
|
-
]
|
|
450
|
-
const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
|
|
451
|
-
expect(result.groups).toHaveLength(2)
|
|
452
|
-
|
|
453
|
-
const ctrl = result.groups.find(g => g.name === 'Ctrl')
|
|
454
|
-
const treat = result.groups.find(g => g.name === 'Treat')
|
|
455
|
-
expect(ctrl?.samples).toEqual(['Sample_001', 'Sample_002'])
|
|
456
|
-
expect(treat?.samples).toEqual(['Sample_003', 'Sample_004'])
|
|
457
|
-
expect(result.excludedSamples).toEqual([])
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
it('should group by multiple enabled CSV columns', () => {
|
|
461
|
-
const csvData = parseCSV(
|
|
462
|
-
'Sample,Condition,Tissue\nS1,Ctrl,Liver\nS2,Ctrl,Brain\nS3,Treat,Liver'
|
|
463
|
-
)
|
|
464
|
-
const columns = [
|
|
465
|
-
{ index: 0, name: 'Condition', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
|
|
466
|
-
{ index: 1, name: 'Tissue', uniqueValues: ['Liver', 'Brain'], cardinality: 2 },
|
|
467
|
-
]
|
|
468
|
-
const result = computeGroupsFromCsv(csvData, columns, new Set([0, 1]))
|
|
469
|
-
expect(result.groups).toHaveLength(3)
|
|
470
|
-
|
|
471
|
-
const names = result.groups.map(g => g.name).sort()
|
|
472
|
-
expect(names).toEqual(['Ctrl / Brain', 'Ctrl / Liver', 'Treat / Liver'])
|
|
473
|
-
})
|
|
474
|
-
|
|
475
|
-
it('should generate metadata rows from CSV data', () => {
|
|
476
|
-
const csvData = parseCSV(
|
|
477
|
-
'Sample,Condition,Tissue\nS1,Ctrl,Liver\nS2,Treat,Brain'
|
|
478
|
-
)
|
|
479
|
-
const columns = [
|
|
480
|
-
{ index: 0, name: 'Condition', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
|
|
481
|
-
{ index: 1, name: 'Tissue', uniqueValues: ['Liver', 'Brain'], cardinality: 2 },
|
|
482
|
-
]
|
|
483
|
-
const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
|
|
484
|
-
expect(result.metadata).toHaveLength(2)
|
|
485
|
-
expect(result.metadata[0]).toEqual({
|
|
486
|
-
sampleName: 'S1',
|
|
487
|
-
fields: { Condition: 'Ctrl', Tissue: 'Liver' },
|
|
488
|
-
group: 'Ctrl',
|
|
489
|
-
})
|
|
490
|
-
})
|
|
491
|
-
|
|
492
|
-
it('should still group correctly after user renames a field', () => {
|
|
493
|
-
const csvData = parseCSV(
|
|
494
|
-
'Sample,Condition,Tissue\nS1,Ctrl,Liver\nS2,Treat,Brain'
|
|
495
|
-
)
|
|
496
|
-
// Simulate user renaming "Condition" to "Treatment" in the Fields step
|
|
497
|
-
const columns = [
|
|
498
|
-
{ index: 0, name: 'Treatment', originalName: 'Condition', uniqueValues: ['Ctrl', 'Treat'], cardinality: 2 },
|
|
499
|
-
{ index: 1, name: 'Organ', originalName: 'Tissue', uniqueValues: ['Liver', 'Brain'], cardinality: 2 },
|
|
500
|
-
]
|
|
501
|
-
const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
|
|
502
|
-
expect(result.groups).toHaveLength(2)
|
|
503
|
-
expect(result.groups.find(g => g.name === 'Ctrl')?.samples).toEqual(['S1'])
|
|
504
|
-
expect(result.groups.find(g => g.name === 'Treat')?.samples).toEqual(['S2'])
|
|
505
|
-
|
|
506
|
-
// Metadata should use the display name as key
|
|
507
|
-
expect(result.metadata[0].fields['Treatment']).toBe('Ctrl')
|
|
508
|
-
expect(result.metadata[0].fields['Organ']).toBe('Liver')
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
it('should handle the real-world TSV format from the bug report', () => {
|
|
512
|
-
const text = [
|
|
513
|
-
'File Name\tTreatment Group',
|
|
514
|
-
'exp275_260401_YS_RNA_blank_001\tBlank',
|
|
515
|
-
'exp275_260401_YS_RNA_SplusA1_002\tS+A',
|
|
516
|
-
'exp275_260401_YS_RNA_A2_004\tA',
|
|
517
|
-
'exp275_260401_YS_RNA_NC_005\tDigControl',
|
|
518
|
-
'exp275_260401_YS_RNA_R2_007\tR',
|
|
519
|
-
'exp275_260401_YS_RNA_S2_009\tS',
|
|
520
|
-
'exp275_260401_YS_RNA_C1_010\tC',
|
|
521
|
-
'exp275_260401_YS_RNA_RplusA1_011\tR+A',
|
|
522
|
-
].join('\n')
|
|
523
|
-
const csvData = parseCSV(text)
|
|
524
|
-
|
|
525
|
-
expect(csvData.sampleColumn).toBe('File Name')
|
|
526
|
-
expect(csvData.columns).toEqual(['File Name', 'Treatment Group'])
|
|
527
|
-
expect(csvData.rows).toHaveLength(8)
|
|
528
|
-
|
|
529
|
-
const columns = [
|
|
530
|
-
{ index: 0, name: 'Treatment Group', uniqueValues: ['Blank', 'S+A', 'A', 'DigControl', 'R', 'S', 'C', 'R+A'], cardinality: 8 },
|
|
531
|
-
]
|
|
532
|
-
const result = computeGroupsFromCsv(csvData, columns, new Set([0]))
|
|
533
|
-
expect(result.groups).toHaveLength(8)
|
|
534
|
-
|
|
535
|
-
const blankGroup = result.groups.find(g => g.name === 'Blank')
|
|
536
|
-
expect(blankGroup?.samples).toEqual(['exp275_260401_YS_RNA_blank_001'])
|
|
537
|
-
|
|
538
|
-
const saGroup = result.groups.find(g => g.name === 'S+A')
|
|
539
|
-
expect(saGroup?.samples).toEqual(['exp275_260401_YS_RNA_SplusA1_002'])
|
|
540
|
-
})
|
|
541
|
-
})
|
|
542
|
-
|
|
543
|
-
describe('integration: mixed experimental + QC + test samples', () => {
|
|
544
|
-
// Simulates the real dataset pattern:
|
|
545
|
-
// 154 experimental (11 fields), ~15 QC (6-7 fields), 3 test (4 fields)
|
|
546
|
-
const experimental = [
|
|
547
|
-
'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33',
|
|
548
|
-
'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
|
|
549
|
-
'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35',
|
|
550
|
-
'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
|
|
551
|
-
'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37',
|
|
552
|
-
'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
|
|
553
|
-
'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39',
|
|
554
|
-
]
|
|
555
|
-
const qcSamples = [
|
|
556
|
-
'LT_13102025_EQC_Jurkat_1_2',
|
|
557
|
-
'LT_13102025_IQC_Pool_3',
|
|
558
|
-
]
|
|
559
|
-
const testSamples = [
|
|
560
|
-
'test_EQC_Jurkat_02',
|
|
561
|
-
]
|
|
562
|
-
const allLines = [...experimental, ...qcSamples, ...testSamples]
|
|
563
|
-
|
|
564
|
-
it('should detect dominantFieldCount=11 and flag QC/test as outliers', () => {
|
|
565
|
-
const analysis = analyzeDelimiter(allLines)
|
|
566
|
-
expect(analysis.delimiter).toBe('_')
|
|
567
|
-
expect(analysis.dominantFieldCount).toBe(11)
|
|
568
|
-
|
|
569
|
-
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
570
|
-
// QC samples (6 fields) and test samples (4 fields) are outliers
|
|
571
|
-
expect(outliers).toHaveLength(qcSamples.length + testSamples.length)
|
|
572
|
-
|
|
573
|
-
// Experimental samples should NOT be flagged
|
|
574
|
-
const outlierIndices = new Set(outliers.map(o => o.index))
|
|
575
|
-
for (let i = 0; i < experimental.length; i++) {
|
|
576
|
-
expect(outlierIndices.has(i)).toBe(false)
|
|
577
|
-
}
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
it('should auto-classify QC/test outliers with smart defaults', () => {
|
|
581
|
-
const analysis = analyzeDelimiter(allLines)
|
|
582
|
-
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
583
|
-
|
|
584
|
-
for (const outlier of outliers) {
|
|
585
|
-
outlier.action = classifyOutlierAction(outlier.sample, '_')
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// All QC and test samples should be classified as 'qc'
|
|
589
|
-
expect(outliers.every(o => o.action === 'qc')).toBe(true)
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
it('should produce 11 columns from conforming samples', () => {
|
|
593
|
-
const analysis = analyzeDelimiter(allLines)
|
|
594
|
-
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
595
|
-
const conforming = allLines.filter(
|
|
596
|
-
(_, i) => !outliers.some(o => o.index === i)
|
|
597
|
-
)
|
|
598
|
-
|
|
599
|
-
const conformingFieldCounts = conforming.map(s => s.split('_').length)
|
|
600
|
-
const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
|
|
601
|
-
|
|
602
|
-
const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
|
|
603
|
-
expect(columns).toHaveLength(11)
|
|
604
|
-
})
|
|
605
|
-
|
|
606
|
-
it('should auto-disable constant columns (cardinality 1)', () => {
|
|
607
|
-
const analysis = analyzeDelimiter(allLines)
|
|
608
|
-
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
609
|
-
const conforming = allLines.filter(
|
|
610
|
-
(_, i) => !outliers.some(o => o.index === i)
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
const conformingFieldCounts = conforming.map(s => s.split('_').length)
|
|
614
|
-
const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
|
|
615
|
-
|
|
616
|
-
const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
|
|
617
|
-
|
|
618
|
-
// Auto-disable: only enable columns with cardinality > 1
|
|
619
|
-
const enabled = new Set(
|
|
620
|
-
columns.filter(f => f.cardinality > 1).map(f => f.index)
|
|
621
|
-
)
|
|
622
|
-
|
|
623
|
-
// Constant columns (LT, 13102025, Glu, 091123) should be disabled
|
|
624
|
-
const constantCols = columns.filter(c => c.cardinality === 1)
|
|
625
|
-
for (const col of constantCols) {
|
|
626
|
-
expect(enabled.has(col.index)).toBe(false)
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
// Variable columns should be enabled
|
|
630
|
-
const variableCols = columns.filter(c => c.cardinality > 1)
|
|
631
|
-
for (const col of variableCols) {
|
|
632
|
-
expect(enabled.has(col.index)).toBe(true)
|
|
633
|
-
}
|
|
634
|
-
expect(variableCols.length).toBeGreaterThan(0)
|
|
635
|
-
})
|
|
636
|
-
})
|
|
637
|
-
|
|
638
68
|
describe('extractSamplesFromDesignData', () => {
|
|
639
69
|
it('should extract samples with conditions into ParsedCsvData', () => {
|
|
640
70
|
const data = {
|
|
@@ -648,12 +78,12 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
648
78
|
const result = extractSamplesFromDesignData(data)
|
|
649
79
|
expect(result).not.toBeNull()
|
|
650
80
|
expect(result!.sampleColumn).toBe('sample_name')
|
|
651
|
-
expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment'])
|
|
81
|
+
expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'sample_type'])
|
|
652
82
|
expect(result!.rows).toHaveLength(3)
|
|
653
|
-
expect(result!.rows[0]).toEqual({ sample_name: 'HeLa_DrugA_1', 'Cell Line': 'HeLa', Treatment: 'Drug A' })
|
|
83
|
+
expect(result!.rows[0]).toEqual({ sample_name: 'HeLa_DrugA_1', 'Cell Line': 'HeLa', Treatment: 'Drug A', sample_type: 'sample' })
|
|
654
84
|
})
|
|
655
85
|
|
|
656
|
-
it('should
|
|
86
|
+
it('should retain QC and blank samples and surface their type in sampleTypeHints', () => {
|
|
657
87
|
const data = {
|
|
658
88
|
samples: [
|
|
659
89
|
{ sample_name: 'Sample_1', sample_type: 'sample', conditions: { Treatment: 'Drug A' } },
|
|
@@ -664,8 +94,11 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
664
94
|
}
|
|
665
95
|
const result = extractSamplesFromDesignData(data)
|
|
666
96
|
expect(result).not.toBeNull()
|
|
667
|
-
|
|
668
|
-
expect(result!.rows
|
|
97
|
+
// All 4 samples are retained — QC/blank are no longer filtered here.
|
|
98
|
+
expect(result!.rows).toHaveLength(4)
|
|
99
|
+
expect(result!.rows.map(r => r.sample_name)).toEqual(['Sample_1', 'QC_Pool', 'Blank_1', 'Sample_2'])
|
|
100
|
+
// sampleTypeHints parallels the rows array.
|
|
101
|
+
expect(result!.sampleTypeHints).toEqual(['sample', 'qc', 'blank', 'sample'])
|
|
669
102
|
})
|
|
670
103
|
|
|
671
104
|
it('should return null when no samples array exists', () => {
|
|
@@ -677,14 +110,18 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
677
110
|
expect(extractSamplesFromDesignData({ samples: [] })).toBeNull()
|
|
678
111
|
})
|
|
679
112
|
|
|
680
|
-
it('should
|
|
113
|
+
it('should retain all-QC/blank batches and set sampleTypeHints accordingly', () => {
|
|
114
|
+
// QC_1 has conditions so allConditionKeys is non-empty; both samples are retained.
|
|
681
115
|
const data = {
|
|
682
116
|
samples: [
|
|
683
117
|
{ sample_name: 'QC_1', sample_type: 'qc', conditions: { Treatment: 'QC' } },
|
|
684
118
|
{ sample_name: 'Blank', sample_type: 'blank', conditions: {} },
|
|
685
119
|
],
|
|
686
120
|
}
|
|
687
|
-
|
|
121
|
+
const result = extractSamplesFromDesignData(data)
|
|
122
|
+
expect(result).not.toBeNull()
|
|
123
|
+
expect(result!.rows).toHaveLength(2)
|
|
124
|
+
expect(result!.sampleTypeHints).toEqual(['qc', 'blank'])
|
|
688
125
|
})
|
|
689
126
|
|
|
690
127
|
it('should return null when no samples have conditions', () => {
|
|
@@ -706,11 +143,11 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
706
143
|
}
|
|
707
144
|
const result = extractSamplesFromDesignData(data)
|
|
708
145
|
expect(result).not.toBeNull()
|
|
709
|
-
expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'Timepoint'])
|
|
146
|
+
expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'Timepoint', 'sample_type'])
|
|
710
147
|
// S1 has no Timepoint → empty string
|
|
711
|
-
expect(result!.rows[0]).toEqual({ sample_name: 'S1', 'Cell Line': 'HeLa', Treatment: 'Drug A', Timepoint: '' })
|
|
148
|
+
expect(result!.rows[0]).toEqual({ sample_name: 'S1', 'Cell Line': 'HeLa', Treatment: 'Drug A', Timepoint: '', sample_type: 'sample' })
|
|
712
149
|
// S2 has no Treatment → empty string
|
|
713
|
-
expect(result!.rows[1]).toEqual({ sample_name: 'S2', 'Cell Line': 'MCF7', Treatment: '', Timepoint: '24h' })
|
|
150
|
+
expect(result!.rows[1]).toEqual({ sample_name: 'S2', 'Cell Line': 'MCF7', Treatment: '', Timepoint: '24h', sample_type: 'sample' })
|
|
714
151
|
})
|
|
715
152
|
|
|
716
153
|
it('should preserve column insertion order from first occurrence', () => {
|
|
@@ -721,8 +158,8 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
721
158
|
],
|
|
722
159
|
}
|
|
723
160
|
const result = extractSamplesFromDesignData(data)
|
|
724
|
-
// Timepoint seen first in S1, then Treatment, then Cell Line from S2
|
|
725
|
-
expect(result!.columns).toEqual(['sample_name', 'Timepoint', 'Treatment', 'Cell Line'])
|
|
161
|
+
// Timepoint seen first in S1, then Treatment, then Cell Line from S2; sample_type appended last.
|
|
162
|
+
expect(result!.columns).toEqual(['sample_name', 'Timepoint', 'Treatment', 'Cell Line', 'sample_type'])
|
|
726
163
|
})
|
|
727
164
|
|
|
728
165
|
it('should handle samples without sample_type (defaults to "sample")', () => {
|
|
@@ -737,7 +174,7 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
737
174
|
expect(result!.rows).toHaveLength(2)
|
|
738
175
|
})
|
|
739
176
|
|
|
740
|
-
it('should
|
|
177
|
+
it('should lower-case sample_type in sampleTypeHints regardless of input casing', () => {
|
|
741
178
|
const data = {
|
|
742
179
|
samples: [
|
|
743
180
|
{ sample_name: 'S1', sample_type: 'SAMPLE', conditions: { Treatment: 'Drug' } },
|
|
@@ -747,8 +184,9 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
747
184
|
}
|
|
748
185
|
const result = extractSamplesFromDesignData(data)
|
|
749
186
|
expect(result).not.toBeNull()
|
|
750
|
-
|
|
751
|
-
expect(result!.rows
|
|
187
|
+
// All 3 samples retained; hints are lower-cased.
|
|
188
|
+
expect(result!.rows).toHaveLength(3)
|
|
189
|
+
expect(result!.sampleTypeHints).toEqual(['sample', 'qc', 'blank'])
|
|
752
190
|
})
|
|
753
191
|
|
|
754
192
|
it('should work with a realistic MS Designer design_data structure', () => {
|
|
@@ -770,30 +208,20 @@ describe('extractSamplesFromDesignData', () => {
|
|
|
770
208
|
}
|
|
771
209
|
const result = extractSamplesFromDesignData(data)
|
|
772
210
|
expect(result).not.toBeNull()
|
|
773
|
-
|
|
774
|
-
expect(result!.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const columns = result!.columns
|
|
778
|
-
.filter(c => c !== 'sample_name')
|
|
779
|
-
.map((col, i) => ({
|
|
780
|
-
index: i,
|
|
781
|
-
name: col,
|
|
782
|
-
originalName: col,
|
|
783
|
-
uniqueValues: [...new Set(result!.rows.map(r => r[col]))],
|
|
784
|
-
cardinality: new Set(result!.rows.map(r => r[col])).size,
|
|
785
|
-
}))
|
|
786
|
-
|
|
787
|
-
// Group by Treatment only
|
|
788
|
-
const groups = computeGroupsFromCsv(result!, columns, new Set([1])) // Treatment is index 1
|
|
789
|
-
expect(groups.groups).toHaveLength(2)
|
|
790
|
-
expect(groups.groups.find(g => g.name === 'Drug A')?.samples).toHaveLength(2)
|
|
791
|
-
expect(groups.groups.find(g => g.name === 'Control')?.samples).toHaveLength(1)
|
|
211
|
+
// All 5 samples are retained (including QC and blank).
|
|
212
|
+
expect(result!.rows).toHaveLength(5)
|
|
213
|
+
expect(result!.columns).toEqual(['sample_name', 'Cell Line', 'Treatment', 'Timepoint', 'sample_type'])
|
|
214
|
+
expect(result!.sampleTypeHints).toEqual(['sample', 'sample', 'sample', 'qc', 'blank'])
|
|
792
215
|
})
|
|
793
216
|
})
|
|
794
217
|
|
|
795
218
|
describe('useAutoGroup auto-disable degenerate columns', () => {
|
|
796
|
-
it('
|
|
219
|
+
it('collapses unique-per-row injection numbers via replicate pre-grouping', () => {
|
|
220
|
+
// The replicate pre-grouping pass strips the trailing 3-digit run-order
|
|
221
|
+
// tokens before tokenisation, so 5 inputs collapse to 2 base names
|
|
222
|
+
// (Ctrl_WT and Treat_WT). This supersedes the old "auto-disable
|
|
223
|
+
// unique-per-row column" mechanism — there IS no unique-per-row column
|
|
224
|
+
// for the schema to see, because pre-grouping already absorbed it.
|
|
797
225
|
const auto = useAutoGroup()
|
|
798
226
|
auto.rawText.value = [
|
|
799
227
|
'Ctrl_WT_001',
|
|
@@ -804,30 +232,35 @@ describe('useAutoGroup auto-disable degenerate columns', () => {
|
|
|
804
232
|
].join('\n')
|
|
805
233
|
auto.parseInput()
|
|
806
234
|
|
|
807
|
-
const lastCol = auto.fields.value.find(f => f.cardinality === 5)
|
|
808
|
-
expect(lastCol, 'expected a unique-per-row column').toBeTruthy()
|
|
809
|
-
expect(auto.enabledFields.value.has(lastCol!.index)).toBe(false)
|
|
810
|
-
// With the unique column auto-disabled, samples aggregate by Condition only
|
|
811
|
-
// (WT is constant, also auto-disabled): Ctrl=3 samples, Treat=2 samples.
|
|
812
235
|
expect(auto.groups.value).toHaveLength(2)
|
|
236
|
+
// Ctrl has 3 samples, Treat has 2 — replicates expanded back from the
|
|
237
|
+
// base names by expandGroupsWithReplicates in the result computed.
|
|
238
|
+
const ctrl = auto.groups.value.find(g => g.name === 'Ctrl')
|
|
239
|
+
const treat = auto.groups.value.find(g => g.name === 'Treat')
|
|
240
|
+
expect(ctrl?.samples).toHaveLength(3)
|
|
241
|
+
expect(treat?.samples).toHaveLength(2)
|
|
813
242
|
expect(auto.groups.value.every(g => g.samples.length > 1)).toBe(true)
|
|
814
243
|
})
|
|
815
244
|
|
|
816
|
-
it('
|
|
245
|
+
it('the schema never sees a degenerate unique-per-row column', () => {
|
|
246
|
+
// Same setup, asserting the contract from the other side: after the
|
|
247
|
+
// pre-grouping pass, the active schema's columns reflect the BASE
|
|
248
|
+
// tokens only. There's no column with cardinality === sample-count.
|
|
817
249
|
const auto = useAutoGroup()
|
|
818
|
-
auto.
|
|
819
|
-
|
|
820
|
-
'
|
|
821
|
-
'
|
|
822
|
-
'
|
|
823
|
-
'
|
|
824
|
-
|
|
825
|
-
)
|
|
250
|
+
auto.rawText.value = [
|
|
251
|
+
'Ctrl_WT_001',
|
|
252
|
+
'Ctrl_WT_002',
|
|
253
|
+
'Ctrl_WT_003',
|
|
254
|
+
'Treat_WT_004',
|
|
255
|
+
'Treat_WT_005',
|
|
256
|
+
].join('\n')
|
|
826
257
|
auto.parseInput()
|
|
827
258
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
259
|
+
// 5 originals → 2 base names → 2 schema columns: condition (Ctrl/Treat)
|
|
260
|
+
// and the constant WT. Neither has cardinality 5.
|
|
261
|
+
const schema = auto.activeSchema.value
|
|
262
|
+
expect(schema).toBeTruthy()
|
|
263
|
+
expect(schema!.columns.every(c => c.cardinality < 5)).toBe(true)
|
|
831
264
|
})
|
|
832
265
|
|
|
833
266
|
it('keeps useful columns (cardinality between 2 and N-1) enabled', () => {
|
|
@@ -850,11 +283,82 @@ describe('useAutoGroup auto-disable degenerate columns', () => {
|
|
|
850
283
|
|
|
851
284
|
it('exposes allSingletons=true when every group has exactly one sample', () => {
|
|
852
285
|
const auto = useAutoGroup()
|
|
286
|
+
// All-unique samples: each is classified as unknown, all 3 columns are factor by default
|
|
853
287
|
auto.rawText.value = ['A_x_1', 'B_y_2', 'C_z_3'].join('\n')
|
|
854
288
|
auto.parseInput()
|
|
855
|
-
|
|
856
|
-
|
|
289
|
+
// With all 3 factor columns in groupBy by default, each unique combination = singleton
|
|
857
290
|
expect(auto.groups.value.length).toBe(3)
|
|
858
291
|
expect(auto.allSingletons.value).toBe(true)
|
|
859
292
|
})
|
|
860
293
|
})
|
|
294
|
+
|
|
295
|
+
describe('useAutoGroup — class-first orchestration', () => {
|
|
296
|
+
it('detects classes after parseInput runs', async () => {
|
|
297
|
+
const ag = useAutoGroup()
|
|
298
|
+
ag.rawText.value = [
|
|
299
|
+
'exp281_260415_SAM_SAH_Tissues_Kidney_M1_001',
|
|
300
|
+
'exp281_260415_SAM_SAH_Tissues_Liver_M1_002',
|
|
301
|
+
'exp281_260415_SAM_SAH_Tissues_IQC_003',
|
|
302
|
+
].join('\n')
|
|
303
|
+
ag.parseInput()
|
|
304
|
+
const kinds = ag.classes.value.map(c => c.kind).sort()
|
|
305
|
+
expect(kinds).toContain('biological')
|
|
306
|
+
expect(kinds).toContain('iqc')
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
it('switches active schema when activeClassKey changes', () => {
|
|
310
|
+
const ag = useAutoGroup()
|
|
311
|
+
ag.rawText.value = [
|
|
312
|
+
'a_Kidney_M1_001',
|
|
313
|
+
'b_Liver_M1_002',
|
|
314
|
+
'c_IQC_003',
|
|
315
|
+
].join('\n')
|
|
316
|
+
ag.parseInput()
|
|
317
|
+
const firstKey = ag.classes.value[0].subKind
|
|
318
|
+
? `${ag.classes.value[0].kind}:${ag.classes.value[0].subKind}`
|
|
319
|
+
: ag.classes.value[0].kind
|
|
320
|
+
ag.activeClassKey.value = firstKey
|
|
321
|
+
expect(ag.activeSchema.value!.classKind).toBe(ag.classes.value[0].kind)
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it('setClassDisposition rewrites disposition and re-derives qcGroups', () => {
|
|
325
|
+
const ag = useAutoGroup()
|
|
326
|
+
ag.rawText.value = [
|
|
327
|
+
'exp_Plasma_M1_001',
|
|
328
|
+
'exp_Plasma_M2_002',
|
|
329
|
+
'exp_IQC_003',
|
|
330
|
+
].join('\n')
|
|
331
|
+
ag.parseInput()
|
|
332
|
+
ag.setClassDisposition('iqc', 'group')
|
|
333
|
+
expect(ag.classes.value.find(c => c.kind === 'iqc')!.disposition).toBe('group')
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it('legacy setOutlierAction is a no-op deprecation shim', () => {
|
|
337
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
338
|
+
const ag = useAutoGroup()
|
|
339
|
+
ag.rawText.value = ['a_b_c'].join('\n')
|
|
340
|
+
ag.parseInput()
|
|
341
|
+
ag.setOutlierAction(0, 'exclude')
|
|
342
|
+
expect(warn).toHaveBeenCalled()
|
|
343
|
+
warn.mockRestore()
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('preserves legacy public refs after redesign', () => {
|
|
347
|
+
const ag = useAutoGroup()
|
|
348
|
+
ag.rawText.value = 'a_b_c\nd_e_f'
|
|
349
|
+
ag.parseInput()
|
|
350
|
+
expect(ag.delimiter.value).toBeDefined()
|
|
351
|
+
expect(ag.fields.value).toBeDefined()
|
|
352
|
+
expect(ag.enabledFields.value).toBeDefined()
|
|
353
|
+
expect(ag.effectiveColumns.value).toBeDefined()
|
|
354
|
+
expect(ag.allSingletons.value).toBeDefined()
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('downloadTemplate composes blank template before classifier ran', () => {
|
|
358
|
+
const ag = useAutoGroup()
|
|
359
|
+
ag.rawText.value = 'a_b\nc_d'
|
|
360
|
+
ag.parseInput()
|
|
361
|
+
const { content } = ag.composeTemplateForTest({ mode: 'blank', format: 'csv' })
|
|
362
|
+
expect(content.split('\n')[0]).toContain('sample_name')
|
|
363
|
+
})
|
|
364
|
+
})
|