jest測試react組件

以下為本節(jié)內(nèi)容,內(nèi)容比較多,且都是日常工作中會用到的測試用例與常見的react組件用例,相信您會有所收獲的

  • 測試React
    • 從一個簡單的測試開始
    • 運行條件
    • 跑通成功
  • react組件測試
    • input輸入框
    • input輸入框不使用UI框架下
    • 下拉列表
    • 復(fù)選框checkbox
    • radioGroup
  • react表單上傳
    • 上傳部分
    • 測試部分

測試React

測試之前我們先安裝好模塊依賴

npm i @testing-library/jest-dom -D
npm i @testing-library/react -D
npm i @testing-library/user-event -D
// 類型支持
npm i @types/jest -D

模塊依賴解釋,和對應(yīng)的文檔位置:https://testing-library.com/docs/

這三個依賴包我們會用到的功能有這么幾個,這幾個基本上可以解決很多測試功能的問題。

// cleanup清除,一般用于全局的afterAll中
// fireEvent操作事件,如點擊等
// render渲染組件,返回一個container,可用于生成快照
// waitFor如果渲染過程中涉及異步操作,那么數(shù)據(jù)的返回就必須要使用waitFor
import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";

// screen == container.screen,用于獲取查詢方法
import { screen } from '@testing-library/dom';
// userEvent同fireEvent但高于fireEvent,我們推薦優(yōu)先使用userEvent,因為他還支持
// change事件
import userEvent from '@testing-library/user-event';

從一個簡單的測試開始

新建一個文件 src/component/head/head.tsx

import './head.css';

const Head = () => {
    return (
        <div>
            <p>head</p>
            <div>i am head</div>
        </div>
    )
}
export default Head;

新建一個文件src/component/head.css

p {
    font-size: 16px;
    color: pink;
}

新建測試 src/__tests__/sum.test.tsx

import { cleanup, fireEvent, render, waitFor } from "@testing-library/react";
import { screen } from '@testing-library/dom';
import userEvent from '@testing-library/user-event';
import Head from "~/component/head/head";

describe('first test',() =>{ 
    test('sum func',() => {
        const Com = (
            <Head />
        )
        const container = render(Com);
        expect(container).toMatchSnapshot('first test')
    })
})

運行條件

  • 確保環(huán)境是jsdom,因此我們需要安裝jest-environment-jsdom:

npm i jest-environment-jsdom -D
testEnvironment: 'jsdom',

  • head組件引入正常 vite.config.ts配置
resolve: {
    alias: {
      '~': path.resolve(__dirname,'src'),
      'src': path.resolve(__dirname,'src')
    }
  },

jest.config.js配置

moduleNameMapper: {
  '^~/(.*)': '<rootDir>/src/$1'
},
  • 解析css文件
    我們需要安裝identity-obj-proxy: npm i identity-obj-proxy -D,并在jest.config.js中做好配置
moduleNameMapper: {
    '\\.(css|scss|less)': 'identity-obj-proxy',
    '^~/(.*)': '<rootDir>/src/$1'
  },

跑通成功

覆蓋率圖

并在同一級目錄中,生成snapshots快照
快照圖

快照圖還是比較有用的,在大型項目中測試通過與否會比較兩次測試的結(jié)果是否會發(fā)生變化,如果發(fā)生了變化會認為測試失?。贿@就意味著我們不僅要上傳最新的測試用例,同時對于業(yè)務(wù)中出現(xiàn)的時間等任何可能改變頁面內(nèi)容的東西,全部mock

react組件測試

主要是測試一些表單控件,暫時都在detail這個頁面測試吧。這里我們使用的輸入框為mui,因此需要安裝一下

npm install @mui/material @emotion/react @emotion/styled  // 組件
npm install @mui/icons-material // icon

官網(wǎng):https://mui.com/material-ui/getting-started/installation/

好,現(xiàn)在直接開擼~

input輸入框

目的:渲染一個頁面,這個頁面包含了input輸入框,我們需要給輸入框添加內(nèi)容。

封裝一個公共的input組件,因此新建文件
src/component/common/Input.tsx

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
}

// 類型寫法二
type AA<Type = object> = Type & {
    startAdornment?: ReactElement,
    endAdornment?: ReactElement
}

type ZLTextProps2 = AA<TextFieldProps>

const ZlInput = (props: ZLTextProps): ReactElement => {
    const { 
        name,
        label,
        placeholder = 'please Input',
        ...rest 
    } = props;
    return (
        <TextField 
            id={name}
            label={label} 
            placeholder={placeholder}
            {...rest}
        />
    )
}
export default ZlInput;

我們在page/detail/detail中引入這個組件,并做處理

import { useState } from 'react';

import ZlInput from 'src/component/common/Input';
import {
    Grid
} from '@mui/material';

import React from 'react';

const Detail = () => {
    const [username,setName] = useState('');
    const [age,setAge] = useState('');

    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const handleChange = (e: any , val_type: string) => {
        const val = e.target.value;
        switch(val_type) {
            case 'username':
                setName(val);
                break;
            case 'age':
                setAge(val);
                break;
            default: 
        }
    }
    return (
        <Grid container>
            <Grid item>
                <ZlInput id='username' 
                label='username' 
                variant="standard" 
                data-testid='username' 
                InputProps={{
                    'aria-label': 'username'
                }}
                value={username}
                onChange={(e) => handleChange(e,'username')}
                />                
            </Grid>
            <Grid>
                <ZlInput id='age' 
                label='age' 
                variant="standard" 
                data-testid='age' 
                InputProps={{
                    'aria-label': 'age'
                }}
                value={age}
                onChange={(e) => handleChange(e,'age')}
                /> 
            </Grid>
        </Grid>
    )
}
export default Detail;

注意了,這里的input必須要說受控組件,否則無法change改變輸入框的value值。

最后建立測試 __tests__/react/com/input.test.tsx

import { render,fireEvent,waitFor } from "@testing-library/react";
import { screen } from "@testing-library/dom";

import Detail from "src/page/detail/detail";

interface InputProps {
    testId?: string,
    value?: string
}

const InputField = (props: InputProps): void => {
    const { testId = '' , value } = props;
    // 使用getByTestId時,傳入值必須要初始值
    const el = screen.getByTestId(testId);
    // 相當于我們找到了testId所對應(yīng)的dom節(jié)點,然后根據(jù)這個節(jié)點我們繼續(xù)往下面找子節(jié)點內(nèi)容
    const input = el.getElementsByTagName('input')[0];
    fireEvent.change(input,{
        target: {
            value: value
        }
    })
}

// 兩種方法獲取dom節(jié)點
const getElement = (
    testId?: string,
    selector?: string
): HTMLElement | null => {
    if ( selector ) {
        return document.querySelector(selector);
    } else if ( testId ) {
        return screen.getByTestId(testId)
    }
    return null;
}

describe('test react com',() => {
    it('test input',() => {
        console.log('kkk')
        const Com = (
            <Detail />
        )
        const container = render(Com);

        InputField({
            testId: 'username',
            value: 'jack'
        })
        InputField({
            testId: 'age',
            value: '12'
        })

        const input_username = getElement('username');
        const val = input_username?.getElementsByTagName('input')[0].value;
        console.log(val,'input username')
        expect(input_username?.getElementsByTagName('input')[0].value).toBe('jack');
        const input_age = getElement('age');
        expect(input_age?.getElementsByTagName('input')[0].value).toBe('12')
        expect(container).toMatchSnapshot('detail-input')        
    })
})

測試內(nèi)容講解

render
用于渲染組件,以便生成快照
fireEvent
用于處理事件,包括點擊、聚焦、改變等
screen
獲取testing-libriary的dom方法,包括三大類,查詢query,獲取get,和查找find
官網(wǎng)地址: https://testing-library.com/docs/queries/about
InputField
用于改變input框的輸入值,即改變value值
getElement
用于獲取dom節(jié)點

快照

input輸入框照結(jié)果

input輸入框不使用UI框架下

我們建立一個文件 __test__/react/com/originInput.test.tsx

import { useState } from 'react';
import { screen } from '@testing-library/dom'
import { render, waitFor } from "@testing-library/react";
import userEvent from '@testing-library/user-event';

import '@testing-library/jest-dom'

const Btn = () => {
    const [name,setName] = useState('jack');
    const handleBtn = () => {
        setName('mike');
    }
    const handleBtn2 = (e: any) => {
        const value = e.target.value;
        setName(value);
    }
    return (
        <div>
            <div>
                <label htmlFor="username">username</label>
                <input type="text" role='username' id='username' name='username' value={name} onChange={handleBtn2} />
            </div>
            <button onClick={handleBtn}>Click me</button>
        </div>
    )
}

describe('user event test', () => {
    it('something has happen',async () => {

        const container = render(<Btn />);
        const input = screen.getByRole('username');
        const btn = screen.getByRole('button',{ name: 'Click me'})
        
        await userEvent.clear(input);
        await waitFor( () => expect(input).toHaveValue('') )
        
        // userEvent.click(btn);
        await userEvent.type(input,'mike');
        await waitFor( ()=> expect(input).toHaveValue('mike') );
        
        expect(container).toMatchSnapshot('auth_button_mock_axios2');
    })
})

解讀:

  • 思路和 上一個例子類似,組件是手控組件
  • 這里我們事件的操作換成了userEvent,官網(wǎng):https://testing-library.com/docs/user-event/intro
  • 這里的type是輸入值的意思
  • 這里的toHaveValue匹配器并不是jest所有的,是我們引入了jest/dom:import '@testing-library/jest-dom' 才有的,大家可以去看源碼,相信我會收獲不少的。當然,就目前我們使用fireEvent也是可以的
  • waitFor這個是異步等待,這里之所以使用到,是因為我們需要等到input數(shù)據(jù)改變之后才能獲取值,和setState有關(guān)。

下拉列表

下拉列表原本是select/otpion的,但是由于我們這里使用到的是mui框架,因此我們發(fā)現(xiàn)它的下拉框其實就是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的文件,相當于我們的業(yè)務(wù)邏輯頁面

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中添加內(nèi)容

...
import Z_select from './Com/select';
const Detail = () => {
    return (
        <>
            ...
            <Z_select />
        </>        
    )
}
export default Detail;

如果您不知道為啥頁面結(jié)構(gòu)是這樣,建議回顧一下我當時項目搭建的過程:
https://www.toutiao.com/article/7250691387551826432/

由于我們的測試都是寫在input.test.tsx中,因此對于一些公共的方法,我們有必要先提取出來。所以新建文件
__tests__/react/utils/index.ts(注意這個ts,后續(xù)您可以直直接拿過來用的)。在這個文件里面我們把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的節(jié)點
    const dropDown = within(screen.getByTestId(testId));
    // 再找到可以點擊的節(jié)點,并點擊
    fireEvent.mouseDown(dropDown.getByRole('button'));

    // 此時會出現(xiàn)下啦框

    // 找到下拉列表的選項
    const listbox = within(screen.getByRole('listbox'));

    expect(listbox).not.toBeNull();
    
    // 選擇一個點擊,這里選擇和value一樣的節(jié)點
    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的時候,必須要有一個初始值
  • 我們書寫下拉框方法的時候是注意了細節(jié)的,即頁面展示了什么,點擊后,會發(fā)生什么都會按照我們ui頁面真實反饋。大家在一邊測試自己項目的時候,也可以打開自己的項目,仔細觀察他們的html結(jié)構(gòu)和屬性,方便自己獲取節(jié)點
  • within方法是用來包裹一個節(jié)點,并將這個節(jié)點返回后具有和screen一樣的查詢方法

復(fù)選框checkbox

新建公共組件 component/common/checkbox.tsx ,讓復(fù)選框都一層封裝

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新加一個方法,這個方法會把點擊復(fù)選框之后,會將input的選中狀態(tài)返回

...
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 />
  )
    // 默認狀態(tài)是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
})

注意事項:

  • 當我們的字組件內(nèi)容只有一個表單時,最好時通過dom選擇器進行獲取
  • checkbox復(fù)選框同樣需要是手控組件
  • 仔細看,會發(fā)現(xiàn),當我們沒有測試點擊這塊的時候,覆蓋率時不會覆蓋到handleChange這個事件的,因此需要我們像上面那樣,手動某一次操作=
  • 注意change事件的類型寫法:React.ChangeEvent

radioGroup

同前面類似,先創(chuàng)建一個文件 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新增內(nèi)容

...
export const setRadio = (props: InputProps): string => {
    const { testId, value } = props;

    // 找到局部內(nèi)容
    const randioGroup = screen.getByRole('radiogroup');
    // 根據(jù)局部內(nèi)容找到全部的randio
    const radioBtns = randioGroup.querySelectorAll('input[type="radio"]');
    // 將類數(shù)組改成真實數(shù)組 注意類型寫法
    const radioBtns2 = [...radioBtns] as Array<HTMLInputElement> ;
    // 根據(jù)測試穿件來的值進行查找,并點擊
    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的匹配內(nèi)容
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結(jié)構(gòu)中有一個role='radioGroup'所以我們找局部結(jié)構(gòu)的時候就用他來
  • 注意change事件的寫法:handleChange: ( event: React.ChangeEvent ) => void
  • querySelectAll返回的是一個類數(shù)組,需要轉(zhuǎn)變成真實的數(shù)組,這里用到了...
  • 數(shù)組對象類型:Array

react表單上傳

以上是表單的所有內(nèi)容,那么現(xiàn)場來一個案例試試水~

這里完成的任務(wù)主要是將以上所講內(nèi)容進行整合,同時發(fā)起一次api請求。

首先我們在express/index.js中加入一個post請求:

...
import bodyparser from 'body-parser';
// 解析body
app.use(bodyparser.urlencoded({extended:false}));

app.post('/home',(req,res) => {
  const body = req.body;
  console.log(body);
  res.send({
    status: 0,
    msg: 'submit success'
  })
})

如果您不知道為啥這里突然加了一個借口,建議回顧一下我當時項目搭建的過程:
https://www.toutiao.com/article/7250691387551826432/

接著新建網(wǎng)絡(luò)請求api/detail.ts文件

import axios from "axios";
const commonUrl = '/api';
interface Obj {
    username: string,
    age: string,
    email: string,
    city: string,
    like: string,
    isLike: boolean
}

export const submitData = async (data: Obj) => {
    const path = commonUrl + '/home';
    const res = await axios.post<any>(
        path,
        {...data},
        {
            headers: {
                'Content-Type':  'application/x-www-form-urlencoded'
            }
        }
    );
    return res.data
}

然后我們寫一個組件 page/detail/Com/submit.tsx
以下代碼確實多,可以訪問gitee:https://gitee.com/xifeng-canyang/jest-copy-file-and-video/blob/master/src/page/detail/Com/submit.tsx

import React,{useEffect,useState} from "react";
import {
    Grid,
    styled,
    Box,
    MenuItem,
    Button
} from '@mui/material';

import ZlInput from "src/component/common/Input";
import RadioButtonsGroup from "src/component/common/radioGroup";
import Zl_checkbox from "src/component/common/checkbox";

import { submitData } from "../../../api/detail";

const ItemStyle = styled('div')({
    [`& .MuiFormControl-root `]: {
        verticalAlign: 'bottom',
        marginLeft: '20px'
    },
    [`& span`]: {
        display: 'inline-block',
        width: '80px',
        textAlign: 'right'
    },
    width: '100%'
})

const RadioStyle = styled('div')({
    display: 'flex',
    alignItems: 'center',
    [`& .MuiFormControl-root `]: {
        verticalAlign: 'bottom',
        marginLeft: '20px'
    },
    [`& .radio-left`]: {
        width: '80px',
        textAlign: 'right',
    },
})
const LikeStyle = styled('div')({
    display: 'flex',
    alignItems: 'center',
    marginTop: '-10px',
    [`& .MuiButtonBase-root`]: {
        marginLeft: '6px'
    },
    [`& .radio-left`]: {
        width: '80px',
        textAlign: 'right',
    },
})
const CityStyle = styled('div')({
    [`& .MuiFormControl-root `]: {
        verticalAlign: 'bottom',
        marginLeft: '20px'
    },
    [`& span`]: {
        display: 'inline-block',
        width: '80px',
        height: '42px',
        textAlign: 'right'
    },
    width: '100%'
})
const SubmitStyle = styled('div')({
    [`& .MuiButtonBase-root `]: {
        verticalAlign: 'bottom',
        marginLeft: '20px'
    },
    [`& span`]: {
        display: 'inline-block',
        width: '80px',
        height: '42px',
        textAlign: 'right'
    },
    width: '100%'
})
interface Item {
    value: string,
    label: string
}
interface Obj {
    username: string,
    age: string,
    email: string,
    city: string,
    like: string,
    isLike: boolean
}

const dd = [
    { value: 'apple', label: 'apple'},
    { value: 'apple2', label: 'apple2'},
    { value: 'apple3', label: 'apple3'},
]

const currencies = [
    {
      value: 'beijing',
      label: 'beijing',
    },
    {
      value: 'shanghai',
      label: 'shanghai',
    },
    {
      value: 'shenzhen',
      label: 'shenzhen',
    }
];

export default function Submit() {
    const [obj,setObj] = useState<Obj>({
        username: '',
        age: '',
        email: '',
        city: '',
        like: '',
        isLike: false
    })
    const [like,setLike] = useState('');
    

    const handleLikeOption = (event: React.ChangeEvent<HTMLInputElement>) => {
        // console.log(event.target.value,'like');
        const target = event.target;
        if ( target.checked ) {
            setLike(event.target.value);
            setObj({
                ...obj,
                like: event.target.value
            })
        } else {
            setLike('')
        }
        
    }

    const City = () => {
        return (
            <ZlInput id="city" data-testid='city' inputType='select' label='city' handleInputChange={changeCity} placeholder="please input" defaultValue={currencies[0].value} variant="standard" helperText="Please select your city" sx={{ width: '400px'}}>
                {currencies.map((option) => (
                    <MenuItem key={option.value} value={option.value}>
                        {option.label}
                    </MenuItem>
                ))}
            </ZlInput>
        )
    }

    const changeCity = (event: React.ChangeEvent<HTMLInputElement>) => {
        setObj({
            ...obj,
            city: event.target.value
        })
    }

    const handleChangeLike = (event: React.ChangeEvent<HTMLInputElement>) => {
        setObj({
            ...obj,
            isLike: event.target.checked
        })
    }


    const handleSubmit = async () => {
        console.log('--submit data--',obj)
        const res = await submitData(obj)
        console.log(res,'submit')
    }

    const changeChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const val = e.target.value;
        if ( e.target.labels?.length ) {
            // 這里有一個BUG,明明給了age輸入框一個label,但是labels獲取的NodeList為空數(shù)組,其他的卻正常
            const labelValue = e.target?.labels[0]?.textContent;
            switch(labelValue) {
                case 'username':
                    setObj({
                        ...obj,
                        username: val
                    })
                    break;
                case 'age':
                    setObj({
                        ...obj,
                        age: val
                    })
                    break;
                case 'email':
                    setObj({
                        ...obj,
                        email: val
                    })
            }
        } else {
            // console.log(e.target.labels,'kkk')
            setObj({
                ...obj,
                age: val
            })
        }
    }
    return (
        <Box sx={{ display: 'flex', flexWrap: 'wrap', width: '800px' }}>
            <Grid container spacing={2} sx={{ padding: '20px'}}>
                <Grid item xs={12}>
                    <ItemStyle>
                        <span>username: </span><ZlInput label='username' data-testid='username'  handleInputChange={changeChange} defaultValue={obj.username} id="usename"  placeholder="please input" variant="standard" sx={{ width: '400px'}}  />
                    </ItemStyle>
                </Grid>
                
                <Grid item xs={12}>
                    <ItemStyle>
                        <span>age: </span><ZlInput id="age" label='age' data-testid='age'  handleInputChange={changeChange} defaultValue={obj.age}  placeholder="please input" variant="standard" sx={{ width: '400px'}} />
                    </ItemStyle>
                </Grid>
                <Grid item xs={12}>
                    <ItemStyle>
                        <span>email: </span><ZlInput id="email" data-testid='email'  handleInputChange={changeChange} label='email' defaultValue={obj.email} placeholder="please input" variant="standard" sx={{ width: '400px'}} />
                    </ItemStyle>
                </Grid>
                <Grid item xs={12}>
                    <CityStyle>
                        <span>city: </span><City />
                    </CityStyle>
                </Grid>

                <Grid item xs={12}>
                    <RadioStyle>
                        <div className="radio-left">like: </div><RadioButtonsGroup data-testid='likeOptions'  id="like-btn" dd={dd}  handleChange={handleLikeOption} value={like} />
                    </RadioStyle>
                </Grid>
                <Grid item xs={12}>
                    <LikeStyle>
                        <div className="radio-left">like apple: </div><Zl_checkbox 
                            checked={obj.isLike}
                            handleChange={handleChangeLike}
                            data-testid='like_apple'
                        />
                    </LikeStyle>
                </Grid>
                <Grid item xs={12}>
                    <SubmitStyle>
                        <span></span>
                        <Button variant="contained" data-testid='submit' color="success" onClick={handleSubmit}>Submit</Button>
                    </SubmitStyle>
                </Grid>
            </Grid>
        </Box>
    )
}

看起來寫了很長,但是仔細閱讀,無非就是一些表單子內(nèi)容,我們這里用到的是mui,可以賦值跑通,結(jié)果大概長這個樣子:


頁面展示效果

最后我們就可以寫測試文件__tests__/react/com/submit.test.tsx

import { fireEvent, screen } from '@testing-library/dom'
import { render, waitFor } from "@testing-library/react";

import * as utils from '../../../api/detail';
jest.mock('../../../api/detail');

const submitDD = jest.spyOn(utils,'submitData');
submitDD.mockImplementation(
    (): Promise<any> => {
        return new Promise((resolve) => {
            resolve({
                status: 0,
                msg: 'submit success2'
            })
        })
    }
)

// 引入jest-dom的匹配內(nèi)容
import '@testing-library/jest-dom';
import { 
    setRadio,
    getSelectValue,
    InputField,
    changeCheckbox  
} from '../utils';

import Submit from 'src/page/detail/Com/submit';

describe('test react submit', () => {
    it('test input',async () => {
        console.log('kkk')
        const Com = (
            <Submit />
        )
        const container = render(Com);

        // 三個輸入框
        InputField({
            testId: 'username',
            value: 'jack'
        })
        InputField({
            testId: 'age',
            value: '12'
        })
        InputField({
            testId: 'email',
            value: '111@qq.com'
        })

        // 選擇城市
        getSelectValue({
            testId: 'city',
            value: 'shanghai'
        })

        // 選擇喜歡項
        const likeValue = setRadio({
            value: 'apple2'
        })

        // 確定是否要apple
        const checked2 = changeCheckbox({
            testId: 'like_apple'
        })

        // 提交
        const submitBtn = screen.getByTestId('submit');
        fireEvent.click(submitBtn);

        
        await waitFor(() => {
            expect(submitDD).toHaveBeenCalled();
        })
        
        expect(container).toMatchSnapshot('submit-input')        
    })
})

當我們成功跑通的時候,我們就會看到如下的打印

表明模擬了表單提交

注意事項:

  • express注意解析body,引入body-parser
  • axios發(fā)起post請求時,我們攜帶的data如果想被express解析,那么需要加一個頭:headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
  • 注意我這里是新建了一個detail.ts來寫detail頁面的網(wǎng)絡(luò)請求,那么為什么我不直接寫在home.ts里面呢?原因是我測試的時候,發(fā)現(xiàn)如果我寫在homt.ts中時,可能由于之前我們使用了自定義mock,home.ts已經(jīng)被__mocks__/home.ts所管理,導(dǎo)致頁面無法訪問post請求的這個方法

組件詳談

表單組件基本元素為這些了,那么對于列表list以及表格這塊怎么說呢,其實這兩個我們只需要給個初始值,他能夠正常渲染,覆蓋率基本上就很高了,而表格的一些高級操作,比如過濾,搜索等,如果你寫了方法,那么確實要測試一下,這個實現(xiàn)思路跟utils/index.ts里面的方法類似,模擬的時候操作了一遍,就能覆蓋到。

這么艱難的看到這里,幫忙點點贊支持~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容