优秀的编程知识分享平台

网站首页 > 技术文章 正文

Jest直通车—— react组件测试二(react test render)

nanyue 2024-07-25 06:01:00 技术文章 13 ℃

声明:本篇测试是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

以上三个案例,加上上一节的输入框,那么表单这块基本上就差不多了,后续大家实际业务上的测试可以依样画瓢,最后,帮忙点赞支持,然后抽空去验证吧!!!

Tags:

最近发表
标签列表