import { mount } from '@vue/test-utils'
import { createPinia } from 'pinia'
import { computed, h, nextTick, ref } from 'vue'
import { describe, expect, it, vi } from 'vitest'
import AppLayout from '../../components/AppLayout.vue'
import AppSidebar from '../../components/AppSidebar.vue'
import AppTopBar from '../../components/AppTopBar.vue'
import ControlWorkspaceView from '../../components/ControlWorkspaceView.vue'
import FormActions from '../../components/FormActions.vue'
import FormBuilder from '../../components/FormBuilder.vue'
import {
defineControlModel,
defineControls,
defineDoseDesignControlModel,
useControlWorkspace,
} from '../../composables/useControlSchema'
vi.mock('../../composables/usePlatformContext', () => ({
usePlatformContext: vi.fn(() => ({
isIntegrated: computed(() => false),
context: ref({ isIntegrated: false, theme: 'system' }),
plugin: computed(() => undefined),
user: computed(() => undefined),
theme: computed(() => 'system' as const),
features: computed(() => undefined),
navigate: vi.fn(),
notify: vi.fn(),
sendToPlatform: vi.fn(),
})),
}))
vi.mock('../../composables/useTheme', () => ({
useTheme: vi.fn(() => ({
isDark: ref(false),
theme: ref('light'),
toggleTheme: vi.fn(),
})),
}))
function createWorkspace() {
return useControlWorkspace(defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
view: 'analysis',
},
chartScale: {
default: 'linear',
options: ['linear', 'log'],
section: 'display',
view: 'results',
},
}), {
views: {
analysis: { label: 'Run' },
results: { label: 'Results' },
},
})
}
function mountView(workspace = createWorkspace()) {
return mount(ControlWorkspaceView, {
props: {
workspace,
title: 'Analysis Workspace',
},
global: {
plugins: [createPinia()],
stubs: {
'router-link': {
template: '',
},
BaseModal: {
template: '
',
},
},
},
})
}
const globalOptions = {
plugins: [createPinia()],
stubs: {
'router-link': {
template: '',
},
BaseModal: {
template: '
',
},
},
}
describe('ControlWorkspaceView', () => {
it('renders AppTopBar, AppSidebar, and FormBuilder from one workspace object', () => {
const workspace = createWorkspace()
const wrapper = mountView(workspace)
expect(wrapper.findComponent(AppTopBar).props('title')).toBe('Analysis Workspace')
expect('tabs' in wrapper.findComponent(AppTopBar).props()).toBe(false)
expect(wrapper.findComponent(AppTopBar).props('pillNav')).toEqual([
{ id: 'analysis', label: 'Run' },
{ id: 'results', label: 'Results' },
])
expect(wrapper.findComponent(AppTopBar).props('currentPillId')).toBe('analysis')
expect(wrapper.findComponent(AppLayout).props('responsiveSidebar')).toBe(true)
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('analysis')
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject(workspace.values)
})
it('lets callers override the generated sidebar chrome', () => {
const wrapper = mount(ControlWorkspaceView, {
props: {
workspace: createWorkspace(),
sidebarVariant: 'default',
responsiveSidebar: false,
sidebarTitle: 'Peak Picking',
sidebarSubtitle: 'Current experiment',
sidebarBadge: 3,
},
global: globalOptions,
})
expect(wrapper.findComponent(AppLayout).props('responsiveSidebar')).toBe(false)
expect(wrapper.findComponent(AppSidebar).props('title')).toBe('Peak Picking')
expect(wrapper.findComponent(AppSidebar).props('subtitle')).toBe('Current experiment')
expect(wrapper.findComponent(AppSidebar).props('badge')).toBe(3)
})
it('can create the workspace internally from direct controls', async () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
view: 'analysis',
},
chartScale: {
default: 'linear',
options: ['linear', 'log'],
section: 'display',
view: 'results',
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
controlOptions: {
views: {
analysis: { label: 'Run' },
results: { label: 'Results' },
},
},
title: 'Direct Workspace',
},
global: globalOptions,
})
expect(wrapper.findComponent(AppTopBar).props('title')).toBe('Direct Workspace')
expect(wrapper.findComponent(AppTopBar).props('pillNav')).toEqual([
{ id: 'analysis', label: 'Run' },
{ id: 'results', label: 'Results' },
])
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('analysis')
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.05,
chartScale: 'linear',
})
wrapper.findComponent(AppTopBar).vm.$emit('pill-select', { id: 'results', label: 'Results' })
await nextTick()
expect(wrapper.findComponent(AppTopBar).props('currentPillId')).toBe('results')
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('results')
})
it('accepts a nested control model through direct v-bind props', () => {
const model = defineControlModel({
views: {
design: {
label: 'Design',
sections: {
dose: {
label: 'Dose design',
controls: {
replicates: {
type: 'number',
default: 3,
min: 1,
},
},
},
},
},
results: {
label: 'Results',
section: 'plots',
sectionLabel: 'Plots',
controls: {
chartScale: ['linear', 'log'],
},
},
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
...model,
title: 'Nested Model Workspace',
},
global: globalOptions,
})
expect(wrapper.findComponent(AppTopBar).props('pillNav')).toEqual([
{ id: 'design', label: 'Design' },
{ id: 'results', label: 'Results' },
])
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('design')
expect(wrapper.findComponent(AppSidebar).props('panels')).toMatchObject({
design: [{ id: 'design-dose', label: 'Dose design' }],
results: [{ id: 'plots', label: 'Plots' }],
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
replicates: 3,
chartScale: 'linear',
})
})
it('accepts a nested control model through one model prop', () => {
const model = defineControlModel({
views: {
design: {
label: 'Design',
sections: {
dose: {
label: 'Dose design',
controls: {
replicates: {
type: 'number',
default: 3,
min: 1,
},
},
},
},
},
results: {
label: 'Results',
section: 'plots',
sectionLabel: 'Plots',
controls: {
chartScale: ['linear', 'log'],
},
},
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
model,
title: 'Model Workspace',
},
global: globalOptions,
})
expect(wrapper.findComponent(AppTopBar).props('pillNav')).toEqual([
{ id: 'design', label: 'Design' },
{ id: 'results', label: 'Results' },
])
expect(wrapper.findComponent(AppSidebar).props('panels')).toMatchObject({
design: [{ id: 'design-dose', label: 'Dose design' }],
results: [{ id: 'plots', label: 'Plots' }],
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
replicates: 3,
chartScale: 'linear',
})
})
it('merges model and prop controlOptions initial values', () => {
const model = defineControlModel({
initialValues: {
replicates: 4,
chartScale: 'log',
},
views: {
design: {
label: 'Design',
sections: {
dose: {
label: 'Dose design',
controls: {
replicates: {
type: 'number',
default: 3,
min: 1,
},
},
},
},
},
results: {
label: 'Results',
section: 'plots',
sectionLabel: 'Plots',
controls: {
chartScale: ['linear', 'log'],
},
},
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
model,
controlOptions: {
initialValues: {
chartScale: 'linear',
},
},
},
global: globalOptions,
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
replicates: 4,
chartScale: 'linear',
})
})
it('accepts a raw nested control model through one model prop', () => {
const wrapper = mount(ControlWorkspaceView, {
props: {
model: {
views: {
setup: {
label: 'Setup',
sections: {
parameters: {
label: 'Parameters',
controls: {
threshold: { type: 'number', default: 0.05 },
normalize: true,
},
},
},
},
review: {
label: 'Review',
controls: {
chartScale: ['linear', 'log'],
},
},
},
},
title: 'Raw Model Workspace',
},
global: globalOptions,
})
expect(wrapper.findComponent(AppTopBar).props('title')).toBe('Raw Model Workspace')
expect(wrapper.findComponent(AppTopBar).props('pillNav')).toEqual([
{ id: 'setup', label: 'Setup' },
{ id: 'review', label: 'Review' },
])
expect(wrapper.findComponent(AppSidebar).props('panels')).toMatchObject({
setup: [{ id: 'setup-parameters', label: 'Parameters' }],
review: [{ id: 'review-controls', label: 'Review' }],
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.05,
normalize: true,
chartScale: 'linear',
})
})
it('resolves component props from a dose design model prop', () => {
const model = defineDoseDesignControlModel({
selectedWells: ['A1', 'A2'],
includeMolecularWeight: true,
})
const wrapper = mount(ControlWorkspaceView, {
props: {
model,
},
slots: {
default: ({ componentBindings, componentPropsById }) => h(
'pre',
{ class: 'component-props-by-id' },
JSON.stringify({ componentBindings, componentPropsById }),
),
},
global: globalOptions,
})
expect(JSON.parse(wrapper.find('.component-props-by-id').text())).toEqual({
componentBindings: [
{
id: 'plate',
component: 'WellPlate',
props: {
modelValue: ['A1', 'A2'],
format: 96,
disabled: false,
},
},
{
id: 'dose',
component: 'DoseCalculator',
props: {
mode: 'serial',
targetWells: ['A1', 'A2'],
disabled: false,
molecularWeight: 300,
},
},
],
componentPropsById: {
plate: {
modelValue: ['A1', 'A2'],
format: 96,
disabled: false,
},
dose: {
mode: 'serial',
targetWells: ['A1', 'A2'],
disabled: false,
molecularWeight: 300,
},
},
})
})
it('accepts component prop mappings from a nested control model', async () => {
const model = defineControlModel({
views: {
design: {
label: 'Dose design',
controls: {
doseMode: {
default: 'serial',
options: ['serial', 'dilution'],
},
targetWells: {
type: 'tags',
default: ['A1', 'A2'],
},
enabled: true,
},
},
},
componentProps: {
mode: 'doseMode',
targetWells: 'targetWells',
disabled: values => !values.enabled,
},
componentPropsById: {
dose: {
mode: 'doseMode',
targetWells: 'targetWells',
disabled: values => !values.enabled,
},
plate: {
modelValue: 'targetWells',
disabled: values => !values.enabled,
},
},
componentBindings: {
dose: {
component: 'DoseCalculator',
props: {
mode: 'doseMode',
targetWells: 'targetWells',
disabled: values => !values.enabled,
},
},
plate: {
component: 'WellPlate',
props: {
modelValue: 'targetWells',
disabled: values => !values.enabled,
},
},
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
...model,
},
slots: {
default: ({ componentBindingsById, componentProps, componentPropsById }) => h(
'pre',
{ class: 'component-props' },
JSON.stringify({ componentBindingsById, componentProps, componentPropsById }),
),
},
global: globalOptions,
})
expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
componentBindingsById: {
dose: {
id: 'dose',
component: 'DoseCalculator',
props: {
mode: 'serial',
targetWells: ['A1', 'A2'],
disabled: false,
},
},
plate: {
id: 'plate',
component: 'WellPlate',
props: {
modelValue: ['A1', 'A2'],
disabled: false,
},
},
},
componentProps: {
mode: 'serial',
targetWells: ['A1', 'A2'],
disabled: false,
},
componentPropsById: {
dose: {
mode: 'serial',
targetWells: ['A1', 'A2'],
disabled: false,
},
plate: {
modelValue: ['A1', 'A2'],
disabled: false,
},
},
})
wrapper.findComponent(FormBuilder).vm.$emit('update:modelValue', {
doseMode: 'dilution',
targetWells: ['B1'],
enabled: false,
})
await nextTick()
expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
componentBindingsById: {
dose: {
id: 'dose',
component: 'DoseCalculator',
props: {
mode: 'dilution',
targetWells: ['B1'],
disabled: true,
},
},
plate: {
id: 'plate',
component: 'WellPlate',
props: {
modelValue: ['B1'],
disabled: true,
},
},
},
componentProps: {
mode: 'dilution',
targetWells: ['B1'],
disabled: true,
},
componentPropsById: {
dose: {
mode: 'dilution',
targetWells: ['B1'],
disabled: true,
},
plate: {
modelValue: ['B1'],
disabled: true,
},
},
})
})
it('accepts initial values directly for internally generated workspaces', () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
},
chartScale: {
default: 'linear',
options: ['linear', 'log'],
section: 'display',
},
sampleNames: {
type: 'tags',
default: ['Control'],
section: 'samples',
},
enabled: true,
})
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
controlOptions: {
initialValues: {
threshold: 0.2,
chartScale: 'linear',
},
},
initialValues: {
chartScale: 'log',
},
},
global: globalOptions,
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.2,
chartScale: 'log',
enabled: true,
})
})
it('syncs internally generated workspace values with v-model:values', async () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
},
chartScale: {
default: 'linear',
options: ['linear', 'log'],
section: 'display',
},
enabled: true,
})
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
values: {
threshold: 0.2,
chartScale: 'log',
sampleNames: ['Vehicle', 'Drug'],
enabled: true,
},
},
global: globalOptions,
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.2,
chartScale: 'log',
sampleNames: ['Vehicle', 'Drug'],
enabled: true,
})
wrapper.findComponent(FormBuilder).vm.$emit('update:modelValue', {
threshold: 0.5,
chartScale: 'linear',
sampleNames: ['Vehicle', 'Dose A'],
enabled: false,
})
await nextTick()
expect(wrapper.emitted('update:values')?.at(-1)).toEqual([{
threshold: 0.5,
chartScale: 'linear',
sampleNames: ['Vehicle', 'Dose A'],
enabled: false,
}])
await wrapper.setProps({
values: {
threshold: 0.8,
chartScale: 'log',
sampleNames: ['Vehicle', 'Dose A'],
enabled: true,
},
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.8,
chartScale: 'log',
sampleNames: ['Vehicle', 'Dose A'],
enabled: true,
})
})
it('syncs internally generated workspace values with default v-model', async () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
},
chartScale: {
default: 'linear',
options: ['linear', 'log'],
section: 'display',
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
modelValue: {
threshold: 0.2,
chartScale: 'log',
},
},
global: globalOptions,
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.2,
chartScale: 'log',
})
wrapper.findComponent(FormBuilder).vm.$emit('update:modelValue', {
threshold: 0.5,
chartScale: 'linear',
})
await nextTick()
expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([{
threshold: 0.5,
chartScale: 'linear',
}])
expect(wrapper.emitted('update:values')?.at(-1)).toEqual([{
threshold: 0.5,
chartScale: 'linear',
}])
await wrapper.setProps({
modelValue: {
threshold: 0.8,
chartScale: 'log',
},
})
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.8,
chartScale: 'log',
})
})
it('activates direct controls that are loaded after mount', async () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
view: 'analysis',
},
method: {
default: 'linear',
options: ['linear', 'logistic'],
section: 'parameters',
view: 'analysis',
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
title: 'Async Workspace',
},
global: globalOptions,
})
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('')
await wrapper.setProps({
controls,
controlOptions: {
views: {
analysis: { label: 'Run' },
},
},
})
expect(wrapper.findComponent(AppTopBar).props('pillNav')).toEqual([
{ id: 'analysis', label: 'Run' },
])
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('analysis')
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.05,
method: 'linear',
})
})
it('preserves generated values and active view when direct controls change', async () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
view: 'analysis',
},
chartScale: {
default: 'linear',
options: ['linear', 'log'],
section: 'display',
view: 'results',
},
})
const nextControls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
view: 'analysis',
},
chartScale: {
default: 'linear',
options: ['linear', 'log'],
section: 'display',
view: 'results',
},
normalize: {
default: true,
section: 'display',
view: 'results',
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
controlOptions: {
views: {
analysis: { label: 'Run' },
results: { label: 'Results' },
},
},
},
global: globalOptions,
})
wrapper.findComponent(FormBuilder).vm.$emit('update:modelValue', {
threshold: 0.5,
chartScale: 'log',
})
wrapper.findComponent(AppTopBar).vm.$emit('pill-select', { id: 'results', label: 'Results' })
await nextTick()
await wrapper.setProps({ controls: nextControls })
expect(wrapper.findComponent(AppTopBar).props('currentPillId')).toBe('results')
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('results')
expect(wrapper.findComponent(FormBuilder).props('modelValue')).toMatchObject({
threshold: 0.5,
chartScale: 'log',
normalize: true,
})
})
it('forwards default FormBuilder submit and cancel events', async () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
controlOptions: {
showCancel: true,
},
showFormActions: true,
},
global: globalOptions,
})
wrapper.findComponent(FormActions).vm.$emit('submit')
wrapper.findComponent(FormActions).vm.$emit('cancel')
await nextTick()
expect(wrapper.emitted('submit')).toEqual([[{ threshold: 0.05 }]])
expect(wrapper.emitted('cancel')).toEqual([[]])
})
it('passes generated form state and enhancements to sidebar and content forms', () => {
const controls = defineControls({
threshold: {
type: 'number',
default: 0.05,
section: 'parameters',
view: 'analysis',
},
})
const formEnhancements = {
onFieldChange: vi.fn(),
}
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
formEnhancements,
formLoading: true,
formDisabled: true,
formReadonly: true,
},
global: globalOptions,
})
const sidebar = wrapper.findComponent(AppSidebar)
expect(sidebar.props('formEnhancements')).toMatchObject(formEnhancements)
expect(sidebar.props('formLoading')).toBe(true)
expect(sidebar.props('formDisabled')).toBe(true)
expect(sidebar.props('formReadonly')).toBe(true)
for (const form of wrapper.findAllComponents(FormBuilder)) {
expect(form.props('enhancements')).toMatchObject(formEnhancements)
expect(form.props('loading')).toBe(true)
expect(form.props('disabled')).toBe(true)
expect(form.props('readonly')).toBe(true)
}
})
it('keeps topbar pill navigation and sidebar view synchronized', async () => {
const workspace = createWorkspace()
const wrapper = mountView(workspace)
wrapper.findComponent(AppTopBar).vm.$emit('pill-select', { id: 'results', label: 'Results' })
await nextTick()
expect(workspace.activeView.value).toBe('results')
expect(wrapper.findComponent(AppTopBar).props('currentPillId')).toBe('results')
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('results')
})
it('exposes workspace data to the default slot', () => {
const workspace = createWorkspace()
const wrapper = mount(ControlWorkspaceView, {
props: { workspace },
slots: {
default: 'Custom analysis content
',
},
global: {
plugins: [createPinia()],
stubs: {
'router-link': {
template: '',
},
BaseModal: {
template: '
',
},
},
},
})
expect(wrapper.find('.custom-content').text()).toBe('Custom analysis content')
expect(wrapper.findAllComponents(FormBuilder)).toHaveLength(1)
})
it('exposes mapped component props to the default slot', async () => {
const controls = defineControls({
sampleNames: {
type: 'tags',
default: ['Control', 'Treatment'],
section: 'samples',
},
enabled: true,
})
const wrapper = mount(ControlWorkspaceView, {
props: {
controls,
componentProps: {
samples: 'sampleNames',
disabled: values => !values.enabled,
},
},
slots: {
default: ({ componentProps }) => h(
'pre',
{ class: 'component-props' },
JSON.stringify(componentProps),
),
},
global: globalOptions,
})
expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
samples: ['Control', 'Treatment'],
disabled: false,
})
wrapper.findComponent(FormBuilder).vm.$emit('update:modelValue', {
sampleNames: ['Vehicle'],
enabled: false,
})
await nextTick()
expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
samples: ['Vehicle'],
disabled: true,
})
})
it('exposes resolved workspace bindings to custom slots', () => {
const model = defineControlModel({
views: {
analysis: {
label: 'Run',
controls: {
threshold: {
type: 'number',
default: 0.05,
},
enabled: true,
},
},
},
componentProps: {
score: 'threshold',
disabled: values => !values.enabled,
},
componentPropsById: {
chart: {
score: 'threshold',
disabled: values => !values.enabled,
},
},
componentBindings: {
chart: {
component: 'ResultChart',
props: {
score: 'threshold',
disabled: values => !values.enabled,
},
},
},
})
const wrapper = mount(ControlWorkspaceView, {
props: {
model,
},
slots: {
topbar: ({ bindings, topBar }) => h(
'pre',
{ class: 'topbar-bindings' },
JSON.stringify({
slotPillId: topBar.currentPillId,
pillId: bindings.topBar.value.currentPillId,
}),
),
sidebar: ({ bindings, sidebar }) => h(
'pre',
{ class: 'sidebar-bindings' },
JSON.stringify({
activeView: sidebar.activeView,
bindingActiveView: bindings.sidebar.activeView,
}),
),
default: ({ bindings, componentBindingsById, componentPropsById }) => h(
'pre',
{ class: 'default-bindings' },
JSON.stringify({
formThreshold: bindings.form.modelValue.threshold,
component: componentBindingsById.chart.component,
bindingComponent: bindings.componentBindingsById.value.chart.component,
bindingScore: bindings.componentProps.value.score,
bindingComponentScore: bindings.componentBindings.value[0].props.score,
slotScore: componentPropsById.chart.score,
bindingSlotScore: bindings.componentPropsById.value.chart.score,
}),
),
},
global: globalOptions,
})
expect(JSON.parse(wrapper.find('.topbar-bindings').text())).toEqual({
slotPillId: 'analysis',
pillId: 'analysis',
})
expect(JSON.parse(wrapper.find('.sidebar-bindings').text())).toEqual({
activeView: 'analysis',
bindingActiveView: 'analysis',
})
expect(JSON.parse(wrapper.find('.default-bindings').text())).toEqual({
formThreshold: 0.05,
component: 'ResultChart',
bindingComponent: 'ResultChart',
bindingScore: 0.05,
bindingComponentScore: 0.05,
slotScore: 0.05,
bindingSlotScore: 0.05,
})
})
})