声明:本篇测试是react+jest+@testing-library+react-router-dom+react-redux,如果react有疑问的,可以google,资源Supernumerary
上一节我们讲完了input的输入框,希望大家亲自去尝试尝试,现在继续!
下拉列表
下拉列表原本是select/otpion的,但是由于我们这里使用到的是mui框架,因此我们发现它的下拉框其实就是TextField,但是需要加属性select,因此我们对于input的封装修改一下:
import React from 'react';
import type { TextFieldProps } from '@mui/material/TextField';
import type { ReactElement } from 'react';
import TextField from '@mui/material/TextField';
// 类型写法一
type ZLTextProps = TextFieldProps & {
startAdornment?: ReactElement,
endAdornment?: ReactElement,
inputType?: string
}
// 类型写法二
type AA<Type = object> = Type & {
startAdornment?: ReactElement,
endAdornment?: ReactElement
}
type ZLTextProps2 = AA<TextFieldProps>
const ZlInput = (props: ZLTextProps): ReactElement => {
const {
name,
label,
placeholder = 'please Input',
inputType = '',
children,
...rest
} = props;
let show_input = null;
switch(inputType) {
case 'select':
show_input = (
<TextField
id={name}
label={label}
select
placeholder={placeholder}
{...rest}
> {children} </TextField>
)
break;
default:
}
return (
<>
{
inputType ? show_input : (
<TextField
id={name}
label={label}
placeholder={placeholder}
{...rest}
/>
)
}
</>
)
}
export default ZlInput;
TextField可以适用于输入框和选择框,因此我们一起封装。
然后新建一个page/detail/Com/select.tsx的文件,相当于我们的业务逻辑页面
import ZlInput from "src/component/common/Input";
import MenuItem from '@mui/material/MenuItem';
const Z_select = () => {
const currencies = [
{
value: 'apple',
label: 'apple',
},
{
value: 'banana',
label: 'banana',
},
{
value: 'pear',
label: 'pear',
},
{
value: 'water',
label: 'water',
},
];
return (
<>
<ZlInput
id='like'
label='like'
variant="standard"
data-testid='like'
inputType='select'
defaultValue="apple"
helperText="Please select your like"
>
{currencies.map((option) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</ZlInput>
</>
)
}
export default Z_select;
接着我们在page/detail/detail.tsx中添加内容
...
import Z_select from './Com/select';
const Detail = () => {
return (
<>
...
<Z_select />
</>
)
}
export default Detail;
如果您不知道为啥页面结构是这样,建议回顾一下我当时项目搭建的过程:https://www.toutiao.com/article/7250691387551826432/
由于我们的测试都是写在input.test.tsx中,因此对于一些公共的方法,我们有必要先提取出来。所以新建文件__tests__/react/utils/index.ts(请注意这个ts,后续您可以直直接拿过来用的)。在这个文件里面我们把InputField方法拿过来了,并写了一个点击下拉框的方法:
import { screen } from "@testing-library/dom";
import { fireEvent,within } from "@testing-library/dom";
interface InputProps {
testId?: string,
value?: string
}
export const InputField = (props: InputProps): void => {
const { testId = '' , value } = props;
const el = screen.getByTestId(testId);
const input = el.getElementsByTagName('input')[0];
fireEvent.change(input,{
target: {
value: value
}
})
}
export const getElement = (
testId?: string,
selector?: string
): HTMLElement | null => {
if ( selector ) {
return document.querySelector(selector);
} else if ( testId ) {
return screen.getByTestId(testId)
}
return null;
}
export const getSelectValue = (props: InputProps) => {
const {
testId = '',
value
} = props;
// 先找到testId的节点
const dropDown = within(screen.getByTestId(testId));
// 再找到可以点击的节点,并点击
fireEvent.mouseDown(dropDown.getByRole('button'));
// 此时会出现下啦框
// 找到下拉列表的选项
const listbox = within(screen.getByRole('listbox'));
expect(listbox).not.toBeNull();
// 选择一个点击,这里选择和value一样的节点
const option = listbox.getByText(value as string);
fireEvent.click(option)
}
最后在测试文件__tests__/react/com/input.test.tsx
...
import { getSelectValue,InputField,getElement } from '../utils/index';
describe('test react com',() => {
...
it('test select',() => {
const Com = (
<Detail />
)
const container = render(Com);
getSelectValue({
testId: 'like',
value: 'pear'
})
expect(container).toMatchSnapshot('detail-input2')
})
})
注意事项:
- 页面中使用React.Fragment测试文件识别不了,我们可以使用<>尖括号替代
- 我们使用getTestById的时候,必须要有一个初始值
- 我们书写下拉框方法的时候是注意了细节的,即页面展示了什么,点击后,会发生什么都会按照我们ui页面真实反馈。大家在一边测试自己项目的时候,也可以打开自己的项目,仔细观察他们的html结构和属性,方便自己获取节点
- within方法是用来包裹一个节点,并将这个节点返回后具有和screen一样的查询方法
复选框checkbox
新建公共组件component/common/checkbox.tsx ,让复选框都一层封装
import * as React from 'react';
import Checkbox from '@mui/material/Checkbox';
interface Props {
checked?: boolean,
handleChange?: (e: React.ChangeEvent<HTMLInputElement>) => void,
}
export default function Zl_checkbox(props: Props) {
const {
checked = false,
handleChange,
...rest
} = props;
return (
<Checkbox
checked={checked}
onChange={handleChange}
inputProps={{ 'aria-label': 'controlled' }}
{...rest}
/>
);
}
新建功能性组件page/detail/Com/checkbox.tsx
import { useState } from "react";
import Zl_checkbox from "src/component/common/checkbox";
export default function Check_Box() {
const [checked,setCheck] = useState(false);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCheck(event.target.checked)
}
return (
<>
<Zl_checkbox
checked={checked}
handleChange={handleChange}
data-testid='like_apple'
/>
</>
)
}
我们给测试的__tests__/react/utils/index.ts新加一个方法,这个方法会把点击复选框之后,会将input的选中状态放回
...
export const changeCheckbox = (props: InputProps): boolean => {
const { testId = '' , value } = props;
const checkbox = screen.getByTestId(testId);
const input = checkbox.getElementsByTagName('input')[0];
fireEvent.click(input);
return input.checked
}
在测试文件__tests__/react/input.test.tsx多加一个测试
it('test checkbox',() => {
const Com = (
<Detail />
)
// 默认状态是false
const container = render(Com);
const checked = changeCheckbox({
testId: 'like_apple'
})
console.log(checked,'first') // true
const checked2 = changeCheckbox({
testId: 'like_apple'
})
console.log(checked2,'second') // false
})
注意事项:
- 当我们的字组件内容只有一个表单时,最好时通过dom选择器进行获取
- checkbox复选框同样需要是手控组件
- 仔细看,会发现,当我们没有测试点击这块的时候,覆盖率时不会覆盖到handleChange这个事件的,因此需要我们像上面那样,手动某一次操作=
- 注意change事件的类型写法:React.ChangeEvent
radioGroup
同前面类似,先创建一个文件component/common/radioGroup.tsx
import React , { useState} from 'react';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
interface Obj {
value: string,
label: string
}
interface Props {
id?: string,
dd: Obj[],
name?: string,
head: string,
value?: string,
handleChange: ( event: React.ChangeEvent<HTMLInputElement> ) => void
}
export default function RadioButtonsGroup(props: Props) {
const {
id = 'demo-controlled-radio-buttons-group',
dd,
name,
head,
value = '',
handleChange,
...rest
} = props;
return (
<FormControl>
<FormLabel id={id}>{head}</FormLabel>
<RadioGroup
aria-labelledby="demo-radio-buttons-group-label"
value={value}
name={name}
onChange={handleChange}
>
{
dd.map((item,i) => (
<FormControlLabel key={JSON.stringify(item)} value={item.value} control={<Radio />} label={item.label}/>
))
}
</RadioGroup>
</FormControl>
);
}
然后写一个测试组件page/detail/Com/radioGroup.tsx
import { useState } from "react";
import RadioButtonsGroup from "src/component/common/radioGroup";
const dd = [
{value: 'boy', label: 'boy'},
{value: 'girl', label: 'girl'}
]
export default function Zl_radioGroup() {
const [val,setVal] = useState('');
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
const isCheck = event.target.checked;
if ( isCheck ) {
setVal(value)
} else {
setVal('')
}
}
return (
<>
<RadioButtonsGroup
value={val}
head='gender'
dd={dd}
handleChange={handleChange}
data-testid='gender123'
/>
</>
)
}
接着放到detail页面中去
...
import Zl_radioGroup from './Com/radioGroup';
const Detail = () => {
return (
<>
...
<div>
<Zl_radioGroup />
</div>
</>
)
}
export default Detail;
测试文件__tests__/react/utils/index.ts新增内容
...
export const setRadio = (props: InputProps): string => {
const { testId, value } = props;
// 找到局部内容
const randioGroup = screen.getByRole('radiogroup');
// 根据局部内容找到全部的randio
const radioBtns = randioGroup.querySelectorAll('input[type="radio"]');
// 将类数组改成真实数组 注意类型写法
const radioBtns2 = [...radioBtns] as Array<HTMLInputElement> ;
// 根据测试穿件来的值进行查找,并点击
const filRadio = radioBtns2.filter((radioInput: HTMLInputElement) => radioInput.value === value);
fireEvent.click(filRadio[0]);
// 最后将点击的按钮值进行返回
return filRadio[0].value
}
新增测试文件__test__/react/radioGroup.test.tsx
import { screen } from '@testing-library/dom'
import { render, waitFor } from "@testing-library/react";
import userEvent from '@testing-library/user-event';
// 引入jest-dom的匹配内容
import '@testing-library/jest-dom';
import { setRadio } from '../utils';
import Detail from "src/page/detail/detail";
describe('test react com',() => {
it('test input',() => {
console.log('kkk')
const Com = (
<Detail />
)
const container = render(Com);
const value = setRadio({
value: 'girl'
})
expect(value).toBe('girl');
expect(container).toMatchSnapshot('radio-input')
})
})
注意事项:
- radioGroup这里html结构中有一个role='radioGroup'所以我们找局部结构的时候就用他来
- 注意change事件的写法:handleChange: ( event: React.ChangeEvent ) => void
- querySelectAll返回的是一个类数组,需要转变成真实的数组,这里用到了...
- 数组对象类型:Array
以上三个案例,加上上一节的输入框,那么表单这块基本上就差不多了,后续大家实际业务上的测试可以依样画瓢,最后,帮忙点赞支持,然后抽空去验证吧!!!