我正在学习 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 特定组件会重新渲染, 也许这就是它无法捕获新值的原因?
我看不到太多解释这一点的文档,我现在害怕在这里发布问题,任何建议将不胜感激。
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.