import { mount } from '@vue/test-utils' import { defineComponent, nextTick } from 'vue' import { describe, expect, it } from 'vitest' import ComponentBindingRenderer from '../../components/ComponentBindingRenderer.vue' const WellPlateStub = defineComponent({ name: 'WellPlate', props: { modelValue: { type: Array, default: () => [] }, readonly: { type: Boolean, default: false }, size: { type: String, default: undefined }, }, template: '
', }) const DoseCalculatorStub = defineComponent({ name: 'DoseCalculator', props: { mode: { type: String, default: undefined }, targetWells: { type: Array, default: () => [] }, }, template: '', }) const DataFrameStub = defineComponent({ name: 'DataFrame', props: { data: { type: Array, default: () => [] }, columns: { type: Array, default: () => [] }, size: { type: String, default: undefined }, maxHeight: { type: String, default: undefined }, }, template: '', }) const PlateEditorStub = defineComponent({ name: 'PlateEditor', props: { modelValue: { type: Array, default: () => [] }, readonly: { type: Boolean, default: false }, size: { type: String, default: undefined }, }, emits: ['assign'], template: '', }) const ExperimentTimelineStub = defineComponent({ name: 'ExperimentTimeline', props: { modelValue: { type: Array, default: () => [] }, editable: { type: Boolean, default: true }, size: { type: String, default: undefined }, }, template: '', }) const ScheduleCalendarStub = defineComponent({ name: 'ScheduleCalendar', props: { modelValue: { type: [String, Date], default: undefined }, events: { type: Array, default: () => [] }, readonly: { type: Boolean, default: false }, showNavigation: { type: Boolean, default: true }, showViewToggle: { type: Boolean, default: true }, view: { type: String, default: undefined }, }, template: '', }) const SampleSelectorStub = defineComponent({ name: 'SampleSelector', props: { samples: { type: Array, default: () => [] }, modelValue: { type: Array, default: () => [] }, enableGrouping: { type: Boolean, default: true }, enableSmartGroup: { type: Boolean, default: true }, }, template: '', }) const globalOptions = { stubs: { DataFrame: DataFrameStub, DoseCalculator: DoseCalculatorStub, ExperimentTimeline: ExperimentTimelineStub, PlateEditor: PlateEditorStub, SampleSelector: SampleSelectorStub, ScheduleCalendar: ScheduleCalendarStub, WellPlate: WellPlateStub, }, } describe('ComponentBindingRenderer', () => { it('renders known SDK component bindings and skips unsupported names', () => { const wrapper = mount(ComponentBindingRenderer, { props: { bindings: [ { id: 'plate', component: 'WellPlate', props: { modelValue: ['A1'] }, description: 'Selected wells', }, { id: 'dose', component: 'DoseCalculator', props: { mode: 'serial', targetWells: ['A1'] }, }, { id: 'custom', component: 'CustomChart', props: { score: 0.5 }, }, ], }, global: globalOptions, }) expect(wrapper.find('[data-component-binding-id="plate"]').exists()).toBe(true) expect(wrapper.find('[data-component-binding-id="dose"]').exists()).toBe(true) expect(wrapper.find('[data-component-binding-id="custom"]').exists()).toBe(false) expect(wrapper.findComponent(WellPlateStub).props()).toMatchObject({ modelValue: ['A1'], readonly: true, size: 'fill', }) expect(wrapper.findComponent(DoseCalculatorStub).props()).toMatchObject({ mode: 'serial', targetWells: ['A1'], }) expect(wrapper.text()).toContain('Selected wells') }) it('accepts a single binding and preview-safe dense props', () => { const wrapper = mount(ComponentBindingRenderer, { props: { binding: { id: 'plate-map', component: 'PlateEditor', propsObject: { modelValue: [] }, }, dense: true, }, global: globalOptions, }) expect(wrapper.findComponent(PlateEditorStub).props()).toMatchObject({ readonly: true, size: 'md', }) }) it('filters bindings and renders a compact empty state', () => { const wrapper = mount(ComponentBindingRenderer, { props: { bindings: [ { id: 'table', component: 'DataFrame', props: { data: [{ id: 1 }], columns: [{ key: 'id', label: 'ID' }] }, }, ], include: ['WellPlate'], emptyText: 'No components.', }, global: globalOptions, }) expect(wrapper.find('[data-component-binding-id="table"]').exists()).toBe(false) expect(wrapper.text()).toContain('No components.') }) it('normalizes DataFrame sizing in dense mode', () => { const wrapper = mount(ComponentBindingRenderer, { props: { bindings: [ { id: 'table', component: 'DataFrame', props: { data: [{ id: 1 }], columns: [{ key: 'id', label: 'ID' }] }, }, ], dense: true, }, global: globalOptions, }) expect(wrapper.findComponent(DataFrameStub).props()).toMatchObject({ size: 'sm', maxHeight: '280px', }) }) it('renders ExperimentTimeline bindings with preview-safe props', () => { const wrapper = mount(ComponentBindingRenderer, { props: { binding: { id: 'instrument-run:ExperimentTimeline', component: 'ExperimentTimeline', propsObject: { modelValue: [{ id: 's001-run', name: 'S001', type: 'measurement', status: 'pending', order: 1 }], editable: true, }, }, dense: true, }, global: globalOptions, }) expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true) expect(wrapper.findComponent(ExperimentTimelineStub).props()).toMatchObject({ modelValue: [{ id: 's001-run', name: 'S001', type: 'measurement', status: 'pending', order: 1 }], editable: false, size: 'sm', }) }) it('renders ScheduleCalendar bindings with preview-safe props', () => { const events = [{ id: 's001-run', title: 'S001', start: '2024-01-01T08:00:00.000Z', end: '2024-01-01T08:10:00.000Z' }] const wrapper = mount(ComponentBindingRenderer, { props: { binding: { id: 'instrument-run:ScheduleCalendar', component: 'ScheduleCalendar', propsObject: { modelValue: events[0].start, events, readonly: false, showNavigation: true, showViewToggle: true, view: 'day', }, }, dense: true, }, global: globalOptions, }) expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true) expect(wrapper.findComponent(ScheduleCalendarStub).props()).toMatchObject({ modelValue: events[0].start, events, readonly: true, showNavigation: false, showViewToggle: false, view: 'day', }) }) it('renders SampleSelector bindings with preview-safe grouping props', () => { const wrapper = mount(ComponentBindingRenderer, { props: { binding: { id: 'assay-matrix:SampleSelector', component: 'SampleSelector', propsObject: { samples: ['S001', 'S002'], modelValue: [], }, }, }, global: globalOptions, }) expect(wrapper.find('[data-component-binding-id="assay-matrix:SampleSelector"]').exists()).toBe(true) expect(wrapper.findComponent(SampleSelectorStub).props()).toMatchObject({ samples: ['S001', 'S002'], modelValue: [], enableGrouping: false, enableSmartGroup: false, }) }) it('should apply PlateEditor intents to its own copy of the plates when not readonly', async () => { const plates = [{ id: 'p1', name: 'P1', format: 96, slot: 'R', injectionVolume: 0, wells: {} }] const wrapper = mount(ComponentBindingRenderer, { props: { binding: { id: 'plate-map', component: 'PlateEditor', propsObject: { modelValue: plates } }, readonly: false, }, global: globalOptions, }) wrapper.findComponent(PlateEditorStub).vm.$emit('assign', 'p1', ['A1'], 'ctrl') await nextTick() expect((wrapper.findComponent(PlateEditorStub).props('modelValue') as Array<{ wells: Record