import { describe, expect, it } from 'vitest' import { assertTemplateEnvelope, bioTemplatePresetControlValuesToOptions, createBioTemplatePackCollection, createBioTemplatePresetCollection, createBioTemplatePresetCollectionFromControls, createAssayMatrixTemplate, createCalibrationCurveTemplate, createDoseResponseTemplate, createElisaAssayCollection, createFlowCytometryAssayCollection, createFlowCytometryPanelTemplate, createInstrumentRunTemplate, createLcmsBatchCollection, createPlateMapTemplate, createProtocolStepsTemplate, createQpcrExpressionCollection, createQpcrPlateTemplate, createReagentListTemplate, createSampleSheetTemplate, createSamplePrepTemplate, createTargetedMetabolomicsCollection, createDefaultBioTemplate, createTemplateCollection, createTimeCourseTemplate, createWellPlateScreenCollection, createWesternBlotAssayCollection, ensureTemplateEnvelope, ensureTemplateFromCollection, extractTemplateCollection, getBioTemplateComponentBindings, getBioTemplateComponentProps, getBioTemplateControlDefaults, getBioTemplateControlSchema, getBioTemplatePackInfo, getBioTemplatePresetInfo, getBioTemplateInfo, listBioTemplateCatalog, listBioTemplateComponentBindings, listBioTemplateControlSchemas, listBioTemplatePacks, listBioTemplatePresets, searchBioTemplatePacks, searchBioTemplateCatalog, searchBioTemplatePresets, toBioTemplateComponentBindings, toBioTemplateComponentBindingsById, toBioTemplateComponentImports, toBioTemplateComponentProps, toBioTemplateComponentPropsByComponent, toBioTemplateComponentPropsById, toBioTemplateComponentSnippets, toBioTemplateComponentUsage, toAssayMatrixColumns, toAssayMatrixDataFrame, toAssayMatrixRows, toAssayMatrixSampleOptions, toCalibrationCurveDataFrame, toCalibrationCurveRows, toDoseConditions, toDoseLayoutState, toFlowPanelDataFrame, toFlowPanelRows, toInstrumentRunDataFrame, toInstrumentRunRows, toInstrumentRunScheduleEvents, toInstrumentRunSteps, racksFromPlateState, toPlateMapEditorState, toProtocolDataFrame, toProtocolSteps, toQpcrDataFrame, toQpcrRows, toQpcrWellPlateWells, toReagentDataFrame, toReagentListItems, toSampleColumns, toSampleDataFrame, toSampleOptions, toSamplePrepDataFrame, toSamplePrepRows, toSampleRows, toTemplateDataFrame, toTimeCourseDataFrame, toTimeCourseRows, toTimeCourseSteps, toWellPlateWells, TEMPLATE_COLLECTION_KEY, } from '../../templates' import bioTemplateCatalogContract from '../../../../bio-template-catalog.contract.json' import bioTemplatePacksContract from '../../../../bio-template-packs.contract.json' import bioTemplatePresetsContract from '../../../../bio-template-presets.contract.json' import type { AssayMatrixTemplate, DoseResponseTemplate, FlowCytometryPanelTemplate, InstrumentRunTemplate, PlateMapTemplate, ProtocolStepsTemplate, QpcrPlateTemplate, ReagentListTemplate, SampleSheetTemplate, } from '../../templates' import doseResponseFixture from '../fixtures/templates/dose-response.json' import plateMapFixture from '../fixtures/templates/plate-map.json' import qpcrPlateFixture from '../fixtures/templates/qpcr-plate.json' import sampleSheetFixture from '../fixtures/templates/sample-sheet.json' describe('bio data templates', () => { it('exposes a discoverable template catalog', () => { const catalog = listBioTemplateCatalog() expect(catalog.map(entry => entry.name)).toEqual([ 'plate-map', 'sample-sheet', 'sample-prep', 'dose-response', 'calibration-curve', 'time-course', 'protocol-steps', 'assay-matrix', 'reagent-list', 'flow-cytometry-panel', 'instrument-run', 'qpcr-plate', ]) expect(getBioTemplateInfo('wellplate')?.template_id).toBe('plate-map') expect(getBioTemplateInfo('sample preparation')?.template_id).toBe('sample-prep') expect(getBioTemplateInfo('dose design')?.template_id).toBe('dose-response') expect(getBioTemplateInfo('dose-design')?.template_id).toBe('dose-response') expect(getBioTemplateInfo('dose calculator')?.template_id).toBe('dose-response') expect(getBioTemplateInfo('dose_calculator')?.template_id).toBe('dose-response') expect(getBioTemplateInfo('protocol')?.template_id).toBe('protocol-steps') expect(getBioTemplateInfo('standard curve')?.template_id).toBe('calibration-curve') expect(getBioTemplateInfo('omics matrix')?.template_id).toBe('assay-matrix') expect(getBioTemplateInfo('antibodies')?.template_id).toBe('reagent-list') expect(getBioTemplateInfo('facs panel')?.template_id).toBe('flow-cytometry-panel') expect(getBioTemplateInfo('run queue')?.template_id).toBe('instrument-run') expect(getBioTemplateInfo('gene expression')?.template_id).toBe('qpcr-plate') expect(searchBioTemplateCatalog('DoseCalculator')[0].name).toBe('dose-response') }) it('matches the shared Python/frontend catalog contract', () => { expect(listBioTemplateCatalog()).toEqual(bioTemplateCatalogContract) }) it('exposes discoverable template collection packs', () => { const packs = listBioTemplatePacks() expect(packs.map(entry => entry.name)).toEqual([ 'cell-culture-screen', 'omics-assay', 'longitudinal-study', 'molecular-assay', ]) expect(getBioTemplatePackInfo('cell culture')?.templates).toEqual([ 'sample-sheet', 'sample-prep', 'plate-map', 'reagent-list', 'protocol-steps', 'dose-response', 'flow-cytometry-panel', ]) expect(getBioTemplatePackInfo('metabolomics')?.name).toBe('omics-assay') expect(getBioTemplatePackInfo('metabolism')?.name).toBe('omics-assay') expect(getBioTemplatePackInfo('metabolite profiling')?.name).toBe('omics-assay') expect(getBioTemplatePackInfo('metabolomics')?.components).toContain('ExperimentTimeline') expect(getBioTemplatePackInfo('longitudinal')?.add_command).toBe('mint add data-template-pack longitudinal-study --page') expect(getBioTemplatePackInfo('gene expression')?.templates.at(-1)).toBe('qpcr-plate') expect(getBioTemplatePackInfo('gene-expression')?.templates.at(-1)).toBe('qpcr-plate') expect(searchBioTemplatePacks('WellPlate').map(entry => entry.name)).toEqual([ 'cell-culture-screen', 'omics-assay', 'molecular-assay', ]) }) it('matches the shared Python/frontend pack contract', () => { expect(listBioTemplatePacks()).toEqual(bioTemplatePacksContract) }) it('creates default template collections from curated packs', () => { const plate = createDefaultBioTemplate('wellplate') as PlateMapTemplate const pack = createBioTemplatePackCollection('cell culture') const templates = extractTemplateCollection(pack) expect(plate.template_id).toBe('plate-map') expect(pack.metadata?.pack).toBe('cell-culture-screen') expect(Object.keys(templates)).toEqual([ 'sample-sheet', 'sample-prep', 'plate-map', 'reagent-list', 'protocol-steps', 'dose-response', 'flow-cytometry-panel', ]) expect(toPlateMapEditorState(templates['plate-map'] as PlateMapTemplate).samples).toHaveLength(2) expect(toDoseConditions(templates['dose-response'] as DoseResponseTemplate)[0].label).toBe('Compound A') expect(getBioTemplateControlSchema('cell-culture-screen')?.sampleSheetSampleCount.default).toBe(2) expect(getBioTemplateComponentBindings('cell-culture-screen').map(binding => binding.component)).toContain('DoseCalculator') expect(() => createBioTemplatePackCollection('missing-pack')).toThrow("Unknown template pack 'missing-pack'.") }) it('exposes discoverable template collection presets', () => { const presets = listBioTemplatePresets() expect(presets.map(entry => entry.name)).toEqual([ 'wellplate-screen', 'qpcr-expression', 'lcms-batch', 'targeted-metabolomics', 'elisa-assay', 'flow-cytometry-assay', 'western-blot-assay', ]) expect(getBioTemplatePresetInfo('wellplate')?.name).toBe('wellplate-screen') expect(getBioTemplatePresetInfo('dose planner')?.name).toBe('wellplate-screen') expect(getBioTemplatePresetInfo('dose-planner')?.name).toBe('wellplate-screen') expect(getBioTemplatePresetInfo('gene expression')?.templates).toEqual([ 'sample-sheet', 'qpcr-plate', ]) expect(searchBioTemplatePresets('run queue')[0].name).toBe('lcms-batch') expect(getBioTemplatePresetInfo('metabolomics')?.name).toBe('lcms-batch') expect(getBioTemplatePresetInfo('metabolism')?.name).toBe('lcms-batch') expect(getBioTemplatePresetInfo('metabolite profiling')?.name).toBe('lcms-batch') expect(getBioTemplatePresetInfo('targeted metabolomics')?.name).toBe('targeted-metabolomics') expect(searchBioTemplatePresets('internal standards')[0].name).toBe('targeted-metabolomics') expect(getBioTemplatePresetInfo('immunoassay')?.templates).toEqual([ 'plate-map', 'sample-sheet', 'calibration-curve', 'assay-matrix', ]) expect(getBioTemplatePresetInfo('facs')?.templates).toEqual([ 'sample-sheet', 'flow-cytometry-panel', 'assay-matrix', ]) expect(getBioTemplatePresetInfo('immunoblot')?.templates).toEqual([ 'sample-sheet', 'reagent-list', 'protocol-steps', 'assay-matrix', ]) }) it('matches the shared Python/frontend preset contract', () => { expect(listBioTemplatePresets()).toEqual(bioTemplatePresetsContract) }) it('should not recommend AutoGroupModal when it is scheduled for removal', () => { const components = listBioTemplateCatalog().flatMap(entry => entry.components) expect(components).not.toContain('AutoGroupModal') }) it('prefers generic dataframe adapters in catalog entries', () => { const dataframeTemplates = listBioTemplateCatalog().filter(entry => entry.components.includes('DataFrame') ) expect(dataframeTemplates.map(entry => entry.name)).toEqual([ 'sample-sheet', 'sample-prep', 'calibration-curve', 'time-course', 'protocol-steps', 'assay-matrix', 'reagent-list', 'flow-cytometry-panel', 'instrument-run', 'qpcr-plate', ]) for (const entry of dataframeTemplates) { expect(entry.frontend_adapters[0]).toBe('toTemplateDataFrame') expect(entry.frontend_import).toContain('toTemplateDataFrame') } }) it('exposes control schemas for built-in templates and presets', () => { const schemas = listBioTemplateControlSchemas() const plateSchema = getBioTemplateControlSchema('wellplate') const presetSchema = getBioTemplateControlSchema('wellplate-screen') const defaults = getBioTemplateControlDefaults('lcms-batch') const targetedDefaults = getBioTemplateControlDefaults('targeted-metabolomics') const elisaDefaults = getBioTemplateControlDefaults('elisa-assay') const flowDefaults = getBioTemplateControlDefaults('flow-cytometry-assay') const westernDefaults = getBioTemplateControlDefaults('western-blot-assay') expect(Object.keys(schemas)).toContain('plate-map') expect(plateSchema?.plateFormat.default).toBe(96) expect(presetSchema?.replicates.default).toBe(3) expect(defaults.instrument).toBe('LC-MS') expect(defaults.featureNames).toEqual(['Glucose', 'Lactate']) expect(targetedDefaults.metaboliteNames).toEqual(['Glucose', 'Lactate', 'Pyruvate']) expect(targetedDefaults.internalStandards).toEqual(['Stable isotope internal standard mix']) expect(targetedDefaults.method).toBe('Targeted metabolomics') expect(elisaDefaults.analyte).toBe('Analyte') expect(elisaDefaults.standardConcentrations).toEqual(['1000', '100', '10', '1']) expect(flowDefaults.markers).toEqual(['CD3', 'CD4', 'CD8']) expect(flowDefaults.includeDefaultControls).toBe(true) expect(westernDefaults.targets).toEqual(['Target protein']) expect(westernDefaults.loadingControl).toBe('ACTB') }) it('exposes component bindings for template-driven frontend rendering', () => { const bindings = listBioTemplateComponentBindings() const wellplateBindings = getBioTemplateComponentBindings('wellplate-screen') const qpcrBindings = getBioTemplateComponentBindings('gene expression') const elisaBindings = getBioTemplateComponentBindings('elisa-assay') const flowBindings = getBioTemplateComponentBindings('flow-cytometry-assay') const lcmsBindings = getBioTemplateComponentBindings('lcms-batch') const targetedBindings = getBioTemplateComponentBindings('targeted-metabolomics') const westernBindings = getBioTemplateComponentBindings('western-blot-assay') const wellplateImports = toBioTemplateComponentImports('wellplate-screen') expect(bindings['plate-map'].map(binding => binding.component)).toEqual(['PlateEditor', 'WellPlate']) expect(wellplateBindings.map(binding => binding.component)).toContain('DoseCalculator') expect(wellplateBindings.map(binding => binding.component)).toContain('WellPlate') expect(qpcrBindings.map(binding => binding.component)).toContain('WellPlate') expect(qpcrBindings.map(binding => binding.component)).toContain('DataFrame') expect(elisaBindings.map(binding => binding.component)).toContain('PlateEditor') expect(elisaBindings.map(binding => binding.component)).toContain('DataFrame') expect(lcmsBindings.map(binding => binding.component)).toContain('ScheduleCalendar') expect(lcmsBindings.map(binding => binding.component)).toContain('ExperimentTimeline') expect(targetedBindings.map(binding => binding.component)).toContain('ReagentList') expect(targetedBindings.map(binding => binding.component)).toContain('DataFrame') expect(targetedBindings.map(binding => binding.component)).toContain('ScheduleCalendar') expect(flowBindings.map(binding => binding.template_id)).toEqual([ 'sample-sheet', 'sample-sheet', 'flow-cytometry-panel', 'assay-matrix', 'assay-matrix', ]) expect(westernBindings.map(binding => binding.component)).toContain('ReagentList') expect(westernBindings.map(binding => binding.component)).toContain('ExperimentTimeline') expect(wellplateImports).toEqual([ { importPath: '@morscherlab/mint-sdk', components: ['PlateEditor', 'WellPlate', 'DataFrame', 'SampleSelector', 'DoseCalculator'], statement: "import { PlateEditor, WellPlate, DataFrame, SampleSelector, DoseCalculator } from '@morscherlab/mint-sdk'", }, ]) }) it('creates plate-map envelopes and adapts them to plate editor state', () => { const template = createPlateMapTemplate({ name: 'Drug screen plate', samples: ['DMSO', 'Drug A'], }) const state = toPlateMapEditorState(template) expect(template.template_id).toBe('plate-map') expect(state.activePlateId).toBe('plate-1') expect(state.plates[0].format).toBe(96) expect(state.samples[1].id).toBe('drug-a') }) it('should convert plate-map state into PlateEditor racks', () => { const state = toPlateMapEditorState(createPlateMapTemplate({ samples: ['DMSO'] })) expect(racksFromPlateState(state)[0]).toMatchObject({ id: 'plate-1', format: 96, slot: 'R' }) }) it('loads the shared plate-map fixture', () => { const state = toPlateMapEditorState(plateMapFixture as PlateMapTemplate) const wells = toWellPlateWells(plateMapFixture as PlateMapTemplate) const componentProps = toBioTemplateComponentProps(plateMapFixture as PlateMapTemplate) const componentBindings = toBioTemplateComponentBindings(plateMapFixture as PlateMapTemplate) const componentPropsById = toBioTemplateComponentPropsById(plateMapFixture as PlateMapTemplate) const componentBindingsById = toBioTemplateComponentBindingsById(plateMapFixture as PlateMapTemplate) const wellPlate = componentProps.find(binding => binding.component === 'WellPlate') const wellPlateBinding = componentBindings.find(binding => binding.component === 'WellPlate') expect(state.plates[0].name).toBe('Drug screen plate') expect(wells.A2.sampleType).toBe('drug-a') expect(wellPlate?.propsObject.format).toBe(96) expect((wellPlate?.propsObject.wells as Record).A2.sampleType).toBe('drug-a') expect(wellPlateBinding?.props.format).toBe(96) expect(wellPlateBinding?.propNames).toContain('wells') expect(componentPropsById['plate-map:WellPlate']).toEqual(wellPlate?.propsObject) expect(componentBindingsById['plate-map:WellPlate'].component).toBe('WellPlate') expect(componentBindingsById['plate-map:WellPlate'].props).toEqual(wellPlate?.propsObject) expect((componentPropsById['plate-map:WellPlate'].wells as Record).A2.sampleType).toBe('drug-a') }) it('returns component props keyed by stable template component id', () => { const collection = createWellPlateScreenCollection({ samples: ['Control', 'Treatment'], compounds: { 'Drug A': [10, 1] }, }) const propsById = toBioTemplateComponentPropsById(collection) const bindingsById = toBioTemplateComponentBindingsById(collection) const propsByComponent = toBioTemplateComponentPropsByComponent(collection) const doseCalculatorProps = getBioTemplateComponentProps(collection, 'DoseCalculator') const doseWellPlateProps = getBioTemplateComponentProps(collection, 'WellPlate', { templateId: 'dose-response', }) expect(Object.keys(propsById)).toEqual([ 'plate-map:PlateEditor', 'plate-map:WellPlate', 'sample-sheet:DataFrame', 'sample-sheet:SampleSelector', 'dose-response:DoseCalculator', 'dose-response:WellPlate', ]) expect(propsById['dose-response:DoseCalculator'].mode).toBe('serial') expect(bindingsById['dose-response:DoseCalculator'].props.mode).toBe('serial') expect(bindingsById['dose-response:DoseCalculator'].propNames).toContain('mode') expect(propsById['plate-map:WellPlate'].wells).toBeDefined() expect(propsById['dose-response:WellPlate'].wells).toBeDefined() expect(propsByComponent.WellPlate).toHaveLength(2) expect(propsByComponent.DoseCalculator[0].mode).toBe('serial') expect(doseCalculatorProps?.mode).toBe('serial') expect(doseWellPlateProps?.wells).toBeDefined() expect(getBioTemplateComponentProps(collection, 'MissingWidget')).toBeUndefined() }) it('creates ELISA assay preset collections with plate, standards, and readout matrix', () => { const collection = createElisaAssayCollection({ samples: ['Control', 'Treatment'], analyte: 'IL-6', standardConcentrations: [1000, 100, 10, 1], unit: 'pg/mL', }) const templates = extractTemplateCollection(collection) const componentPropsById = toBioTemplateComponentPropsById(collection) expect(Object.keys(templates)).toEqual([ 'plate-map', 'sample-sheet', 'calibration-curve', 'assay-matrix', ]) expect(collection.metadata?.preset).toBe('elisa-assay') expect(templates['calibration-curve'].data).toMatchObject({ analyte: 'IL-6', model: 'four-parameter-logistic', xUnit: 'pg/mL', responseUnit: 'OD450', }) expect(templates['assay-matrix'].data).toMatchObject({ features: [{ id: 'il-6', name: 'IL-6', type: 'protein', unit: 'OD450', metadata: {} }], }) expect(componentPropsById['plate-map:WellPlate'].wells).toBeDefined() expect(componentPropsById['calibration-curve:DataFrame'].data).toBeDefined() expect(componentPropsById['assay-matrix:DataFrame'].columns).toBeDefined() expect(componentPropsById['assay-matrix:SampleSelector'].samples).toEqual(['Control', 'Treatment']) }) it('generates Vue snippets for concrete template component props', () => { const snippets = toBioTemplateComponentSnippets(plateMapFixture as PlateMapTemplate, { propsVariable: 'bindings', }) expect(snippets.map(snippet => snippet.component)).toEqual(['PlateEditor', 'WellPlate']) expect(snippets.map(snippet => snippet.propsName)).toEqual([ 'plateMapPlateEditorProps', 'plateMapWellPlateProps', ]) expect(snippets[0].propsExpression).toBe('bindings[0].propsObject') expect(snippets[0].template).toBe('') expect(snippets[1].template).toBe('') }) it('generates a Vue usage bundle for concrete template component props', () => { const usage = toBioTemplateComponentUsage(plateMapFixture as PlateMapTemplate, { propsVariable: 'bindings', targetExpression: 'plateMap', }) expect(usage.imports[0].statement).toBe( "import { PlateEditor, WellPlate } from '@morscherlab/mint-sdk'" ) expect(usage.componentProps).toHaveLength(2) expect(usage.propsDeclarations).toEqual([ 'const plateMapPlateEditorProps = bindings[0].propsObject', 'const plateMapWellPlateProps = bindings[1].propsObject', ]) expect(usage.scriptSetup).toBe([ 'const bindings = toBioTemplateComponentProps(plateMap)', 'const plateMapPlateEditorProps = bindings[0].propsObject', 'const plateMapWellPlateProps = bindings[1].propsObject', ].join('\n')) expect(usage.template).toBe([ '', '', ].join('\n')) }) it('generates a complete Vue SFC for concrete template component props', () => { const usage = toBioTemplateComponentUsage(plateMapFixture as PlateMapTemplate) expect(usage.scriptSetup).toContain( 'const props = defineProps<{ templateCollection: BioTemplateEnvelope | TemplateCollectionEnvelope }>()' ) expect(usage.sfc).toContain("import { PlateEditor, WellPlate } from '@morscherlab/mint-sdk'") expect(usage.sfc).toContain("import { toBioTemplateComponentProps } from '@morscherlab/mint-sdk/templates'") expect(usage.sfc).toContain('defineProps<{ templateCollection: BioTemplateEnvelope | TemplateCollectionEnvelope }>()') expect(usage.sfc).toContain('') expect(usage.sfc).toContain('') expect(usage.sfc).toMatch(/^