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('