import { describe, expect, it } from 'vitest' import { nextTick } from 'vue' import { controlsToFormSchema, controlsToSectionFormSchema, controlsToSectionFormSchemas, controlsToSidebarPanels, controlsToSettingsSchema, controlsToTopBarSettingsConfig, controlsToViewIds, controlsToViewItems, controlValuesToComponentBindings, controlValuesToComponentBindingsById, controlValuesToComponentProps, defineControlComponentBindings, defineControlModel, defineDoseDesignControlModel, defineDoseCalculatorControlProps, defineControls, defineWellPlateControlProps, defineWellPlateDoseComponentBindings, defineWellPlateDoseControlProps, getDefaultControlView, getControlDefaults, isControlModelBinding, resolveControlModel, useControlSchema, useControlWorkspace, } from '../../composables/useControlSchema' import { useBioTemplateComponents, } from '../../composables/useBioTemplateComponents' import { useBioTemplateControls, } from '../../composables/useBioTemplateControls' import { createWellPlateScreenCollection, } from '../../templates' describe('useControlSchema', () => { const shorthandControls = defineControls({ threshold: 0.05, model: ['linear', 'logistic'], enabled: true, title: 'Drug screen', mode: [ { value: 'fast', label: 'Fast' }, { value: 'careful', label: 'Careful' }, ], }) const controls = defineControls({ threshold: { type: 'slider', label: 'Threshold', default: 0.05, min: 0, max: 1, props: { step: 0.01 }, section: 'parameters', sectionLabel: 'Parameters', sectionSubtitle: 'Analysis controls', view: 'analysis', sidebar: { label: 'Analysis Parameters', icon: 'settings', iconColor: '#6366f1', iconBg: '#e0e7ff', }, }, method: { label: 'Model', default: 'linear', options: ['linear', 'logistic'], required: true, section: 'parameters', view: 'analysis', }, saveResults: { label: 'Save results', default: true, section: 'output', sectionLabel: 'Output', view: 'analysis', sidebar: { defaultOpen: false, showToggle: true }, }, notes: { label: 'Notes', placeholder: 'Optional notes', section: 'output', view: 'analysis', sidebar: false, }, diagnostics: { label: 'Diagnostics', default: false, section: 'diagnostics', sectionLabel: 'Diagnostics', view: 'analysis', sidebar: { enabled: false }, }, }) it('builds defaults from compact controls', () => { expect(getControlDefaults(controls)).toEqual({ threshold: 0.05, method: 'linear', saveResults: true, notes: '', diagnostics: false, }) }) it('accepts primitive and option-array shorthand controls', () => { const schema = controlsToFormSchema(shorthandControls) expect(getControlDefaults(shorthandControls)).toEqual({ threshold: 0.05, model: 'linear', enabled: true, title: 'Drug screen', mode: 'fast', }) expect(schema.sections).toHaveLength(1) expect(schema.sections[0].fields.map(field => [field.name, field.type, field.defaultValue])).toEqual([ ['threshold', 'number', 0.05], ['model', 'select', 'linear'], ['enabled', 'toggle', true], ['title', 'text', 'Drug screen'], ['mode', 'select', 'fast'], ]) expect(schema.sections[0].fields[1].props?.options).toEqual([ { value: 'linear', label: 'Linear' }, { value: 'logistic', label: 'Logistic' }, ]) expect(schema.sections[0].fields[4].props?.options).toEqual([ { value: 'fast', label: 'Fast' }, { value: 'careful', label: 'Careful' }, ]) }) it('converts controls to a FormBuilder schema', () => { const schema = controlsToFormSchema(controls, { submitLabel: 'Run analysis', sections: { parameters: { columns: 2 }, }, }) expect(schema.submitLabel).toBe('Run analysis') expect(schema.sections.map(section => section.id)).toEqual(['parameters', 'output', 'diagnostics']) expect(schema.sections[0].title).toBe('Parameters') expect(schema.sections[0].description).toBeUndefined() expect(schema.sections[0].columns).toBe(2) expect(schema.sections[0].fields.map(field => field.name)).toEqual(['threshold', 'method']) const threshold = schema.sections[0].fields[0] expect(threshold.type).toBe('slider') expect(threshold.defaultValue).toBe(0.05) expect(threshold.props).toMatchObject({ min: 0, max: 1, step: 0.01 }) const method = schema.sections[0].fields[1] expect(method.type).toBe('select') expect(method.validation?.required).toBe(true) expect(method.props?.options).toEqual([ { label: 'Linear', value: 'linear' }, { label: 'Logistic', value: 'logistic' }, ]) }) it('converts controls to AppSidebar panels', () => { const panels = controlsToSidebarPanels(controls) expect(Object.keys(panels)).toEqual(['analysis']) expect(panels.analysis).toEqual([ { id: 'parameters', label: 'Analysis Parameters', subtitle: 'Analysis controls', icon: 'settings', iconColor: '#6366f1', iconBg: '#e0e7ff', defaultOpen: undefined, showToggle: undefined, }, { id: 'output', label: 'Output', subtitle: undefined, icon: undefined, iconColor: undefined, iconBg: undefined, defaultOpen: false, showToggle: true, }, ]) }) it('returns view helpers that can drive AppTopBar pillNav and AppSidebar together', () => { expect(controlsToViewIds(controls)).toEqual(['analysis']) expect(controlsToViewItems(controls)).toEqual([{ id: 'analysis', label: 'Analysis' }]) expect(getDefaultControlView(controls)).toBe('analysis') }) it('uses shared view metadata for AppTopBar pill navigation', () => { const runIcon = 'M8 5v14l11-7z' const options = { views: { analysis: { label: 'Run', icon: runIcon, disabled: false, }, }, } expect(controlsToViewItems(controls, options)).toEqual([ { id: 'analysis', label: 'Run', icon: runIcon, disabled: false, }, ]) }) it('flattens nested control models into workspace bindings', () => { const model = defineControlModel({ initialValues: { replicates: 4 }, topBarSettings: { title: 'Design settings', showAppearance: false }, views: { design: { label: 'Design', icon: 'flask', sections: { dose: { label: 'Dose design', description: 'Dose and layout controls', icon: 'settings', columns: 2, defaultOpen: false, controls: { concentrationUnit: ['uM', 'nM'], replicates: { type: 'number', default: 3, min: 1 }, }, }, display: { label: 'Display', sidebar: false, controls: { showHeatmap: true, }, }, }, }, results: { label: 'Results', section: 'plots', sectionLabel: 'Plots', controls: { chartScale: ['linear', 'log'], }, }, }, componentProps: { mode: 'concentrationUnit', targetWells: values => values.showHeatmap ? ['A1', 'A2'] : [], disabled: values => !values.showHeatmap, }, componentPropsById: { dose: { mode: 'concentrationUnit', targetWells: values => values.showHeatmap ? ['A1', 'A2'] : [], disabled: values => !values.showHeatmap, }, plate: { selectedWells: values => values.showHeatmap ? ['A1', 'A2'] : [], disabled: values => !values.showHeatmap, }, }, components: defineControlComponentBindings({ dose: { component: 'DoseCalculator', props: { mode: 'concentrationUnit', targetWells: values => values.showHeatmap ? ['A1', 'A2'] : [], disabled: values => !values.showHeatmap, }, }, plate: { component: 'WellPlate', props: { modelValue: values => values.showHeatmap ? ['A1', 'A2'] : [], disabled: values => !values.showHeatmap, }, }, }), }) expect(model.controls.concentrationUnit).toMatchObject({ default: 'uM', section: 'design-dose', view: 'design', }) expect(model.controls.showHeatmap).toMatchObject({ default: true, section: 'design-display', view: 'design', sidebar: false, }) expect(model.controlOptions.sections?.['design-dose']).toMatchObject({ id: 'design-dose', label: 'Dose design', description: 'Dose and layout controls', icon: 'settings', columns: 2, defaultOpen: false, }) expect(model.controlOptions.views?.design).toMatchObject({ label: 'Design', icon: 'flask', }) expect(model.componentProps).toBeDefined() expect(model.componentPropsById).toBeDefined() expect(model.componentBindings).toBeDefined() const workspace = useControlWorkspace(model.controls, model.controlOptions) expect(workspace.values.replicates).toBe(4) expect(workspace.topBar.value.pillNav.map(item => [item.id, item.label])).toEqual([ ['design', 'Design'], ['results', 'Results'], ]) expect(workspace.sidebarPanels.design.map(panel => panel.id)).toEqual(['design-dose']) expect(workspace.sidebarPanels.results.map(panel => panel.id)).toEqual(['plots']) expect(workspace.formSchema.sections.map(section => section.id)).toEqual([ 'design-dose', 'design-display', 'plots', ]) expect(workspace.topBarSettings.settingsConfig.title).toBe('Design settings') expect(workspace.topBarSettings.settingsConfig.showAppearance).toBe(false) expect(workspace.getComponentProps(model.componentProps)).toEqual({ mode: 'uM', targetWells: ['A1', 'A2'], disabled: false, }) expect(workspace.getComponentPropsById(model.componentPropsById)).toEqual({ dose: { mode: 'uM', targetWells: ['A1', 'A2'], disabled: false, }, plate: { selectedWells: ['A1', 'A2'], disabled: false, }, }) expect(workspace.getComponentBindings(model.componentBindings)).toEqual([ { id: 'dose', component: 'DoseCalculator', props: { mode: 'uM', targetWells: ['A1', 'A2'], disabled: false, }, }, { id: 'plate', component: 'WellPlate', props: { modelValue: ['A1', 'A2'], disabled: false, }, }, ]) expect(workspace.getComponentBindingsById(model.componentBindings).plate.props.modelValue).toEqual(['A1', 'A2']) }) it('normalizes raw and complete control models through one resolver', () => { const rawModel = { initialValues: { threshold: 0.1 }, controls: { threshold: { type: 'number' as const, label: 'Threshold', default: 0.05 }, }, } const binding = resolveControlModel(rawModel) expect(isControlModelBinding(rawModel)).toBe(false) expect(isControlModelBinding(binding)).toBe(true) expect(binding?.controls.threshold).toMatchObject({ label: 'Threshold', default: 0.05, }) expect(binding?.controlOptions.initialValues).toEqual({ threshold: 0.1 }) expect(resolveControlModel(binding)).toBe(binding) expect(resolveControlModel(undefined)).toBeUndefined() }) it('maps control values into named SDK component bindings', () => { const values = { threshold: 0.05, selectedWells: ['A1'], enabled: true, } const bindings = defineControlComponentBindings([ { component: 'ResultChart', props: { score: 'threshold', disabled: values => !values.enabled, }, }, { component: 'ResultChart', props: ['selectedWells'], }, { id: 'plate', component: 'WellPlate', props: { modelValue: 'selectedWells', }, }, ]) expect(controlValuesToComponentBindings(values, bindings)).toEqual([ { id: 'ResultChart', component: 'ResultChart', props: { score: 0.05, disabled: false, }, }, { id: 'ResultChart-2', component: 'ResultChart', props: { selectedWells: ['A1'], }, }, { id: 'plate', component: 'WellPlate', props: { modelValue: ['A1'], }, }, ]) expect(controlValuesToComponentBindingsById(values, bindings).plate.component).toBe('WellPlate') }) it('keeps direct view controls in separate generated sections', () => { const model = defineControlModel({ views: { run: { label: 'Run', controls: { threshold: 0.05 }, }, results: { label: 'Results', controls: { chartScale: ['linear', 'log'] }, }, }, }) const workspace = useControlWorkspace(model.controls, model.controlOptions) expect(model.controls.threshold).toMatchObject({ section: 'run-controls', view: 'run' }) expect(model.controls.chartScale).toMatchObject({ section: 'results-controls', view: 'results' }) expect(workspace.sectionSchemas['run-controls'].sections[0].fields.map(field => field.name)).toEqual([ 'threshold', ]) expect(workspace.sectionSchemas['results-controls'].sections[0].fields.map(field => field.name)).toEqual([ 'chartScale', ]) expect(workspace.sidebarPanels.run.map(panel => panel.id)).toEqual(['run-controls']) expect(workspace.sidebarPanels.results.map(panel => panel.id)).toEqual(['results-controls']) }) it('rejects duplicate control names in nested control models', () => { expect(() => defineControlModel({ sections: { first: { controls: { threshold: 0.05 } }, second: { controls: { threshold: 0.1 } }, }, }), ).toThrow('Duplicate control "threshold"') }) it('converts controls to a SettingsModal schema', () => { const condition = { field: 'method', eq: 'linear' } as const const schema = controlsToSettingsSchema(controls, { sections: { parameters: { label: 'Analysis Settings', description: 'Controls used by the analysis run', icon: '', columns: 2, condition, access: { visibleFor: 'admin', permissions: ['plugins.configure'] }, }, }, }) expect(schema.groups.map(group => group.id)).toEqual(['parameters', 'output', 'diagnostics']) expect(schema.groups[0]).toMatchObject({ id: 'parameters', label: 'Analysis Settings', description: 'Controls used by the analysis run', icon: '', columns: 2, condition, access: { visibleFor: 'admin', permissions: ['plugins.configure'] }, }) expect(schema.groups[0].fields.map(field => field.name)).toEqual(['threshold', 'method']) expect(schema.groups[1].label).toBe('Output') expect(schema.groups[1].description).toBeUndefined() }) it('should emit no flat access keys when converting controls to a settings group', () => { const schema = controlsToSettingsSchema(controls) expect(Object.keys(schema.groups[0])).not.toContain('requiresAdmin') }) it('should emit no flat access keys when converting a control to a form field', () => { const schema = controlsToSettingsSchema(controls) expect(Object.keys(schema.groups[0].fields[0])).not.toContain('requiresAdmin') }) it('converts controls to an AppTopBar settings config', () => { const options = { sections: { parameters: { label: 'Analysis Settings' }, }, } const config = controlsToTopBarSettingsConfig(controls, options, { title: 'Analysis controls', showAppearance: false, layout: 'vertical', }) expect(config.title).toBe('Analysis controls') expect(config.showAppearance).toBe(false) expect(config.layout).toBe('vertical') expect(config.controls).toBe(controls) expect(config.controlOptions).toBe(options) expect(config.schema).toBeUndefined() }) it('returns section-only schemas for sidebar slots', () => { const schema = controlsToSectionFormSchema(controls, 'output') expect(schema.sections).toHaveLength(1) expect(schema.sections[0].id).toBe('output') expect(schema.sections[0].title).toBe('') expect(schema.sections[0].fields.map(field => field.name)).toEqual(['saveResults', 'notes']) }) it('returns section-only schemas keyed by section for AppSidebar forms', () => { const schemas = controlsToSectionFormSchemas(controls) expect(Object.keys(schemas)).toEqual(['parameters', 'output', 'diagnostics']) expect(schemas.parameters.sections[0].title).toBe('') expect(schemas.parameters.sections[0].fields.map(field => field.name)).toEqual(['threshold', 'method']) expect(schemas.output.sections[0].fields.map(field => field.name)).toEqual(['saveResults', 'notes']) }) it('packages form schema, sidebar panels, defaults, and section helpers', () => { const toolkit = useControlSchema(controls) expect(toolkit.initialValues.saveResults).toBe(true) expect(toolkit.formSchema.sections[0].id).toBe('parameters') expect(toolkit.form.schema).toBe(toolkit.formSchema) expect(toolkit.settingsSchema.groups[0].id).toBe('parameters') expect(toolkit.settings.schema).toBe(toolkit.settingsSchema) expect(toolkit.topBarSettingsConfig.controls).toBe(toolkit.controls) expect(toolkit.topBarSettingsConfig.controlOptions).toEqual({}) expect(toolkit.topBarSettingsConfig.schema).toBeUndefined() expect(toolkit.topBarSettings).toEqual({ showSettings: true, settingsConfig: toolkit.topBarSettingsConfig, }) expect(toolkit.sidebarPanels.analysis).toHaveLength(2) expect(toolkit.viewIds).toEqual(['analysis']) expect(toolkit.viewItems).toEqual([{ id: 'analysis', label: 'Analysis' }]) expect(toolkit.defaultView).toBe('analysis') expect(toolkit.sidebar.panels).toBe(toolkit.sidebarPanels) expect(toolkit.sidebar.forms).toBe(toolkit.sectionSchemas) expect(toolkit.sidebar.viewIds).toBe(toolkit.viewIds) expect(toolkit.sidebar.viewItems).toBe(toolkit.viewItems) expect(toolkit.sidebar.defaultView).toBe('analysis') expect(toolkit.fields.map(field => field.name)).toEqual([ 'threshold', 'method', 'saveResults', 'notes', 'diagnostics', ]) expect(toolkit.sectionSchema('parameters').sections[0].fields).toHaveLength(2) expect(toolkit.sectionSchemas.parameters.sections[0].fields).toHaveLength(2) }) it('packages shared values and direct component bindings for control workspaces', () => { const workspace = useControlWorkspace(shorthandControls, { initialValues: { threshold: 0.2 }, topBarSettings: { title: 'Controls', showAppearance: false }, }) expect(workspace.schema.controls).toBe(workspace.controls) expect(workspace.values).toMatchObject({ threshold: 0.2, model: 'linear', enabled: true, title: 'Drug screen', mode: 'fast', }) expect(workspace.form.modelValue).toBe(workspace.values) expect(workspace.sidebar.modelValue).toBe(workspace.values) expect(workspace.sidebar.values).toBe(workspace.values) expect(workspace.sidebar.activeView).toBe('default') expect(workspace.topBar.value.pillNav).toEqual([{ id: 'default', label: 'Default' }]) expect(workspace.topBar.value.currentPillId).toBe('default') expect(workspace.pillNav.items).toEqual([{ id: 'default', label: 'Default' }]) expect(workspace.pillNav.currentItemId).toBe('default') expect(workspace.topBarSettings.settingsConfig.values).toBe(workspace.values) expect(workspace.topBarSettings.settingsConfig.title).toBe('Controls') expect(workspace.topBarSettings.settingsConfig.showAppearance).toBe(false) expect(workspace.topBarSettings.settingsConfig.controls).toBe(workspace.controls) expect(workspace.topBarSettings.settingsConfig.controlOptions).toEqual({}) expect(workspace.topBarSettings.settingsConfig.schema).toBeUndefined() expect(workspace.bindings.form).toBe(workspace.form) expect(workspace.bindings.sidebar).toBe(workspace.sidebar) expect(workspace.bindings.topBar).toBe(workspace.topBar) expect(workspace.bindings.topBarSettings).toBe(workspace.topBarSettings) expect(workspace.bindings.pillNav).toBe(workspace.pillNav) expect(workspace.bindings.componentBindings).toBe(workspace.componentBindings) expect(workspace.bindings.componentBindingsById).toBe(workspace.componentBindingsById) expect(workspace.bindings.componentProps).toBe(workspace.componentProps) expect(workspace.bindings.componentPropsById).toBe(workspace.componentPropsById) expect(workspace.bindings.topBar.value).toMatchObject({ pillNav: workspace.pillNav.items, currentPillId: 'default', showSettings: true, settingsConfig: workspace.topBarSettingsConfig, }) workspace.form['onUpdate:modelValue']({ threshold: 0.5, model: 'logistic' }) expect(workspace.values.threshold).toBe(0.5) expect(workspace.values.model).toBe('logistic') workspace.sidebar['onUpdate:values']({ enabled: false }) expect(workspace.values.enabled).toBe(false) workspace.sidebar['onUpdate:modelValue']({ enabled: true, title: 'Updated screen' }) expect(workspace.values.enabled).toBe(true) expect(workspace.values.title).toBe('Updated screen') workspace.topBarSettings.onSettingsValuesChange({ mode: 'careful' }) expect(workspace.values.mode).toBe('careful') workspace.topBar.value.onPillSelect({ id: 'missing', label: 'Missing' }) expect(workspace.activeView.value).toBe('default') workspace.resetValues() expect(workspace.values).toMatchObject({ threshold: 0.05, model: 'linear', enabled: true, title: 'Drug screen', mode: 'fast', }) }) it('maps workspace values into direct component props', () => { const workspace = useControlWorkspace(defineControls({ sampleNames: { default: ['Control', 'Treatment'], type: 'tags', }, unit: 'uM', enabled: true, })) expect(workspace.getComponentProps(['sampleNames', 'unit'])).toEqual({ sampleNames: ['Control', 'Treatment'], unit: 'uM', }) expect(workspace.getComponentProps({ samples: 'sampleNames', concentrationUnit: 'unit', disabled: values => !values.enabled, })).toEqual({ samples: ['Control', 'Treatment'], concentrationUnit: 'uM', disabled: false, }) expect(workspace.getComponentPropsById({ plate: { modelValue: 'sampleNames', disabled: values => !values.enabled, }, dose: { unit: 'unit', readonly: values => !values.enabled, }, })).toEqual({ plate: { modelValue: ['Control', 'Treatment'], disabled: false, }, dose: { unit: 'uM', readonly: false, }, }) expect(workspace.getComponentPropsById()).toEqual({}) workspace.setValues({ enabled: false, unit: 'nM' }) expect(controlValuesToComponentProps(workspace.values, { unit: 'unit', disabled: values => !values.enabled, })).toEqual({ unit: 'nM', disabled: true, }) }) it('provides built-in WellPlate and DoseCalculator component prop mappings', () => { const model = defineControlModel({ controls: { selectedWells: { type: 'tags', default: ['A1', 'A2'] }, plateFormat: [96, 384], doseMode: ['serial', 'dilution'], disabled: false, }, componentPropsById: defineWellPlateDoseControlProps(), }) const workspace = useControlWorkspace(model.controls, model.controlOptions) const componentPropsById = workspace.getComponentPropsById(model.componentPropsById) expect(componentPropsById.plate.modelValue).toEqual(['A1', 'A2']) expect(componentPropsById.plate.format).toBe(96) expect(componentPropsById.plate.wells).toBeUndefined() expect(componentPropsById.plate.disabled).toBe(false) expect(componentPropsById.plate.readonly).toBeUndefined() expect(componentPropsById.dose.mode).toBe('serial') expect(componentPropsById.dose.targetWells).toEqual(['A1', 'A2']) expect(componentPropsById.dose.disabled).toBe(false) const updatePlateSelection = componentPropsById.plate['onUpdate:modelValue'] expect(typeof updatePlateSelection).toBe('function') if (typeof updatePlateSelection !== 'function') throw new Error('Expected WellPlate update handler') updatePlateSelection(['B1', 'B2']) expect(workspace.values.selectedWells).toEqual(['B1', 'B2']) const syncedPropsById = workspace.getComponentPropsById(model.componentPropsById) expect(syncedPropsById.dose.targetWells).toEqual(['B1', 'B2']) const applyToWells = syncedPropsById.dose.onApplyToWells expect(typeof applyToWells).toBe('function') if (typeof applyToWells !== 'function') throw new Error('Expected DoseCalculator apply handler') applyToWells([ { wellId: 'B1', concentration: { value: 100, unit: 'µM' }, volume: { value: 50, unit: 'µL' }, }, { wellId: 'B2', concentration: { value: 50, unit: 'µM' }, }, ]) expect(workspace.values.wells).toMatchObject({ B1: { state: 'filled', value: 100, metadata: { concentration: { value: 100, unit: 'µM' }, volume: { value: 50, unit: 'µL' }, }, }, B2: { state: 'filled', value: 50, metadata: { concentration: { value: 50, unit: 'µM' }, }, }, }) expect(workspace.getComponentPropsById(model.componentPropsById).plate.wells).toBe(workspace.values.wells) }) it('accepts a complete control model binding directly', () => { const model = defineDoseDesignControlModel({ selectedWells: ['E1'], plateFormat: 384, }) const workspace = useControlWorkspace(model, { initialValues: { disabled: true, }, }) expect(workspace.values.selectedWells).toEqual(['E1']) expect(workspace.values.plateFormat).toBe(384) expect(workspace.values.doseMode).toBe('serial') expect(workspace.values.disabled).toBe(true) expect(workspace.sidebar.panels.design.map(panel => panel.id)).toEqual(['design-dose']) expect(workspace.componentProps.value).toEqual({}) expect(workspace.componentPropsById.value.plate.modelValue).toEqual(['E1']) expect(workspace.componentPropsById.value.plate.format).toBe(384) expect(workspace.componentPropsById.value.plate.disabled).toBe(true) expect(workspace.componentPropsById.value.dose.mode).toBe('serial') expect(workspace.componentPropsById.value.dose.targetWells).toEqual(['E1']) expect(workspace.componentPropsById.value.dose.disabled).toBe(true) expect(workspace.componentBindings.value.map(binding => [binding.id, binding.component])).toEqual([ ['plate', 'WellPlate'], ['dose', 'DoseCalculator'], ]) expect(workspace.componentBindingsById.value.plate.props.modelValue).toEqual(['E1']) expect(workspace.componentBindingsById.value.dose.props.disabled).toBe(true) const updatePlateSelection = workspace.componentPropsById.value.plate['onUpdate:modelValue'] expect(typeof updatePlateSelection).toBe('function') if (typeof updatePlateSelection !== 'function') throw new Error('Expected WellPlate update handler') updatePlateSelection(['F1', 'F2']) expect(workspace.values.selectedWells).toEqual(['F1', 'F2']) expect(workspace.componentPropsById.value.dose.targetWells).toEqual(['F1', 'F2']) }) it('allows biology component prop mapping field overrides', () => { const workspace = useControlWorkspace(defineControls({ wells: { type: 'tags', default: ['B1'] }, format: [384, 96], mode: ['dilution', 'serial'], locked: true, molecularWeight: 300.44, })) const plateProps = workspace.getComponentProps(defineWellPlateControlProps({ selectedWells: 'wells', format: 'format', wells: () => undefined, disabled: 'locked', showSampleTypeIndicator: () => true, })) expect(plateProps.modelValue).toEqual(['B1']) expect(plateProps.format).toBe(384) expect(plateProps.wells).toBeUndefined() expect(plateProps.disabled).toBe(true) expect(plateProps.readonly).toBeUndefined() expect(plateProps.showSampleTypeIndicator).toBe(true) const updatePlateSelection = plateProps['onUpdate:modelValue'] expect(typeof updatePlateSelection).toBe('function') if (typeof updatePlateSelection !== 'function') throw new Error('Expected WellPlate update handler') updatePlateSelection(['C1', 'C2']) expect(workspace.values.wells).toEqual(['C1', 'C2']) const doseProps = workspace.getComponentProps(defineDoseCalculatorControlProps({ mode: 'mode', targetWells: 'wells', disabled: 'locked', molecularWeight: 'molecularWeight', })) expect(doseProps.mode).toBe('dilution') expect(doseProps.targetWells).toEqual(['C1', 'C2']) expect(doseProps.disabled).toBe(true) expect(doseProps.molecularWeight).toBe(300.44) expect(doseProps.onApplyToWells).toBeUndefined() }) it('creates WellPlate and DoseCalculator component bindings for dose controls', () => { const workspace = useControlWorkspace(defineControls({ selectedWells: { type: 'tags', default: ['B1'] }, plateFormat: 384, doseMode: ['serial', 'dilution'], disabled: false, })) const bindings = workspace.getComponentBindings(defineWellPlateDoseComponentBindings()) expect(bindings.map(binding => [binding.id, binding.component])).toEqual([ ['plate', 'WellPlate'], ['dose', 'DoseCalculator'], ]) expect(bindings[0].props.modelValue).toEqual(['B1']) expect(bindings[0].props.format).toBe(384) expect(bindings[1].props.mode).toBe('serial') expect(bindings[1].props.targetWells).toEqual(['B1']) }) it('creates a complete dose design control model from one helper', () => { const model = defineDoseDesignControlModel({ selectedWells: ['C1', 'C2'], plateFormat: 384, includeMolecularWeight: true, molecularWeight: 300.44, }) const workspace = useControlWorkspace(model.controls, model.controlOptions) expect(model.controlOptions.views?.design).toMatchObject({ label: 'Design' }) expect(model.controlOptions.sections?.['design-dose']).toMatchObject({ label: 'Dose design', description: 'Well selection and dose calculator controls', }) expect(workspace.values.selectedWells).toEqual(['C1', 'C2']) expect(workspace.values.plateFormat).toBe(384) expect(workspace.values.doseMode).toBe('serial') expect(workspace.values.disabled).toBe(false) expect(workspace.values.molecularWeight).toBe(300.44) expect(workspace.sidebar.panels.design.map(panel => panel.id)).toEqual(['design-dose']) const componentPropsById = workspace.getComponentPropsById(model.componentPropsById) expect(componentPropsById.plate.modelValue).toEqual(['C1', 'C2']) expect(componentPropsById.plate.format).toBe(384) expect(componentPropsById.plate.wells).toBeUndefined() expect(componentPropsById.plate.disabled).toBe(false) expect(componentPropsById.plate.readonly).toBeUndefined() expect(componentPropsById.dose.mode).toBe('serial') expect(componentPropsById.dose.targetWells).toEqual(['C1', 'C2']) expect(componentPropsById.dose.disabled).toBe(false) expect(componentPropsById.dose.molecularWeight).toBe(300.44) expect(workspace.getComponentBindings(model.componentBindings).map(binding => [binding.id, binding.component])).toEqual([ ['plate', 'WellPlate'], ['dose', 'DoseCalculator'], ]) const updatePlateSelection = componentPropsById.plate['onUpdate:modelValue'] expect(typeof updatePlateSelection).toBe('function') if (typeof updatePlateSelection !== 'function') throw new Error('Expected WellPlate update handler') updatePlateSelection(['D1']) expect(workspace.values.selectedWells).toEqual(['D1']) expect(workspace.getComponentPropsById(model.componentPropsById).dose.targetWells).toEqual(['D1']) }) it('customizes the generated dose design control model ids and component mappings', () => { const model = defineDoseDesignControlModel({ viewId: 'setup', viewLabel: 'Setup', sectionId: 'plate', sectionLabel: 'Plate setup', componentProps: { plateId: 'wellPlate', doseId: 'doseCalculator', plate: { showLegend: () => true }, }, }) const workspace = useControlWorkspace(model.controls, model.controlOptions) expect(model.controlOptions.views?.setup).toMatchObject({ label: 'Setup' }) expect(model.controlOptions.sections?.['setup-plate']).toMatchObject({ label: 'Plate setup' }) const componentPropsById = workspace.getComponentPropsById(model.componentPropsById) expect(componentPropsById.wellPlate.modelValue).toEqual(['A1', 'A2']) expect(componentPropsById.wellPlate.showLegend).toBe(true) expect(componentPropsById.doseCalculator.mode).toBe('serial') expect(componentPropsById.doseCalculator.targetWells).toEqual(['A1', 'A2']) expect(workspace.getComponentBindings(model.componentBindings).map(binding => [binding.id, binding.component])).toEqual([ ['wellPlate', 'WellPlate'], ['doseCalculator', 'DoseCalculator'], ]) }) it('keeps AppTopBar pill navigation and AppSidebar active view on the same workspace state', async () => { const workspace = useControlWorkspace({ threshold: { default: 0.05, section: 'parameters', view: 'analysis', }, chartScale: { default: 'linear', section: 'display', view: 'results', }, }, { views: { analysis: { label: 'Run' }, results: { label: 'Results' }, }, }) expect(workspace.activeView.value).toBe('analysis') expect(workspace.sidebar.activeView).toBe('analysis') expect(workspace.topBar.value.currentPillId).toBe('analysis') expect(workspace.pillNav.currentItemId).toBe('analysis') expect(workspace.bindings.topBar.value.currentPillId).toBe('analysis') expect(workspace.topBar.value.pillNav.map(item => item.label)).toEqual(['Run', 'Results']) expect(workspace.pillNav.items.map(item => item.label)).toEqual(['Run', 'Results']) workspace.topBar.value.onPillSelect({ id: 'results', label: 'Results' }) expect(workspace.activeView.value).toBe('results') expect(workspace.sidebar.activeView).toBe('results') expect(workspace.topBar.value.currentPillId).toBe('results') expect(workspace.pillNav.currentItemId).toBe('results') expect(workspace.bindings.topBar.value.currentPillId).toBe('results') workspace.setActiveView('analysis') expect(workspace.activeView.value).toBe('analysis') expect(workspace.sidebar.activeView).toBe('analysis') expect(workspace.topBar.value.currentPillId).toBe('analysis') expect(workspace.pillNav.currentItemId).toBe('analysis') workspace.bindings.topBar.value.onPillSelect({ id: 'results', label: 'Results' }) expect(workspace.activeView.value).toBe('results') expect(workspace.bindings.topBar.value.currentPillId).toBe('results') workspace.pillNav.onSelect({ id: 'analysis', label: 'Run' }) expect(workspace.activeView.value).toBe('analysis') expect(workspace.bindings.topBar.value.currentPillId).toBe('analysis') workspace.pillNav.onSelect({ id: 'results', label: 'Results' }) expect(workspace.activeView.value).toBe('results') expect(workspace.sidebar.activeView).toBe('results') expect(workspace.topBar.value.currentPillId).toBe('results') expect(workspace.pillNav.currentItemId).toBe('results') workspace.activeView.value = 'analysis' await nextTick() expect(workspace.sidebar.activeView).toBe('analysis') expect(workspace.topBar.value.currentPillId).toBe('analysis') expect(workspace.pillNav.currentItemId).toBe('analysis') workspace.pillNav.currentItemId = 'results' await nextTick() expect(workspace.activeView.value).toBe('results') expect(workspace.sidebar.activeView).toBe('results') expect(workspace.topBar.value.currentPillId).toBe('results') }) it('builds controls and component bindings for biology templates', () => { const controlsToolkit = useBioTemplateControls('wellplate-screen') const collection = createWellPlateScreenCollection({ samples: ['Control', 'Treatment'], compounds: { 'Drug A': [10, 1] }, }) const componentsToolkit = useBioTemplateComponents(collection) expect(controlsToolkit.initialValues.plateFormat).toBe(96) expect(controlsToolkit.sidebarPanels.design.map(panel => panel.id)).toContain('dose') expect(componentsToolkit.bindings.map(binding => binding.component)).toContain('WellPlate') expect(componentsToolkit.imports[0].statement).toContain('DoseCalculator') expect(componentsToolkit.componentBindingsById['dose-response:DoseCalculator'].component).toBe('DoseCalculator') expect(componentsToolkit.componentBindingsById['dose-response:DoseCalculator'].props.mode).toBe('serial') expect(componentsToolkit.componentProps.map(binding => binding.component)).toContain('DoseCalculator') expect(componentsToolkit.componentPropsById['dose-response:DoseCalculator'].mode).toBe('serial') expect(componentsToolkit.componentPropsByComponent.WellPlate).toHaveLength(2) expect(componentsToolkit.getComponentProps('DoseCalculator')?.mode).toBe('serial') expect(componentsToolkit.snippets.map(snippet => snippet.component)).toContain('DoseCalculator') expect(componentsToolkit.usage?.template).toContain('