Procházet zdrojové kódy

fix(area-management-ui): 修复AreaSelectForm组件验证错误显示问题

- 重构AreaSelectForm组件使用Controller管理表单字段
- 修复测试中mock AreaSelect组件的required属性问题
- 添加act()包裹表单提交操作避免React警告
- 添加完整的集成测试覆盖验证错误场景

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
yourname před 2 týdny
rodič
revize
9e8b6533da

+ 94 - 52
packages/area-management-ui/src/components/AreaSelectForm.tsx

@@ -1,4 +1,4 @@
-import { Controller, useFormContext, Control, UseFormSetValue, FieldValues, Path } from 'react-hook-form';
+import { Controller, useFormContext, Control, FieldValues, Path } from 'react-hook-form';
 import { AreaSelect } from './AreaSelect';
 import { FormDescription, FormLabel } from '@d8d/shared-ui-components/components/ui/form';
 
@@ -12,7 +12,6 @@ interface AreaSelectFormProps<T extends FieldValues = FieldValues> {
   disabled?: boolean;
   className?: string;
   control?: Control<T>;
-  setValue?: UseFormSetValue<T>;
 }
 
 export const AreaSelectForm = <T extends FieldValues = FieldValues>({
@@ -24,25 +23,17 @@ export const AreaSelectForm = <T extends FieldValues = FieldValues>({
   required = false,
   disabled = false,
   className,
-  control: propControl,
-  setValue: propSetValue
+  control: propControl
 }: AreaSelectFormProps<T>) => {
-  // 使用传入的 control 和 setValue,或者从上下文中获取
+  // 使用传入的 control,或者从上下文中获取
   const formContext = useFormContext<T>();
   const control = propControl || formContext?.control;
-  const setValue = propSetValue || formContext?.setValue;
-  const watch = formContext?.watch;
 
-  if (!control || !setValue || !watch) {
-    console.error('AreaSelectForm: 缺少 react-hook-form 上下文或 props');
+  if (!control) {
+    console.error('AreaSelectForm: 缺少 react-hook-form 上下文或 control prop');
     return null;
   }
 
-  // 监听表单字段值
-  const provinceValue = watch(provinceName as any);
-  const cityValue = watch(cityName as any);
-  const districtValue = districtName ? watch(districtName as any) : undefined;
-
   return (
     <div className={className}>
       {label && (
@@ -51,46 +42,97 @@ export const AreaSelectForm = <T extends FieldValues = FieldValues>({
         </FormLabel>
       )}
 
-      <AreaSelect
-        value={{
-          provinceId: provinceValue ? Number(provinceValue) : undefined,
-          cityId: cityValue ? Number(cityValue) : undefined,
-          districtId: districtValue ? Number(districtValue) : undefined
-        }}
-        onChange={(value) => {
-          console.debug('AreaSelectForm onChange:', {
-            provinceName,
-            cityName,
-            districtName,
-            value,
-            provinceValue: value.provinceId?.toString() || '',
-            cityValue: value.cityId?.toString() || '',
-            districtValue: value.districtId?.toString() || ''
-          });
-          // 更新所有相关字段
-          setValue(provinceName, (value.provinceId?.toString() || '') as any, { shouldValidate: true });
-          setValue(cityName, (value.cityId?.toString() || '') as any, { shouldValidate: true });
-          if (districtName) {
-            setValue(districtName, (value.districtId?.toString() || '') as any, { shouldValidate: true });
-          }
-        }}
-        disabled={disabled}
-        required={required}
-      />
+      {/* 使用Controller来管理省份字段 */}
+      <Controller
+        name={provinceName as any}
+        control={control}
+        render={({ field: provinceField, fieldState: provinceFieldState }) => (
+          <div>
+            {/* 使用Controller来管理城市字段 */}
+            <Controller
+              name={cityName as any}
+              control={control}
+              render={({ field: cityField, fieldState: cityFieldState }) => (
+                <div>
+                  {/* 使用Controller来管理区县字段(如果提供) */}
+                  {districtName ? (
+                    <Controller
+                      name={districtName as any}
+                      control={control}
+                      render={({ field: districtField, fieldState: districtFieldState }) => (
+                        <AreaSelect
+                          value={{
+                            provinceId: provinceField.value ? Number(provinceField.value) : undefined,
+                            cityId: cityField.value ? Number(cityField.value) : undefined,
+                            districtId: districtField.value ? Number(districtField.value) : undefined
+                          }}
+                          onChange={(value) => {
+                            console.debug('AreaSelectForm onChange:', {
+                              provinceName,
+                              cityName,
+                              districtName,
+                              value,
+                              provinceValue: value.provinceId?.toString() || '',
+                              cityValue: value.cityId?.toString() || '',
+                              districtValue: value.districtId?.toString() || ''
+                            });
 
-      {/* 显示省份验证错误 */}
-      {control?.getFieldState(provinceName as any).error && (
-        <div className="text-sm font-medium text-destructive mt-1">
-          {control.getFieldState(provinceName as any).error?.message}
-        </div>
-      )}
+                            // 更新所有字段
+                            provinceField.onChange(value.provinceId?.toString() || '');
+                            cityField.onChange(value.cityId?.toString() || '');
+                            districtField.onChange(value.districtId?.toString() || '');
+                          }}
+                          disabled={disabled}
+                          required={required}
+                        />
+                      )}
+                    />
+                  ) : (
+                    <AreaSelect
+                      value={{
+                        provinceId: provinceField.value ? Number(provinceField.value) : undefined,
+                        cityId: cityField.value ? Number(cityField.value) : undefined,
+                        districtId: undefined
+                      }}
+                      onChange={(value) => {
+                        console.debug('AreaSelectForm onChange:', {
+                          provinceName,
+                          cityName,
+                          districtName,
+                          value,
+                          provinceValue: value.provinceId?.toString() || '',
+                          cityValue: value.cityId?.toString() || '',
+                          districtValue: value.districtId?.toString() || ''
+                        });
 
-      {/* 显示城市验证错误 */}
-      {control?.getFieldState(cityName as any).error && (
-        <div className="text-sm font-medium text-destructive mt-1">
-          {control.getFieldState(cityName as any).error?.message}
-        </div>
-      )}
+                        // 更新省份和城市字段
+                        provinceField.onChange(value.provinceId?.toString() || '');
+                        cityField.onChange(value.cityId?.toString() || '');
+                      }}
+                      disabled={disabled}
+                      required={required}
+                    />
+                  )}
+
+                  {/* 显示城市验证错误 */}
+                  {cityFieldState.error && (
+                    <div className="text-sm font-medium text-destructive mt-1">
+                      {cityFieldState.error.message}
+                    </div>
+                  )}
+                </div>
+              )}
+            />
+
+            {/* 显示省份验证错误 */}
+            {provinceFieldState.error && (
+              <div className="text-sm font-medium text-destructive mt-1">
+                {provinceFieldState.error.message}
+              </div>
+            )}
+          </div>
+        )}
+      />
 
       {description && (
         <FormDescription className="mt-2">

+ 309 - 0
packages/area-management-ui/tests/integration/area-select-form.integration.test.tsx

@@ -0,0 +1,309 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
+import { useForm, FormProvider } from 'react-hook-form';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { z } from 'zod';
+import { AreaSelectForm } from '../../src/components/AreaSelectForm';
+import { Button } from '@d8d/shared-ui-components/components/ui/button';
+import { Form } from '@d8d/shared-ui-components/components/ui/form';
+
+// 测试用的schema
+const TestSchema = z.object({
+  province: z.string().min(1, '省份不能为空'),
+  city: z.string().min(1, '城市不能为空'),
+  district: z.string().optional(),
+});
+
+type TestFormData = z.infer<typeof TestSchema>;
+
+// Mock AreaSelect 组件
+vi.mock('../../src/components/AreaSelect', () => ({
+  AreaSelect: vi.fn(({ value, onChange, disabled, required }) => {
+    return (
+      <div data-testid="area-select">
+        <select
+          data-testid="province-select"
+          value={value?.provinceId || ''}
+          onChange={(e) => {
+            const newValue = {
+              ...value,
+              provinceId: e.target.value ? Number(e.target.value) : undefined,
+            };
+            onChange(newValue);
+          }}
+          disabled={disabled}
+          // 移除 required 属性,让 react-hook-form 处理验证
+        >
+          <option value="">选择省份</option>
+          <option value="1">北京市</option>
+          <option value="2">上海市</option>
+        </select>
+        <select
+          data-testid="city-select"
+          value={value?.cityId || ''}
+          onChange={(e) => {
+            const newValue = {
+              ...value,
+              cityId: e.target.value ? Number(e.target.value) : undefined,
+            };
+            onChange(newValue);
+          }}
+          disabled={disabled}
+          // 移除 required 属性,让 react-hook-form 处理验证
+        >
+          <option value="">选择城市</option>
+          <option value="3">北京市</option>
+          <option value="4">上海市</option>
+        </select>
+        <select
+          data-testid="district-select"
+          value={value?.districtId || ''}
+          onChange={(e) => {
+            const newValue = {
+              ...value,
+              districtId: e.target.value ? Number(e.target.value) : undefined,
+            };
+            onChange(newValue);
+          }}
+          disabled={disabled}
+        >
+          <option value="">选择区县</option>
+          <option value="5">东城区</option>
+          <option value="6">黄浦区</option>
+        </select>
+      </div>
+    );
+  }),
+}));
+
+// 测试组件
+const TestForm = () => {
+  const form = useForm<TestFormData>({
+    resolver: zodResolver(TestSchema),
+    defaultValues: {
+      province: '',
+      city: '',
+      district: '',
+    },
+    mode: 'onSubmit', // 使用onSubmit模式,这是默认值
+  });
+
+  const onSubmit = vi.fn();
+
+  return (
+    <FormProvider {...form}>
+      <Form {...form}>
+        <form onSubmit={form.handleSubmit(onSubmit)}>
+          <AreaSelectForm<TestFormData>
+            provinceName="province"
+            cityName="city"
+            districtName="district"
+            label="地区选择"
+            required={true}
+            control={form.control}
+          />
+          <Button type="submit" data-testid="submit-button">
+            提交
+          </Button>
+        </form>
+      </Form>
+    </FormProvider>
+  );
+};
+
+describe('AreaSelectForm 集成测试', () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+  });
+
+  it('应该正确渲染AreaSelectForm组件', () => {
+    render(<TestForm />);
+
+    expect(screen.getByText('地区选择')).toBeInTheDocument();
+    // 检查是否包含必填标记(星号)
+    const label = screen.getByText('地区选择');
+    expect(label.parentElement).toContainHTML('*');
+    expect(screen.getByTestId('area-select')).toBeInTheDocument();
+  });
+
+  it('应该显示省份和城市的验证错误当表单提交时', async () => {
+    let formRef: any;
+    const TestFormWithRef = () => {
+      const form = useForm<TestFormData>({
+        resolver: zodResolver(TestSchema),
+        defaultValues: {
+          province: '',
+          city: '',
+          district: '',
+        },
+        mode: 'onSubmit',
+      });
+
+      formRef = form;
+
+      const onSubmit = vi.fn();
+
+      return (
+        <FormProvider {...form}>
+          <Form {...form}>
+            <form onSubmit={form.handleSubmit(onSubmit)}>
+              <AreaSelectForm<TestFormData>
+                provinceName="province"
+                cityName="city"
+                districtName="district"
+                label="地区选择"
+                required={true}
+                control={form.control}
+              />
+              <Button type="submit" data-testid="submit-button">
+                提交
+              </Button>
+            </form>
+          </Form>
+        </FormProvider>
+      );
+    };
+
+    render(<TestFormWithRef />);
+
+    // 初始状态不应该有验证错误
+    expect(screen.queryByText('省份不能为空')).not.toBeInTheDocument();
+    expect(screen.queryByText('城市不能为空')).not.toBeInTheDocument();
+
+    // 提交表单(不选择任何省份和城市)
+    const submitButton = screen.getByTestId('submit-button');
+    await act(async () => {
+      fireEvent.click(submitButton);
+    });
+
+    // 等待一下,让验证完成
+    await new Promise(resolve => setTimeout(resolve, 100));
+
+    // 等待验证错误显示
+    await waitFor(() => {
+      expect(screen.getByText('省份不能为空')).toBeInTheDocument();
+      expect(screen.getByText('城市不能为空')).toBeInTheDocument();
+    }, { timeout: 3000 });
+  });
+
+  it('应该正确更新表单字段值当选择省份和城市时', async () => {
+    render(<TestForm />);
+
+    // 选择省份
+    const provinceSelect = screen.getByTestId('province-select');
+    fireEvent.change(provinceSelect, { target: { value: '1' } });
+
+    // 选择城市
+    const citySelect = screen.getByTestId('city-select');
+    fireEvent.change(citySelect, { target: { value: '3' } });
+
+    // 提交表单
+    const submitButton = screen.getByTestId('submit-button');
+    fireEvent.click(submitButton);
+
+    // 不应该有验证错误
+    await waitFor(() => {
+      expect(screen.queryByText('省份不能为空')).not.toBeInTheDocument();
+      expect(screen.queryByText('城市不能为空')).not.toBeInTheDocument();
+    });
+  });
+
+  it('应该显示验证错误当只选择省份不选择城市时', async () => {
+    render(<TestForm />);
+
+    // 只选择省份
+    const provinceSelect = screen.getByTestId('province-select');
+    fireEvent.change(provinceSelect, { target: { value: '1' } });
+
+    // 提交表单
+    const submitButton = screen.getByTestId('submit-button');
+    fireEvent.click(submitButton);
+
+    // 应该只有城市验证错误
+    await waitFor(() => {
+      expect(screen.queryByText('省份不能为空')).not.toBeInTheDocument();
+      expect(screen.getByText('城市不能为空')).toBeInTheDocument();
+    });
+  });
+
+  it('应该显示验证错误当只选择城市不选择省份时', async () => {
+    render(<TestForm />);
+
+    // 只选择城市
+    const citySelect = screen.getByTestId('city-select');
+    fireEvent.change(citySelect, { target: { value: '3' } });
+
+    // 提交表单
+    const submitButton = screen.getByTestId('submit-button');
+    fireEvent.click(submitButton);
+
+    // 应该只有省份验证错误
+    await waitFor(() => {
+      expect(screen.getByText('省份不能为空')).toBeInTheDocument();
+      expect(screen.queryByText('城市不能为空')).not.toBeInTheDocument();
+    });
+  });
+
+  it('应该正确处理区县字段(可选)', async () => {
+    render(<TestForm />);
+
+    // 选择省份和城市
+    const provinceSelect = screen.getByTestId('province-select');
+    const citySelect = screen.getByTestId('city-select');
+    fireEvent.change(provinceSelect, { target: { value: '1' } });
+    fireEvent.change(citySelect, { target: { value: '3' } });
+
+    // 选择区县(可选)
+    const districtSelect = screen.getByTestId('district-select');
+    fireEvent.change(districtSelect, { target: { value: '5' } });
+
+    // 提交表单
+    const submitButton = screen.getByTestId('submit-button');
+    fireEvent.click(submitButton);
+
+    // 不应该有验证错误
+    await waitFor(() => {
+      expect(screen.queryByText('省份不能为空')).not.toBeInTheDocument();
+      expect(screen.queryByText('城市不能为空')).not.toBeInTheDocument();
+    });
+  });
+
+  it('应该支持禁用状态', () => {
+    const DisabledTestForm = () => {
+      const form = useForm<TestFormData>({
+        resolver: zodResolver(TestSchema),
+        defaultValues: {
+          province: '',
+          city: '',
+          district: '',
+        },
+      });
+
+      return (
+        <FormProvider {...form}>
+          <Form {...form}>
+            <AreaSelectForm<TestFormData>
+              provinceName="province"
+              cityName="city"
+              districtName="district"
+              label="地区选择"
+              required={true}
+              disabled={true}
+              control={form.control}
+            />
+          </Form>
+        </FormProvider>
+      );
+    };
+
+    render(<DisabledTestForm />);
+
+    const provinceSelect = screen.getByTestId('province-select');
+    const citySelect = screen.getByTestId('city-select');
+    const districtSelect = screen.getByTestId('district-select');
+
+    expect(provinceSelect).toBeDisabled();
+    expect(citySelect).toBeDisabled();
+    expect(districtSelect).toBeDisabled();
+  });
+});