From c34ed6aafc0542ea12ba7d32e0130eb227da1f81 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Tue, 1 Sep 2026 11:59:41 +0800 Subject: [PATCH] =?UTF-8?q?feat(form):=20=E5=AE=B9=E5=99=A8=E9=80=8F?= =?UTF-8?q?=E4=BC=A0=20labelPosition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 容器未单独配置时回落到父级/表单级,子项自身配置优先。 --- .../src/layouts/props-panel/FormPanel.vue | 2 +- packages/form/src/Form.vue | 3 +- packages/form/src/FormBox.vue | 2 +- packages/form/src/FormDialog.vue | 2 +- packages/form/src/FormDrawer.vue | 2 +- packages/form/src/containers/Col.vue | 2 + packages/form/src/containers/Container.vue | 9 +- packages/form/src/containers/Fieldset.vue | 5 ++ packages/form/src/containers/FlexLayout.vue | 2 + packages/form/src/containers/GroupList.vue | 2 + .../form/src/containers/GroupListItem.vue | 2 + packages/form/src/containers/Panel.vue | 3 + packages/form/src/containers/Row.vue | 2 + packages/form/src/containers/Step.vue | 2 + packages/form/src/containers/Tabs.vue | 2 + .../table-group-list/TableGroupList.vue | 2 + packages/form/src/utils/submitHeadless.ts | 2 +- packages/form/src/utils/tableGroupList.ts | 1 + packages/form/tests/unit/Form.extra.spec.ts | 20 +++++ .../tests/unit/containers/GroupList.spec.ts | 46 ++++++++++ .../form/tests/unit/containers/Panel.spec.ts | 88 +++++++++++++++++++ .../form/tests/unit/containers/Step.spec.ts | 24 +++++ .../form/tests/unit/containers/Tabs.spec.ts | 20 +++++ .../tests/unit/utils/tableGroupList.spec.ts | 27 ++++++ 24 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 packages/form/tests/unit/utils/tableGroupList.spec.ts diff --git a/packages/editor/src/layouts/props-panel/FormPanel.vue b/packages/editor/src/layouts/props-panel/FormPanel.vue index f1c7ef9d..7be705f1 100644 --- a/packages/editor/src/layouts/props-panel/FormPanel.vue +++ b/packages/editor/src/layouts/props-panel/FormPanel.vue @@ -74,7 +74,7 @@ const props = defineProps<{ disabledShowSrc?: boolean; labelWidth?: string; codeValueKey?: string; - labelPosition?: string; + labelPosition?: 'top' | 'left' | 'right'; extendState?: (_state: FormState) => Record | Promise>; }>(); diff --git a/packages/form/src/Form.vue b/packages/form/src/Form.vue index 5dce27bc..65ea78d2 100644 --- a/packages/form/src/Form.vue +++ b/packages/form/src/Form.vue @@ -23,6 +23,7 @@ :last-values="lastValuesProcessed" :is-compare="isCompare" :label-width="item.labelWidth || labelWidth" + :label-position="item.labelPosition || labelPosition" :step-active="stepActive" :size="size" @change="changeHandler" @@ -92,7 +93,7 @@ const props = withDefaults( stepActive?: string | number; size?: 'small' | 'default' | 'large'; inline?: boolean; - labelPosition?: string; + labelPosition?: 'top' | 'left' | 'right'; keyProp?: string; popperClass?: string; preventSubmitDefault?: boolean; diff --git a/packages/form/src/FormBox.vue b/packages/form/src/FormBox.vue index 9492c9b0..40624ac6 100644 --- a/packages/form/src/FormBox.vue +++ b/packages/form/src/FormBox.vue @@ -66,7 +66,7 @@ const props = withDefaults( size?: 'small' | 'default' | 'large'; confirmText?: string; inline?: boolean; - labelPosition?: string; + labelPosition?: 'top' | 'left' | 'right'; preventSubmitDefault?: boolean; /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */ useFieldTextInError?: boolean; diff --git a/packages/form/src/FormDialog.vue b/packages/form/src/FormDialog.vue index d7453a98..611aef19 100644 --- a/packages/form/src/FormDialog.vue +++ b/packages/form/src/FormDialog.vue @@ -94,7 +94,7 @@ const props = withDefaults( disabled?: boolean; title?: string; inline?: boolean; - labelPosition?: string; + labelPosition?: 'top' | 'left' | 'right'; zIndex?: number; size?: 'small' | 'default' | 'large'; confirmText?: string; diff --git a/packages/form/src/FormDrawer.vue b/packages/form/src/FormDrawer.vue index fa333185..0850288e 100644 --- a/packages/form/src/FormDrawer.vue +++ b/packages/form/src/FormDrawer.vue @@ -88,7 +88,7 @@ const props = withDefaults( size?: 'small' | 'default' | 'large'; confirmText?: string; inline?: boolean; - labelPosition?: string; + labelPosition?: 'top' | 'left' | 'right'; preventSubmitDefault?: boolean; /** 透传给内部 `MForm`,控制表单校验失败时错误提示前缀是否使用字段的 text 文案 */ useFieldTextInError?: boolean; diff --git a/packages/form/src/containers/Col.vue b/packages/form/src/containers/Col.vue index b8082c6e..f2c0a433 100644 --- a/packages/form/src/containers/Col.vue +++ b/packages/form/src/containers/Col.vue @@ -7,6 +7,7 @@ :config="config" :prop="prop" :label-width="config.labelWidth || labelWidth" + :label-position="config.labelPosition || labelPosition" :expand-more="expandMore" :size="size" :disabled="disabled" @@ -37,6 +38,7 @@ const props = defineProps<{ isCompare?: boolean; config: FormItemConfig; labelWidth?: string | number; + labelPosition?: 'top' | 'left' | 'right'; expandMore?: boolean; span?: number; size?: string; diff --git a/packages/form/src/containers/Container.vue b/packages/form/src/containers/Container.vue index 523bec3c..c73f22b2 100644 --- a/packages/form/src/containers/Container.vue +++ b/packages/form/src/containers/Container.vue @@ -19,6 +19,7 @@ :step-active="stepActive" :expand-more="expand" :label-width="itemLabelWidth" + :label-position="itemLabelPosition" :style="config.fieldStyle" @change="onChangeHandler" @addDiffCount="onAddDiffCount" @@ -35,7 +36,7 @@ :label-title="config.labelTitle" :text="text" > - + {{ config.titleExtra }} @@ -217,6 +218,7 @@ :step-active="stepActive" :expand-more="expand" :label-width="itemLabelWidth" + :label-position="itemLabelPosition" :prop="itemProp" @change="onChangeHandler" @addDiffCount="onAddDiffCount" @@ -293,6 +295,7 @@ const props = withDefaults( prop?: string; disabled?: boolean; labelWidth?: string | number; + labelPosition?: 'top' | 'left' | 'right'; expandMore?: boolean; stepActive?: string | number; size?: string; @@ -445,7 +448,7 @@ const fieldsProps = computed(() => ({ const formItemProps = computed(() => ({ prop: itemProp.value, labelWidth: itemLabelWidth.value, - labelPosition: props.config.labelPosition, + labelPosition: itemLabelPosition.value, rules: rule.value, extra: filterFunction(mForm, props.config.extra, props), @@ -454,6 +457,8 @@ const formItemProps = computed(() => ({ const itemLabelWidth = computed(() => props.config.labelWidth ?? props.labelWidth); +const itemLabelPosition = computed(() => props.config.labelPosition ?? props.labelPosition); + watchEffect(() => { expand.value = props.expandMore; }); diff --git a/packages/form/src/containers/Fieldset.vue b/packages/form/src/containers/Fieldset.vue index 096f3ce6..53362713 100644 --- a/packages/form/src/containers/Fieldset.vue +++ b/packages/form/src/containers/Fieldset.vue @@ -28,6 +28,7 @@ :prop="prop" :disabled="disabled" :labelWidth="lWidth" + :labelPosition="lPosition" :size="size" @change="changeHandler" @add-diff-count="onAddDiffCount()" @@ -48,6 +49,7 @@ :config="item" :prop="prop" :labelWidth="lWidth" + :labelPosition="lPosition" :size="size" :disabled="disabled" @change="changeHandler" @@ -74,6 +76,7 @@ defineOptions({ const props = withDefaults( defineProps<{ labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; prop: string; size?: string; model: Record; @@ -140,6 +143,8 @@ const lWidth = computed(() => { return props.config.labelWidth || props.labelWidth || (props.config.text ? undefined : '0'); }); +const lPosition = computed(() => props.config.labelPosition || props.labelPosition); + const valueChangeHandler = (value: number | boolean) => { emit('change', value, { modifyKey: checkboxName.value }); }; diff --git a/packages/form/src/containers/FlexLayout.vue b/packages/form/src/containers/FlexLayout.vue index 6e57ad14..6f2b4d72 100644 --- a/packages/form/src/containers/FlexLayout.vue +++ b/packages/form/src/containers/FlexLayout.vue @@ -11,6 +11,7 @@ :size="size" :disabled="disabled" :label-width="config.labelWidth || labelWidth" + :label-position="config.labelPosition || labelPosition" @change="changeHandler" @addDiffCount="onAddDiffCount()" /> @@ -37,6 +38,7 @@ const props = defineProps<{ config: FlexLayoutConfig; name?: string; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; prop?: string; size?: string; disabled?: boolean; diff --git a/packages/form/src/containers/GroupList.vue b/packages/form/src/containers/GroupList.vue index 1378f320..e43d1a5c 100644 --- a/packages/form/src/containers/GroupList.vue +++ b/packages/form/src/containers/GroupList.vue @@ -16,6 +16,7 @@ :prop="prop" :index="Number(index)" :label-width="labelWidth" + :label-position="labelPosition" :size="size" :disabled="disabled" :group-model="model[name]" @@ -53,6 +54,7 @@ const props = defineProps<{ config: GroupListConfig; name: string; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; prop?: string; size?: string; disabled?: boolean; diff --git a/packages/form/src/containers/GroupListItem.vue b/packages/form/src/containers/GroupListItem.vue index 89baf2cd..4600c403 100644 --- a/packages/form/src/containers/GroupListItem.vue +++ b/packages/form/src/containers/GroupListItem.vue @@ -99,6 +99,7 @@ :lastValues="lastValues" :is-compare="isCompare" :labelWidth="labelWidth" + :labelPosition="labelPosition" :prop="rowProp" :size="size" :disabled="disabled" @@ -131,6 +132,7 @@ const props = defineProps<{ groupModel: any[]; config: GroupListConfig; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; prop?: string; size?: string; index: number; diff --git a/packages/form/src/containers/Panel.vue b/packages/form/src/containers/Panel.vue index b1c375c6..98477f09 100644 --- a/packages/form/src/containers/Panel.vue +++ b/packages/form/src/containers/Panel.vue @@ -39,6 +39,7 @@ :size="size" :disabled="disabled" :label-width="config.labelWidth || labelWidth" + :label-position="config.labelPosition || labelPosition" @change="changeHandler" @addDiffCount="onAddDiffCount()" > @@ -59,6 +60,7 @@ :size="size" :disabled="disabled" :label-width="config.labelWidth || labelWidth" + :label-position="config.labelPosition || labelPosition" @change="changeHandler" @addDiffCount="onAddDiffCount()" > @@ -89,6 +91,7 @@ const props = defineProps<{ config: PanelConfig; name?: string; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; prop?: string; size?: string; disabled?: boolean; diff --git a/packages/form/src/containers/Row.vue b/packages/form/src/containers/Row.vue index 2aa2610c..4d64a680 100644 --- a/packages/form/src/containers/Row.vue +++ b/packages/form/src/containers/Row.vue @@ -6,6 +6,7 @@ :span="col.span || config.span || 24 / config.items.length" :config="col" :labelWidth="config.labelWidth || labelWidth" + :labelPosition="config.labelPosition || labelPosition" :expandMore="expandMore" :model="name ? model[name] : model" :lastValues="name ? lastValues[name] : lastValues" @@ -39,6 +40,7 @@ const props = defineProps<{ config: RowConfig; name: string; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; prop?: string; size?: string; expandMore?: boolean; diff --git a/packages/form/src/containers/Step.vue b/packages/form/src/containers/Step.vue index 85218697..9f1ec609 100644 --- a/packages/form/src/containers/Step.vue +++ b/packages/form/src/containers/Step.vue @@ -24,6 +24,7 @@ :size="size" :disabled="disabled" :label-width="config.labelWidth || labelWidth" + :label-position="config.labelPosition || labelPosition" @change="changeHandler" @addDiffCount="onAddDiffCount()" > @@ -54,6 +55,7 @@ const props = withDefaults( config: StepConfig; stepActive?: number; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; size?: string; disabled?: boolean; }>(), diff --git a/packages/form/src/containers/Tabs.vue b/packages/form/src/containers/Tabs.vue index cdb83378..bc683949 100644 --- a/packages/form/src/containers/Tabs.vue +++ b/packages/form/src/containers/Tabs.vue @@ -70,6 +70,7 @@ " :size="size" :label-width="tab.labelWidth || labelWidth" + :label-position="tab.labelPosition || labelPosition" :expand-more="expandMore" @change="changeHandler" @addDiffCount="onAddDiffCount(Number(tabIndex))" @@ -106,6 +107,7 @@ const props = withDefaults( name: string; size?: string; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; prop?: string; expandMore?: boolean; disabled?: boolean; diff --git a/packages/form/src/containers/table-group-list/TableGroupList.vue b/packages/form/src/containers/table-group-list/TableGroupList.vue index e1ca674d..66c473b1 100644 --- a/packages/form/src/containers/table-group-list/TableGroupList.vue +++ b/packages/form/src/containers/table-group-list/TableGroupList.vue @@ -12,6 +12,7 @@ :last-values="lastValues" :prop="prop" :label-width="labelWidth" + :label-position="labelPosition" :show-index="showIndex" :sort-key="sortKey" :sort="sort" @@ -80,6 +81,7 @@ const props = defineProps<{ name: string; prop?: string; labelWidth?: string; + labelPosition?: 'top' | 'left' | 'right'; disabled?: boolean; size?: string; enableToggleMode?: true; diff --git a/packages/form/src/utils/submitHeadless.ts b/packages/form/src/utils/submitHeadless.ts index bad081ae..db1b1811 100644 --- a/packages/form/src/utils/submitHeadless.ts +++ b/packages/form/src/utils/submitHeadless.ts @@ -43,7 +43,7 @@ export interface SubmitFormOptions { stepActive?: string | number; size?: 'small' | 'default' | 'large'; inline?: boolean; - labelPosition?: string; + labelPosition?: 'top' | 'left' | 'right'; keyProp?: string; popperClass?: string; preventSubmitDefault?: boolean; diff --git a/packages/form/src/utils/tableGroupList.ts b/packages/form/src/utils/tableGroupList.ts index eaf3fd3f..e0ce2711 100644 --- a/packages/form/src/utils/tableGroupList.ts +++ b/packages/form/src/utils/tableGroupList.ts @@ -94,6 +94,7 @@ export const getGroupListRowConfig = (config: GroupListConfig, index: number, ke span: config.span || 24, items: config.items, labelWidth: config.labelWidth, + labelPosition: config.labelPosition, [key]: `${(config as Record)[key]}${String(index)}`, }; }; diff --git a/packages/form/tests/unit/Form.extra.spec.ts b/packages/form/tests/unit/Form.extra.spec.ts index 94439c90..bf378d97 100644 --- a/packages/form/tests/unit/Form.extra.spec.ts +++ b/packages/form/tests/unit/Form.extra.spec.ts @@ -51,6 +51,26 @@ describe('Form.vue —— 默认 props', () => { const formEl = wrapper.find('.m-form').element as HTMLElement; expect(formEl.getAttribute('style') || '').toContain('height: 300px'); }); + + test('labelPosition 与 labelWidth 一样,未配置时回落到表单级,配置了则用自身', async () => { + const wrapper = mountForm({ + labelPosition: 'left', + labelWidth: '120px', + config: [ + { name: 'a', type: 'text', text: 'a' }, + { name: 'b', type: 'text', text: 'b', labelPosition: 'top' }, + ], + initValues: { a: '1', b: '2' }, + }); + await nextTick(); + + const containers = wrapper.findAllComponents({ name: 'MFormContainer' }); + const byName = Object.fromEntries(containers.map((c) => [c.props('config')?.name, c])); + + expect(byName.a.props('labelWidth')).toBe('120px'); + expect(byName.a.props('labelPosition')).toBe('left'); + expect(byName.b.props('labelPosition')).toBe('top'); + }); }); describe('Form.vue —— formState getter 行为', () => { diff --git a/packages/form/tests/unit/containers/GroupList.spec.ts b/packages/form/tests/unit/containers/GroupList.spec.ts index ba75db04..43f4f462 100644 --- a/packages/form/tests/unit/containers/GroupList.spec.ts +++ b/packages/form/tests/unit/containers/GroupList.spec.ts @@ -98,4 +98,50 @@ describe('GroupList container', () => { expect(wrapper.text()).not.toContain('下移'); }); }); + + describe('labelPosition 透传', () => { + const getFormItemLabelPosition = (wrapper: ReturnType, prop: string) => { + const item = wrapper.findAllComponents({ name: 'TMFormItem' }).find((w) => w.props('prop') === prop); + return item?.props('labelPosition'); + }; + + test('group-list 的 labelPosition 透传到子表单项', async () => { + const wrapper = mountForm( + [ + { + type: 'group-list', + name: 'list', + labelPosition: 'left', + labelWidth: '80px', + items: [{ name: 'text', type: 'text', text: 'text' }], + }, + ], + { list: [{ text: 'a' }] }, + ); + await nextTick(); + + expect(getFormItemLabelPosition(wrapper, 'list.0.text')).toBe('left'); + }); + + test('子项自身的 labelPosition 优先于 group-list', async () => { + const wrapper = mountForm( + [ + { + type: 'group-list', + name: 'list', + labelPosition: 'left', + items: [ + { name: 'text', type: 'text', text: 'text' }, + { name: 'title', type: 'text', text: 'title', labelPosition: 'top' }, + ], + }, + ], + { list: [{ text: 'a', title: 'b' }] }, + ); + await nextTick(); + + expect(getFormItemLabelPosition(wrapper, 'list.0.text')).toBe('left'); + expect(getFormItemLabelPosition(wrapper, 'list.0.title')).toBe('top'); + }); + }); }); diff --git a/packages/form/tests/unit/containers/Panel.spec.ts b/packages/form/tests/unit/containers/Panel.spec.ts index e7d8b2e1..71269388 100644 --- a/packages/form/tests/unit/containers/Panel.spec.ts +++ b/packages/form/tests/unit/containers/Panel.spec.ts @@ -89,4 +89,92 @@ describe('Panel container', () => { await nextTick(); expect(wrapper.exists()).toBe(true); }); + + describe('labelPosition 透传', () => { + const getFormItemLabelPosition = (wrapper: ReturnType, prop: string) => { + const item = wrapper.findAllComponents({ name: 'TMFormItem' }).find((w) => w.props('prop') === prop); + return item?.props('labelPosition'); + }; + + test('panel 的 labelPosition 透传到子表单项', async () => { + const wrapper = mountForm( + [ + { + type: 'panel', + title: 'group', + labelPosition: 'left', + items: [{ name: 'text', type: 'text', text: 'text' }], + }, + ], + { text: 'hello' }, + ); + await nextTick(); + + expect(getFormItemLabelPosition(wrapper, 'text')).toBe('left'); + }); + + test('fieldset 的 labelPosition 透传到子表单项', async () => { + const wrapper = mountForm( + [ + { + type: 'fieldset', + legend: 'fs', + labelPosition: 'left', + items: [{ name: 'text', type: 'text', text: 'text' }], + }, + ], + { text: 'fs' }, + ); + await nextTick(); + + expect(getFormItemLabelPosition(wrapper, 'text')).toBe('left'); + }); + + test('flex-layout 的 labelPosition 透传到子表单项', async () => { + const wrapper = mountForm( + [ + { + type: 'flex-layout', + labelPosition: 'left', + items: [{ name: 'text', type: 'text', text: 'text' }], + }, + ], + { text: 'fl' }, + ); + await nextTick(); + + expect(getFormItemLabelPosition(wrapper, 'text')).toBe('left'); + }); + + test('row 的 labelPosition 透传到子表单项', async () => { + const wrapper = mountForm( + [ + { + type: 'row', + labelPosition: 'left', + items: [{ name: 'text', type: 'text', text: 'text' }], + }, + ], + { text: 'r' }, + ); + await nextTick(); + + expect(getFormItemLabelPosition(wrapper, 'text')).toBe('left'); + }); + + test('无 type 的 items 容器把 labelPosition 透传到子项', async () => { + const wrapper = mountForm( + [ + { + labelPosition: 'left', + items: [{ name: 'text', type: 'text', text: 'text' }], + }, + ], + { text: 'x' }, + ); + await nextTick(); + + expect(getFormItemLabelPosition(wrapper, 'text')).toBe('left'); + }); + }); }); diff --git a/packages/form/tests/unit/containers/Step.spec.ts b/packages/form/tests/unit/containers/Step.spec.ts index c7d9befb..af489fa8 100644 --- a/packages/form/tests/unit/containers/Step.spec.ts +++ b/packages/form/tests/unit/containers/Step.spec.ts @@ -43,4 +43,28 @@ describe('Step container', () => { expect(wrapper.text()).toContain('Step 1'); expect(wrapper.text()).toContain('Step 2'); }); + + test('step 的 labelPosition 透传到子表单项', async () => { + const wrapper = mountForm( + [ + { + type: 'step', + labelPosition: 'left', + items: [ + { + title: 'Step 1', + name: 's1', + items: [{ name: 'text', type: 'text', text: 'text' }], + }, + ], + }, + ], + { s1: { text: 'a' } }, + { stepActive: 1 }, + ); + await nextTick(); + + const item = wrapper.findAllComponents({ name: 'TMFormItem' }).find((w) => w.props('prop') === 's1.text'); + expect(item?.props('labelPosition')).toBe('left'); + }); }); diff --git a/packages/form/tests/unit/containers/Tabs.spec.ts b/packages/form/tests/unit/containers/Tabs.spec.ts index c3c6c69d..2ab41807 100644 --- a/packages/form/tests/unit/containers/Tabs.spec.ts +++ b/packages/form/tests/unit/containers/Tabs.spec.ts @@ -63,4 +63,24 @@ describe('Tabs', () => { const value = await (wrapper.vm as any).submitForm(); expect(value.text).toBe('text'); }); + + test('tab 的 labelPosition 透传到子表单项', async () => { + const wrapper = getWrapper([ + { + type: 'tab', + items: [ + { + title: 'tab1', + labelPosition: 'left', + items: [{ name: 'text', text: 'text' }], + }, + ], + }, + ]); + + await nextTick(); + + const item = wrapper.findAllComponents({ name: 'TMFormItem' }).find((w) => w.props('prop') === 'text'); + expect(item?.props('labelPosition')).toBe('left'); + }); }); diff --git a/packages/form/tests/unit/utils/tableGroupList.spec.ts b/packages/form/tests/unit/utils/tableGroupList.spec.ts new file mode 100644 index 00000000..7a5f79b8 --- /dev/null +++ b/packages/form/tests/unit/utils/tableGroupList.spec.ts @@ -0,0 +1,27 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2025 Tencent. + */ +import { describe, expect, test } from 'vitest'; +import { getGroupListRowConfig } from '@form/utils/tableGroupList'; + +describe('getGroupListRowConfig', () => { + test('把 group-list 的 labelWidth / labelPosition 复制到 row 配置', () => { + const row = getGroupListRowConfig( + { + type: 'group-list', + name: 'list', + labelWidth: '80px', + labelPosition: 'left', + items: [{ name: 'text', type: 'text', text: 'text' }], + } as any, + 0, + ); + + expect(row.type).toBe('row'); + expect(row.labelWidth).toBe('80px'); + expect(row.labelPosition).toBe('left'); + expect(row.items).toHaveLength(1); + }); +});