|
1 | 1 | import React from 'react'; |
2 | 2 | import { MinusCircleIcon } from 'tdesign-icons-react'; |
3 | | -import { fireEvent, mockTimeout, render, vi } from '@test/utils'; |
| 3 | +import { fireEvent, mockTimeout, render, vi, waitFor, within } from '@test/utils'; |
4 | 4 |
|
5 | 5 | import Button from '../../button'; |
| 6 | +import Dialog from '../../dialog'; |
6 | 7 | import Input from '../../input'; |
7 | 8 | import Radio from '../../radio'; |
8 | 9 | import FormList from '../FormList'; |
@@ -361,7 +362,7 @@ describe('FormList 组件测试', () => { |
361 | 362 | expect(fn).toHaveBeenCalledTimes(1); |
362 | 363 | }); |
363 | 364 |
|
364 | | - test('FormList with nested structures', async () => { |
| 365 | + test('FormList with nested objects', async () => { |
365 | 366 | const TestView = () => { |
366 | 367 | const [form] = Form.useForm(); |
367 | 368 |
|
@@ -733,6 +734,167 @@ describe('FormList 组件测试', () => { |
733 | 734 | expect(queryByText('用户名必填')).not.toBeTruthy(); |
734 | 735 | }); |
735 | 736 |
|
| 737 | + test('FormList with nested arrays', async () => { |
| 738 | + const onValuesChangeFn = vi.fn(); |
| 739 | + let latestFormValues: any = {}; |
| 740 | + let latestChangedValues: any = {}; |
| 741 | + |
| 742 | + const TestView = () => { |
| 743 | + const [form] = Form.useForm(); |
| 744 | + |
| 745 | + const INIT_DATA = { |
| 746 | + vector: ['v1', 'v2', 'v3'], |
| 747 | + matrix: [['m11', 'm12'], ['m21', 'm22', 'm23'], ['m31']], |
| 748 | + }; |
| 749 | + |
| 750 | + return ( |
| 751 | + <Form |
| 752 | + form={form} |
| 753 | + initialData={INIT_DATA} |
| 754 | + onValuesChange={(changedValues, allValues) => { |
| 755 | + onValuesChangeFn(changedValues, allValues); |
| 756 | + latestChangedValues = changedValues; |
| 757 | + latestFormValues = allValues; |
| 758 | + }} |
| 759 | + > |
| 760 | + <div data-testid="vector"> |
| 761 | + <FormList name="vector"> |
| 762 | + {(fields, { add, remove }) => ( |
| 763 | + <> |
| 764 | + {fields.map(({ key, name }) => ( |
| 765 | + <div key={key}> |
| 766 | + <FormItem name={name}> |
| 767 | + <Input /> |
| 768 | + </FormItem> |
| 769 | + <Button onClick={() => remove(name)}>Remove</Button> |
| 770 | + </div> |
| 771 | + ))} |
| 772 | + <Button onClick={() => add('v-new')}>Add Vector</Button> |
| 773 | + </> |
| 774 | + )} |
| 775 | + </FormList> |
| 776 | + </div> |
| 777 | + |
| 778 | + <div data-testid="matrix"> |
| 779 | + <FormList name="matrix"> |
| 780 | + {(rowFields, { add: addRow, remove: removeRow }) => ( |
| 781 | + <> |
| 782 | + {rowFields.map(({ key, name: rowName }) => ( |
| 783 | + <div key={key}> |
| 784 | + <div>Row {rowName}</div> |
| 785 | + <FormList name={rowName}> |
| 786 | + {(colFields, { add: addCol, remove: removeCol }) => ( |
| 787 | + <> |
| 788 | + {colFields.map(({ key, name: colName }) => ( |
| 789 | + <div key={key}> |
| 790 | + <FormItem name={colName}> |
| 791 | + <Input /> |
| 792 | + </FormItem> |
| 793 | + <Button onClick={() => removeCol(colName)}>Remove Col</Button> |
| 794 | + </div> |
| 795 | + ))} |
| 796 | + <Button onClick={() => addCol('m-new')}>Add Col</Button> |
| 797 | + </> |
| 798 | + )} |
| 799 | + </FormList> |
| 800 | + <Button onClick={() => removeRow(rowName)}>Remove Row</Button> |
| 801 | + </div> |
| 802 | + ))} |
| 803 | + <Button onClick={() => addRow(['m-row'])}>Add Row</Button> |
| 804 | + </> |
| 805 | + )} |
| 806 | + </FormList> |
| 807 | + </div> |
| 808 | + </Form> |
| 809 | + ); |
| 810 | + }; |
| 811 | + |
| 812 | + const { getByText, getByTestId, getAllByText } = render(<TestView />); |
| 813 | + |
| 814 | + const getInputValues = (testId: string) => |
| 815 | + within(getByTestId(testId)) |
| 816 | + .getAllByRole('textbox') |
| 817 | + .map((el) => (el as HTMLInputElement).value); |
| 818 | + |
| 819 | + // ===== initial ===== |
| 820 | + expect(getInputValues('vector')).toEqual(['v1', 'v2', 'v3']); |
| 821 | + expect(getInputValues('matrix')).toEqual(['m11', 'm12', 'm21', 'm22', 'm23', 'm31']); |
| 822 | + |
| 823 | + // ===== vector: add ===== |
| 824 | + fireEvent.click(getByText('Add Vector')); |
| 825 | + await mockTimeout(() => true); |
| 826 | + expect(getInputValues('vector')).toEqual(['v1', 'v2', 'v3', 'v-new']); |
| 827 | + expect(onValuesChangeFn).toHaveBeenCalled(); |
| 828 | + expect(latestChangedValues).toEqual({ |
| 829 | + vector: ['v1', 'v2', 'v3', 'v-new'], |
| 830 | + }); |
| 831 | + expect(latestFormValues).toEqual({ |
| 832 | + vector: ['v1', 'v2', 'v3', 'v-new'], |
| 833 | + matrix: [['m11', 'm12'], ['m21', 'm22', 'm23'], ['m31']], |
| 834 | + }); |
| 835 | + |
| 836 | + // ===== vector: remove ===== |
| 837 | + fireEvent.click(within(getByTestId('vector')).getAllByText('Remove')[0]); |
| 838 | + await mockTimeout(() => true); |
| 839 | + expect(latestChangedValues).toEqual({ |
| 840 | + vector: ['v2', 'v3', 'v-new'], |
| 841 | + }); |
| 842 | + expect(latestFormValues).toEqual({ |
| 843 | + vector: ['v2', 'v3', 'v-new'], |
| 844 | + matrix: [['m11', 'm12'], ['m21', 'm22', 'm23'], ['m31']], |
| 845 | + }); |
| 846 | + |
| 847 | + // ===== matrix: add col (row 0) ===== |
| 848 | + const matrix = getByTestId('matrix'); |
| 849 | + fireEvent.click(within(matrix).getAllByText('Add Col')[0]); |
| 850 | + await mockTimeout(() => true); |
| 851 | + const expectedChangedMatrix1 = []; |
| 852 | + expectedChangedMatrix1[0] = ['m11', 'm12', 'm-new']; |
| 853 | + expect(latestChangedValues).toEqual({ |
| 854 | + matrix: expectedChangedMatrix1, |
| 855 | + }); |
| 856 | + expect(latestFormValues).toEqual({ |
| 857 | + vector: ['v2', 'v3', 'v-new'], |
| 858 | + matrix: [['m11', 'm12', 'm-new'], ['m21', 'm22', 'm23'], ['m31']], |
| 859 | + }); |
| 860 | + |
| 861 | + // ===== matrix: remove col ===== |
| 862 | + const row0 = getAllByText(/^Row /)[0].parentElement; |
| 863 | + fireEvent.click(within(row0).getAllByText('Remove Col')[0]); |
| 864 | + await mockTimeout(() => true); |
| 865 | + const expectedChangedMatrix2 = []; |
| 866 | + expectedChangedMatrix2[0] = ['m12', 'm-new']; |
| 867 | + expect(latestChangedValues).toEqual({ |
| 868 | + matrix: expectedChangedMatrix2, |
| 869 | + }); |
| 870 | + expect(latestFormValues).toEqual({ |
| 871 | + vector: ['v2', 'v3', 'v-new'], |
| 872 | + matrix: [['m12', 'm-new'], ['m21', 'm22', 'm23'], ['m31']], |
| 873 | + }); |
| 874 | + |
| 875 | + // ===== matrix: add row ===== |
| 876 | + fireEvent.click(getByText('Add Row')); |
| 877 | + await mockTimeout(() => true); |
| 878 | + expect(latestChangedValues).toEqual({ |
| 879 | + matrix: [['m12', 'm-new'], ['m21', 'm22', 'm23'], ['m31'], ['m-row']], |
| 880 | + }); |
| 881 | + expect(latestFormValues).toEqual({ |
| 882 | + vector: ['v2', 'v3', 'v-new'], |
| 883 | + matrix: [['m12', 'm-new'], ['m21', 'm22', 'm23'], ['m31'], ['m-row']], |
| 884 | + }); |
| 885 | + |
| 886 | + // ===== matrix: remove row ===== |
| 887 | + fireEvent.click(within(getAllByText(/^Row /)[0].parentElement).getByText('Remove Row')); |
| 888 | + await mockTimeout(() => true); |
| 889 | + expect(latestChangedValues).toEqual({ |
| 890 | + matrix: [['m21', 'm22', 'm23'], ['m31'], ['m-row']], |
| 891 | + }); |
| 892 | + expect(latestFormValues).toEqual({ |
| 893 | + vector: ['v2', 'v3', 'v-new'], |
| 894 | + matrix: [['m21', 'm22', 'm23'], ['m31'], ['m-row']], |
| 895 | + }); |
| 896 | + }); |
| 897 | + |
736 | 898 | test('FormList with shouldUpdate', async () => { |
737 | 899 | const TestView = () => { |
738 | 900 | const [form] = Form.useForm(); |
@@ -985,4 +1147,114 @@ describe('FormList 组件测试', () => { |
985 | 1147 | expect(specifiedWeightInputAgain.value).toBe('50'); |
986 | 1148 | expect(container.querySelector('[placeholder="route-abtest-0-3"]')).toBeFalsy(); |
987 | 1149 | }); |
| 1150 | + |
| 1151 | + test('FormList with Form in Dialog', async () => { |
| 1152 | + const TestView = () => { |
| 1153 | + const [mainForm] = Form.useForm(); |
| 1154 | + const [dialogForm] = Form.useForm(); |
| 1155 | + |
| 1156 | + const [dialogVisible, setDialogVisible] = React.useState(false); |
| 1157 | + const [editingIndex, setEditingIndex] = React.useState<number | null>(null); |
| 1158 | + |
| 1159 | + const openDialog = (index: number) => { |
| 1160 | + setEditingIndex(index); |
| 1161 | + const currentAmount = mainForm.getFieldValue(['main', index, 'userAmount']) || ''; |
| 1162 | + dialogForm.setFieldsValue({ amount: currentAmount }); |
| 1163 | + setDialogVisible(true); |
| 1164 | + }; |
| 1165 | + |
| 1166 | + const handleConfirm = () => { |
| 1167 | + const amount = dialogForm.getFieldValue('amount'); |
| 1168 | + mainForm.setFieldsValue({ |
| 1169 | + main: mainForm |
| 1170 | + .getFieldValue('main') |
| 1171 | + .map((item: any, idx: number) => (idx === editingIndex ? { ...item, userAmount: amount } : item)), |
| 1172 | + }); |
| 1173 | + setDialogVisible(false); |
| 1174 | + }; |
| 1175 | + |
| 1176 | + return ( |
| 1177 | + <Form form={mainForm} initialData={{ main: [{ userAmount: '' }] }}> |
| 1178 | + <FormList name="main"> |
| 1179 | + {(fields, { add }) => ( |
| 1180 | + <> |
| 1181 | + {fields.map(({ key, name }) => ( |
| 1182 | + <div key={key}> |
| 1183 | + <FormItem name={[name, 'userAmount']} label={`用户金额 ${name + 1}`}> |
| 1184 | + <Input disabled data-testid={`amount-${name}`} /> |
| 1185 | + </FormItem> |
| 1186 | + <Button onClick={() => openDialog(name)}>设置金额</Button> |
| 1187 | + </div> |
| 1188 | + ))} |
| 1189 | + |
| 1190 | + <Button onClick={() => add({ userAmount: '' })}>新增一项</Button> |
| 1191 | + |
| 1192 | + <Dialog visible={dialogVisible} onConfirm={handleConfirm} onCancel={() => setDialogVisible(false)}> |
| 1193 | + <Form form={dialogForm}> |
| 1194 | + <FormItem name="amount"> |
| 1195 | + <Input data-testid="dialog-input" /> |
| 1196 | + </FormItem> |
| 1197 | + </Form> |
| 1198 | + </Dialog> |
| 1199 | + </> |
| 1200 | + )} |
| 1201 | + </FormList> |
| 1202 | + </Form> |
| 1203 | + ); |
| 1204 | + }; |
| 1205 | + |
| 1206 | + const { getByText, getAllByText, getByTestId, findByTestId } = render(<TestView />); |
| 1207 | + |
| 1208 | + // 初始只有一项 |
| 1209 | + expect(getByText('用户金额 1')).toBeInTheDocument(); |
| 1210 | + |
| 1211 | + // 新增一项 |
| 1212 | + fireEvent.click(getByText('新增一项')); |
| 1213 | + expect(getByText('用户金额 2')).toBeInTheDocument(); |
| 1214 | + |
| 1215 | + // ===== 设置第一项金额 ===== |
| 1216 | + fireEvent.click(getAllByText('设置金额')[0]); |
| 1217 | + |
| 1218 | + const dialogInputWrapper1 = await findByTestId('dialog-input'); |
| 1219 | + const dialogInput1 = within(dialogInputWrapper1).getByRole('textbox'); |
| 1220 | + fireEvent.change(dialogInput1, { target: { value: '100' } }); |
| 1221 | + |
| 1222 | + const confirmButton1 = document.querySelector('.t-dialog__confirm') as HTMLButtonElement; |
| 1223 | + fireEvent.click(confirmButton1); |
| 1224 | + |
| 1225 | + await waitFor(() => { |
| 1226 | + const amountInput1 = within(getByTestId('amount-0')).getByRole('textbox'); |
| 1227 | + expect(amountInput1).toHaveValue('100'); |
| 1228 | + }); |
| 1229 | + |
| 1230 | + // ===== 设置第二项金额 ===== |
| 1231 | + fireEvent.click(getAllByText('设置金额')[1]); |
| 1232 | + |
| 1233 | + const dialogInputWrapper2 = await findByTestId('dialog-input'); |
| 1234 | + const dialogInput2 = within(dialogInputWrapper2).getByRole('textbox'); |
| 1235 | + fireEvent.change(dialogInput2, { target: { value: '200' } }); |
| 1236 | + |
| 1237 | + const confirmButton2 = document.querySelector('.t-dialog__confirm') as HTMLButtonElement; |
| 1238 | + fireEvent.click(confirmButton2); |
| 1239 | + |
| 1240 | + await waitFor(() => { |
| 1241 | + const amountInput2 = within(getByTestId('amount-1')).getByRole('textbox'); |
| 1242 | + expect(amountInput2).toHaveValue('200'); |
| 1243 | + }); |
| 1244 | + |
| 1245 | + // ===== 取消不生效 ===== |
| 1246 | + fireEvent.click(getAllByText('设置金额')[0]); |
| 1247 | + |
| 1248 | + const dialogInputWrapper3 = await findByTestId('dialog-input'); |
| 1249 | + const dialogInput3 = within(dialogInputWrapper3).getByRole('textbox'); |
| 1250 | + fireEvent.change(dialogInput3, { target: { value: '999' } }); |
| 1251 | + |
| 1252 | + const cancelButton = document.querySelector('.t-dialog__cancel') as HTMLButtonElement; |
| 1253 | + fireEvent.click(cancelButton); |
| 1254 | + |
| 1255 | + await waitFor(() => { |
| 1256 | + const firstAmountInputAfterCancel = within(getByTestId('amount-0')).getByRole('textbox'); |
| 1257 | + expect(firstAmountInputAfterCancel).toHaveValue('100'); |
| 1258 | + }); |
| 1259 | + }); |
988 | 1260 | }); |
0 commit comments