React 测试库表单验证 onChange 未按预期工作

问题描述 投票:0回答:1

我正在学习 React 测试库,并努力理解如何使用 onchange 验证错误消息。 我什至简化了它,以便在两个输入都有效之前禁用该表单。 它在手动测试期间工作得很好,但由于某种原因在 React 测试库中,它无法工作。

要么是我遗漏了一些没有记录的基本内容,因为它太明显了,要么是某处存在错误。

输入在更改事件期间未正确验证,甚至在我单击提交按钮时也未正确验证。

当表格明显无效时,它表示其有效。

我有两个输入:

第一个是必需的,最小长度为 5, 第二个输入的最小和最大长度为 3,所以正好是 3,

这里是显示各种测试的代码,这些测试应该很容易通过,但由于某些奇怪的原因失败了:

我尝试了受控和非受控形式,但我很难理解为什么会发生这种情况。我感觉好像有什么东西就在我面前,但我却想念它。

我正在使用 Vitest,以及 jest-dom 和 RTL。

export default function ErrorMessage(props) {
  return (
    <span data-testid={props.testId} ref={props.ref} style={{color: 'red'}}>{props.text}</span>
  )
}


export default function ControlledForm() {

  console.log('Component form rendered!');

    const [inputOne, setInputOne] = useState({
        value: "",
        isValid: false,
        errorMessage: ""
    });

    const [inputTwo, setInputTwo] = useState({
      value: "",
      isValid: false,
      errorMessage: ""
    });

    const isValid = inputOne.isValid && inputTwo.isValid;

    console.log('Form isValid: ', isValid);

    function handleSubmit(e) {
      e.preventDefault();
      console.log('Form Submitted!', e.target.elements);
    }

    return (
      <div>
        <h3>Controlled Form</h3>
        <p>
          In this component, all state for inputs is in the top component!
        </p>
        <form 
          action="" 
          method='POST' 
          onSubmit={e => handleSubmit(e)}
          style={{
            display: 'flex',
            flexDirection: 'column',
            gap: '1em'
          }}
        >
          <div>
            <input 
              className='practice-inputs'
              type="text" 
              name="inputOne" 
              placeholder='Input One:'
              value={inputOne.value} 
              minLength={5}
              maxLength={7}
              required
              onChange={(e) => {
                //Validate and check input on change here
                  console.log('Input 1 Validity on change: ', e.target.validity);
                  const isValid = e.target.checkValidity();
                  console.log('Is valid: ',isValid);
                  setInputOne({
                    value: e.target.value,
                    isValid: isValid,
                    errorMessage: (!isValid) ? 'Error happenned' : ''
                  })
              }}
            />
            <ErrorMessage testId='cErrorMessage1' text={inputOne.errorMessage} />
          </div>
          
          <div>
            <input 
              className='practice-inputs'
              type="text" 
              name="inputTwo" 
              placeholder='Input Two: '
              value={inputTwo.value} 
              minLength={3}
              maxLength={3}
              required
              onChange={(e) => {
                //Validate and check input on change here
                  console.log('Input 2 Validity on change: ', e.target.validity);
                  setInputTwo({
                    value: e.target.value,
                    isValid: e.target.checkValidity(),
                    errorMessage: (!e.target.checkValidity()) ? 'Error happenned' : ''
                  })
              }}
            />
            <ErrorMessage testId='cErrorMessage2' text={inputTwo.errorMessage} />
          </div>
          
          <SubmitButton disabled={!isValid} text='Submit' />

        </form>
      </div>
    )
}

测试:

describe('Controlled Form basic tests', () => {

    let inputOne; let inputTwo; let submitButton; let user;

    beforeEach(() => {
        render(<ControlledForm />)
        inputOne = screen.getByPlaceholderText(/Input One:/);
        inputTwo = screen.getByPlaceholderText(/Input Two:/);
        submitButton = screen.getByText(/submit/i);
    })

    it('Renders', () => {

    })

    it('Should be able to show an input by placeholder text', () => {
        expect(inputOne).toBeInTheDocument()
    })

    /**
     * Note, when looking for something that doesn't exist, I should use queryby
     */
    it('Should not be able to show inputs by an incorrect placeholder', () => {
        expect(screen.queryByPlaceholderText('placeholder that doesnt exist')).not.toBeInTheDocument()
    })

    /**
     * Here I am learning how to interact with inputs,
     * I need to wait for the type to finish, as it can take a bit of time to type the input,
     * Otherwise it would go to the next line without waiting and the input takes a bit of time
     * to be there
     */
    it('Just shows value of the input', async () => {
        await userEvent.type(inputOne, 'abc');
        expect(inputOne).toHaveValue('abc');
    })

    /**
     * ok
     */
    it('Should have the error component in the document', async () => {
        await userEvent.type(inputOne, 'abc');
        expect(screen.getByTestId('cErrorMessage1')).toBeInTheDocument();
    })


    //Okay
    it('Should have css style ?', async () => {
        await userEvent.type(inputOne, 'abc');
        expect(screen.getByTestId('cErrorMessage1')).toHaveStyle('color: rgb(255,0,0)');    
    })

    //Okay
    it('Expect submit button to be in the document', async () => {
        expect(submitButton).toBeInTheDocument();
    })

    //Okay
    it('Its submit button should be disabled', () => {
        expect(submitButton).toBeDisabled();
    })

    /**
     * Why is this test failing ??
     */
    it('Expect submit button to be disabled when inputs are not valid', async () => {
        await userEvent.type(inputOne, 'a');
        await userEvent.type(inputTwo, 'a');
        expect(submitButton).toBeDisabled();
    })

    it('Should be valid', async () => {
        await userEvent.type(inputTwo, 'abc');
        expect(inputTwo).toBeValid()
    })

    //This is invalid but for some reason fails, because it's valid ?
    it('Should be valid', async () => {
        await userEvent.type(inputTwo, 'ab');
        expect(inputTwo).toBeInvalid()
    })



/**
     * Fails
     */
    it('Should be invalid', async () => {
        const user = userEvent.setup();
        await user.type(inputOne, 'abc');
        expect(inputOne).toBeInvalid();
    })

    /**
     * Fails
     * Error text does not have value,
     * But It clearly can be seen on browser
     */
    it('Should display error message', async () => {
        const user = userEvent.setup();
        await user.type(inputOne, 'abc');
        expect(screen.getByTestId('cErrorMessage1')).toHaveValue(/error/i);
    })

在手动测试中它是完美的:

我还通过手动测试将输出记录在控制台中, 它工作完美:

我查看了这里的文档以获取匹配器: https://github.com/testing-library/jest-dom?tab=readme-ov-file#tobeinvalid

它清楚地表明检查有效性是否返回 false,它显然确实如此

这是我的手动浏览器测试,显示它显然有效:

这是另一个接收空值的失败测试:

再次在手动测试中可以看到错误消息:

这里它在手动测试中完美运行:

我正在努力真正理解这是如何运作的, 因为 onChange 特定组件会重新渲染, 也许这就是它无法捕获新值的原因?

我看不到太多解释这一点的文档,我现在害怕在这里发布问题,任何建议将不胜感激。

reactjs validation react-testing-library onchange
1个回答
0
投票
如果输入值是由 JavaScript 而不是用户交互设置的,则

HTMLInputElement.checkValidity()
将始终返回
true
。请参阅相关问题:

这就是与输入验证相关的测试用例失败的原因。

一种解决方案是使用 Hyperform 接管输入验证:

它在 JavaScript 中完整实现了 HTML5 表单验证 API,替换了浏览器的本机方法(如果它们甚至实现了……),并通过自定义事件和挂钩丰富了您的工具箱。

jest.setup.js

import hyperform from 'hyperform';

globalThis.HTMLInputElement.prototype.checkValidity = function () {
  return hyperform.checkValidity(this);
};

jest.config.js

module.exports = {
  testEnvironment: 'jsdom',
  setupFiles: ['<rootDir>/jest.setup.js'],
};

为了演示,我简化了您的代码:

index.tsx

import React, { useState } from 'react';

const ErrorMessage = (props) => (
  <span data-testid={props.testId} style={{ color: 'red' }}>
    {props.text}
  </span>
);

export default function ControlledForm() {
  const [inputOne, setInputOne] = useState({ value: '', isValid: false, errorMessage: '' });
  const [inputTwo, setInputTwo] = useState({ value: '', isValid: false, errorMessage: '' });
  const isValid = inputOne.isValid && inputTwo.isValid;

  return (
    <form action="" method="POST" style={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
      <div>
        <input
          className="practice-inputs"
          type="text"
          name="inputOne"
          placeholder="Input One:"
          value={inputOne.value}
          minLength={5}
          maxLength={7}
          required
          onChange={(e) => {
            const isValid = e.target.checkValidity();
            setInputOne({
              value: e.target.value,
              isValid: isValid,
              errorMessage: !isValid ? 'Error happenned' : '',
            });
          }}
        />
        <ErrorMessage testId="cErrorMessage1" text={inputOne.errorMessage} />
      </div>

      <div>
        <input
          className="practice-inputs"
          type="text"
          name="inputTwo"
          placeholder="Input Two: "
          value={inputTwo.value}
          minLength={3}
          maxLength={3}
          required
          onChange={(e) => {
            setInputTwo({
              value: e.target.value,
              isValid: e.target.checkValidity(),
              errorMessage: !e.target.checkValidity() ? 'Error happenned' : '',
            });
          }}
        />
        <ErrorMessage testId="cErrorMessage2" text={inputTwo.errorMessage} />
      </div>

      <button disabled={!isValid} type="submit">
        Submit
      </button>
    </form>
  );
}

index.test.tsx

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import React from 'react';
import ControlledForm from '.';

describe('Controlled Form basic tests', () => {
  let inputOne: HTMLInputElement;
  let inputTwo: HTMLInputElement;
  let submitButton: HTMLButtonElement;

  beforeEach(() => {
    render(<ControlledForm />);
    inputOne = screen.getByPlaceholderText(/Input One:/);
    inputTwo = screen.getByPlaceholderText(/Input Two:/);
    submitButton = screen.getByText(/submit/i);
  });

  it('Expect submit button to be disabled when inputs are not valid', async () => {
    await userEvent.type(inputOne, 'a');
    await userEvent.type(inputTwo, 'a');
    expect(submitButton).toBeDisabled();
  });

  it('Should be valid', async () => {
    await userEvent.type(inputTwo, 'abc');
    expect(inputTwo).toBeValid();
  });

  it('Should be valid', async () => {
    await userEvent.type(inputTwo, 'ab');
    expect(inputTwo).toBeInvalid();
  });

  it('Should be invalid', async () => {
    const user = userEvent.setup();
    await user.type(inputOne, 'abc');
    expect(inputOne).toBeInvalid();
  });

  it('Should display error message', async () => {
    const user = userEvent.setup();
    await user.type(inputOne, 'abc');
    expect(screen.getByTestId('cErrorMessage1')).toHaveTextContent('Error happenned');
  });
});

测试结果:

 PASS  stackoverflow/78199219/index.test.tsx
  Controlled Form basic tests
    √ Expect submit button to be disabled when inputs are not valid (204 ms)                                                                                                                                                                                 
    √ Should be valid (108 ms)                                                                                                                                                                                                                               
    √ Should be valid (94 ms)                                                                                                                                                                                                                                
    √ Should be invalid (94 ms)                                                                                                                                                                                                                              
    √ Should display error message (95 ms)                                                                                                                                                                                                                   
                                                                                                                                                                                                                                                             
Test Suites: 1 passed, 1 total                                                                                                                                                                                                                               
Tests:       5 passed, 5 total                                                                                                                                                                                                                               
Snapshots:   0 total
Time:        1.888 s, estimated 2 s
Ran all test suites related to changed files.
© www.soinside.com 2019 - 2024. All rights reserved.