reactjs - 尽管 DOM 行为正确,但在 React 测试库/Jest 中测试失败
问题描述
我对 Jest 和测试还很陌生,所以我正在使用 React、React 测试库和 Jest 制作一个应用程序来提高我的技能。
我的一项测试失败了,我不知道为什么。这是我的测试代码:
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
// using UrlShortener since the state has been lifted up for UrlList
import UrlShortener from '../../pages/UrlShortener/UrlShortener'
...
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
userEvent.click(submitBtn);
const listButton = screen.getByText('Link History');
userEvent.click(listButton);
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
让我感到困惑的是,我可以从失败的测试中看到该列表在日志中有 4 个元素长,但由于某种原因,它没有在 expect() 函数中被拾取。这是日志给我的内容(它清楚地显示了列表中的 4 个元素):
expect(received).toHaveLength(expected)
Expected length: 4
Received length: 3
Received array: [<p>Visits: 2</p>, <p>Visits: 1</p>, <p>Visits: 5</p>]
...
<div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/123"
>
test1
</a>
</p>
<p>
Visits:
2
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/456"
>
test2
</a>
</p>
<p>
Visits:
1
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/789"
>
test3
</a>
</p>
<p>
Visits:
5
</p>
</div>
<div
class="sc-iqHYmW gBcZyO"
>
<p>
<a
href="http://www.baseUrl.com/shorten/123"
>
test4
</a>
</p>
<p>
Visits:
9
</p>
</div>
</div>
DOM 怎么可能在日志中按预期运行,但在实际测试中却失败了?
更新:
我正在添加更多信息,所以很明显我在做什么。基本上,我已经将状态从子组件 ( UrlList
) 提升到父组件 (),UrlShortener
以便我可以将状态更新器函数向下传递给兄弟组件 ( UrlBar
)。UrlShortener 对后端进行 axios 调用,然后将 URL 列表传递给UrlList
组件。当您单击UrlBar
组件中的提交按钮时,它会重新运行 axios 调用并使用添加的新 URL 更新列表。
父组件:
import { useEffect, useState } from 'react';
import { SectionPage, BackButton, PageTitle } from './style';
import axios from 'axios';
import UrlBar from '../../components/UrlBar/UrlBar';
import UrlList from '../../components/UrlList/UrlList';
import { Url } from '../../types/types';
const UrlShortener = () => {
const [urls, setUrls] = useState<Url[] | []>([]);
const getUrls = () => {
axios
.get('https://fullstack-demos.herokuapp.com/shorten/urls/all')
.then((res) => setUrls(res.data));
};
useEffect(() => {
getUrls();
}, []);
return (
<SectionPage>
<BackButton href='/'>Go Back</BackButton>
<PageTitle>URL Shortener</PageTitle>
<UrlBar getUrls={getUrls} />
<UrlList urls={urls} />
</SectionPage>
);
};
export default UrlShortener;
孩子们:
import React, { useState } from 'react';
import {
ComponentWrapper,
Subtitle,
Triangle,
LinksContainer,
LinkGroup,
} from './style';
import { Url } from '../../types/types';
interface IProps {
urls: Url[] | [];
}
const UrlList: React.FC<IProps> = ({ urls }) => {
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen((prevState) => !prevState);
};
return (
<ComponentWrapper>
<Subtitle onClick={handleClick}>
Link History <Triangle>{open ? '▼' : '▲'}</Triangle>
</Subtitle>
<LinksContainer>
<div>
{open &&
urls.map(({ urlId, shortUrl, urlName, visits }: Url) => (
<LinkGroup key={urlId}>
<p>
<a href={shortUrl}>{urlName}</a>
</p>
<p>Visits: {visits}</p>
</LinkGroup>
))}
</div>
</LinksContainer>
</ComponentWrapper>
);
};
export default UrlList;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { UrlInput, NameInput, UrlButton } from './style';
import { validateUrl } from '../../utils/utils';
interface IProps {
getUrls: () => void;
}
const UrlBar: React.FC<IProps> = ({ getUrls }) => {
const [urlInput, setUrlInput] = useState('');
const [nameInput, setNameInput] = useState('');
const [error, setError] = useState<boolean | string>(false);
useEffect(() => {
// Cleanup fixes React testing error: "Can't perform a React state update on an unmounted component"
return () => {
setUrlInput('');
};
}, []);
const handleUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setUrlInput(e.target.value);
};
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setNameInput(e.target.value);
};
const handleSubmit = async (e: React.SyntheticEvent) => {
e.preventDefault();
if (!nameInput) {
setError('Please name your URL');
} else if (!validateUrl(urlInput)) {
setError('Invalid Input');
} else {
setError(false);
await axios.post('https://fullstack-demos.herokuapp.com/shorten', {
longUrl: urlInput,
urlName: nameInput,
});
setUrlInput('');
setNameInput('');
getUrls();
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<NameInput
type='text'
name='nameInput'
id='nameInput'
placeholder='Name your URL...'
maxLength={20}
onChange={handleNameChange}
value={nameInput}
/>
<UrlInput
type='text'
name='urlInput'
id='urlInput'
placeholder='Enter URL here...'
onChange={handleUrlChange}
value={urlInput}
/>
<UrlButton name='button' type='submit'>
Shorten!
</UrlButton>
{error && <label htmlFor='urlInput'>{error}</label>}
</form>
</div>
);
};
export default UrlBar;
解决方案
所以在努力让我的测试通过另一个组件之后,我终于想出了如何让这个通过。显然我只需要添加更多的 waitFor() 和 await 语句来捕获我的组件中发生的一些异步内容。如果我说我理解为什么这可以解决我的问题,那我是在撒谎,但现在我知道,如果我的测试失败,即使我可以在 JEST DOM 中看到正确的结果,这可能与缺少 waitFor / awaits 有关。
test('URL list displays valid URL from input bar', async () => {
const passingText = 'http://www.google.com';
const testText = 'test4';
render(<UrlShortener />);
const urlInput = screen.getByPlaceholderText('Enter URL here...');
const nameInput = screen.getByPlaceholderText('Name your URL...');
const submitBtn = screen.getByRole('button', { name: 'Shorten!' });
userEvent.type(urlInput, passingText);
userEvent.type(nameInput, testText);
await waitFor(() => userEvent.click(submitBtn));
const listButton = await screen.findByText('Link History');
await waitFor(() => userEvent.click(listButton));
const list = await screen.findAllByText(/visits/i);
await waitFor(() => expect(list).toHaveLength(4));
});
});
推荐阅读
- javascript - 我们如何在 codeceptjs.config.js 文件中传递动态目标 url?
- typescript - 如何以角度从一个组件导航到另一个组件的特定分区?
- mysql - MySQLi php 5.3 NULL 值自动转换为不可为空字段的字符串
- sql - UNION 后如何删除空结果
- java - 嵌套排球请求
- c# - (文本框)发件人与文本框1
- postgresql - 如何使用带有云融合的 Fixer.io API 来转换货币字段(从欧元到美元)并将其集成以将数据从 postgres 发送到 bigquery?
- odoo - 多家公司的 Oodo / OPEN ERP 11 SAAS 会计
- rest - 不同的身份验证方法会导致不同的访问级别吗?
- typescript - 具有不同角色的领域驱动设计