rhf-form-field.test.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. import { describe, expect, it, vi } from 'vitest';
  2. import { fireEvent, render, screen, waitFor } from '@testing-library/react';
  3. import { FormProvider } from 'react-hook-form';
  4. import { Form, Input, InputNumber, Switch } from 'antd';
  5. import { z } from 'zod';
  6. import { FormField, useZodForm } from '@/components/form/rhf';
  7. const GB = 1024 * 1024 * 1024;
  8. const Schema = z.object({
  9. email: z.string().min(1, 'email-required'),
  10. enabled: z.boolean(),
  11. slug: z.string(),
  12. bytes: z.number(),
  13. });
  14. type Values = z.infer<typeof Schema>;
  15. function Harness({ onSubmit }: { onSubmit: (values: Values) => void }) {
  16. const methods = useZodForm(Schema, {
  17. defaultValues: { email: '', enabled: false, slug: '', bytes: 2 * GB },
  18. });
  19. return (
  20. <FormProvider {...methods}>
  21. <Form layout="vertical">
  22. <FormField name="email" label="Email">
  23. <Input aria-label="email" />
  24. </FormField>
  25. <FormField name="enabled" label="Enabled" valueProp="checked">
  26. <Switch aria-label="enabled" />
  27. </FormField>
  28. <FormField name="slug" label="Slug" transform={{ output: (v) => String(v).toLowerCase() }}>
  29. <Input aria-label="slug" />
  30. </FormField>
  31. <FormField
  32. name="bytes"
  33. label="Traffic"
  34. transform={{
  35. input: (v) => (typeof v === 'number' ? v / GB : v),
  36. output: (v) => (typeof v === 'number' ? v * GB : 0),
  37. }}
  38. >
  39. <InputNumber aria-label="bytes" />
  40. </FormField>
  41. <button type="button" onClick={methods.handleSubmit(onSubmit)}>
  42. Save
  43. </button>
  44. </Form>
  45. </FormProvider>
  46. );
  47. }
  48. describe('FormField', () => {
  49. it('submits values normalized from the Ant Design inputs', async () => {
  50. const onSubmit = vi.fn();
  51. render(<Harness onSubmit={onSubmit} />);
  52. fireEvent.change(screen.getByLabelText('email'), { target: { value: '[email protected]' } });
  53. fireEvent.click(screen.getByLabelText('enabled'));
  54. fireEvent.change(screen.getByLabelText('slug'), { target: { value: 'HELLO' } });
  55. fireEvent.click(screen.getByText('Save'));
  56. await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
  57. expect(onSubmit.mock.calls[0][0]).toMatchObject({
  58. email: '[email protected]',
  59. enabled: true,
  60. slug: 'hello',
  61. bytes: 2 * GB,
  62. });
  63. });
  64. it('applies the transform output before storing the value', async () => {
  65. const onSubmit = vi.fn();
  66. render(<Harness onSubmit={onSubmit} />);
  67. fireEvent.change(screen.getByLabelText('email'), { target: { value: 'x' } });
  68. const bytes = screen.getByLabelText('bytes');
  69. fireEvent.change(bytes, { target: { value: '5' } });
  70. fireEvent.blur(bytes);
  71. fireEvent.click(screen.getByText('Save'));
  72. await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1));
  73. expect(onSubmit.mock.calls[0][0].bytes).toBe(5 * GB);
  74. });
  75. it('surfaces a resolver validation error as help text and blocks submit', async () => {
  76. const onSubmit = vi.fn();
  77. render(<Harness onSubmit={onSubmit} />);
  78. fireEvent.click(screen.getByText('Save'));
  79. await screen.findByText('email-required');
  80. expect(onSubmit).not.toHaveBeenCalled();
  81. });
  82. });