preface

In the last article, we added some simple tests for common capabilities of the React Component. In this article, we will continue to check and fill in the gaps on the basis of the previous article to ensure that our simple tests can cover our scenarios

Judge method call

In React Component, methods are undoubtedly very important. In daily development, in order to improve the reusability or decoupling of components, we will expose some lifecycle hook methods or provide callback methods based on certain stages or scenarios. Our single test is also to ensure that these methods can be called normally

Modify the components

Here we add an onClickTitle argument, passing in a function that returns the title content as an entry parameter to the function when the title is clicked

import "./index.css"; import { PropsWithChildren, ReactNode, CSSProperties, useCallback, } from "react"; Interface TodoHeaderProps {// Title: string; // The outermost containerStyle? : CSSProperties; // Whether to finish isFinish? : boolean; // Icon link iconUrl? : string; // Additional information extraInfo? : ReactNode; // Click on the title event onClickTitle? : (title: string) => void; } export default function TodoHeader({ title, containerStyle, iconUrl, isFinish = false, children, extraInfo, OnClickTitle,}: PropsWithChildren<TodoHeaderProps>) {// clickTitleFn = useCallback(() => {onClickTitle? .(title); }, [onClickTitle, title]); return ( <div className="report-header" style={containerStyle}> {iconUrl && <img src={iconUrl} alt="icon" />} <span className="title" data-testid="todo-header-title" style={{ background: isFinish ? "red" : "white" }} onClick={clickTitleFn} > {title} </span> <span className="extra">{extraInfo}</span> {children} </div> ); }Copy the code

Modify the call

const onClickTitle = useCallback((title: string) => { window.alert(title); } []); <TodoHeader title=" This is a title "containerStyle={{border: "1px solid blue" }} isFinish={true} iconUrl={logo} onClickTitle={onClickTitle} />Copy the code

Look at the effect. It’s as expected

A single measurement to write

Mock function (jest. Fn ()) is used to create a test function and pass it into the component. The toBeCalled API is used to determine whether our method is called. The third is fireEvent, which is a feature provided by Testing Libray that simulates user actions, such as the click event we used here

It (' responds correctly to onClickTitle events', () => {const mockClickFn = jest.fn(); Const title = "title "; const { getByText } = render( <TodoHeader title={title} onClickTitle={mockClickFn} /> ); fireEvent.click(getByText(title)); expect(mockClickFn).toBeCalled(); });Copy the code

PNPM test SRC /components/__tests__/todo-header.test.tsx

The toBeCalledTimes API can also be used to assert the number of times it was called. We added an assertion to the above use case. The assertion was called twice

It (' responds correctly to onClickTitle events', () => {const mockClickFn = jest.fn(); Const title = "title "; const { getByText } = render( <TodoHeader title={title} onClickTitle={mockClickFn} /> ); fireEvent.click(getByText(title)); expect(mockClickFn).toBeCalled(); expect(mockClickFn).toBeCalledTimes(2); });Copy the code

The result is as follows,Let’s put the top onetoBeCalledTimesChange it to 1, rerun it, and now it’s doneI’m going to add another API,toBeCalledWithThis can assert some parameters of the call. Let’s add an assertion to the case above

expect(mockClickFn).toBeCalledWith(title);
Copy the code

Re-run it and it still passes

Determine the invocation of asynchronous methods

As we know, js is full of all kinds of asynchrony, we have encountered in this before the various single test basically do not involve asynchrony things, but in the actual project asynchrony is essential, so here we also do a single test of asynchrony in this case

Modify the components

This is an asynchronous function that returns a string. We call this method in the component, and then use the setState method to set the string returned as the title to update the title asynchronously

import "./index.css"; import { PropsWithChildren, ReactNode, CSSProperties, useCallback, useState, useEffect, } from "react"; Interface TodoHeaderProps {// Title: string; // The outermost containerStyle? : CSSProperties; // Whether to finish isFinish? : boolean; // Icon link iconUrl? : string; // Additional information extraInfo? : ReactNode; // Click on the title event onClickTitle? : (title: string) => void; // The initialization method onInit? : () => Promise<string>; } export default function TodoHeader({ title, containerStyle, iconUrl, isFinish = false, children, extraInfo, onClickTitle, onInit, }: PropsWithChildren<TodoHeaderProps>) { const [currentTitle, setCurrentTitle] = useState<string>(title); Const clickTitleFn = useCallback(() => {onClickTitle? .(title); }, [onClickTitle, title]); useEffect(() => { if (onInit) { (async () => { const result = await onInit(); setCurrentTitle(result); }) (); } }, [onInit]); return ( <div className="report-header" style={containerStyle}> {iconUrl && <img src={iconUrl} alt="icon" />} <span className="title" data-testid="todo-header-title" style={{ background: isFinish ? "red" : "white" }} onClick={clickTitleFn} > {currentTitle} </span> <span className="extra">{extraInfo}</span> {children} </div> ); }Copy the code

A single measurement to write

First we write a single test as we did before, mock an asynchronous function that returns a new string, and then query the string to see if it exists in the view

It (' correctly respond to onInit event ', () => {const title = "title "; Const newTitle = "newTitle "; const mockInitFn = jest.fn(() => Promise.resolve(newTitle)); const { queryByText } = render( <TodoHeader title={title} onInit={mockInitFn} /> ); const element = queryByText(newTitle); expect(element).not.toBeNull(); });Copy the code

Look at the result of the run, sure enough, the error has been reportedBecause we have an internal update state, and the update state is actually asynchronous, the assertion we make is based on the content after the update, so we need to use the solutionactWrap the Render method to change the above test case

It (' correctly respond to onInit event ', async () => {const title = "title "; Const newTitle = "newTitle "; const mockInitFn = jest.fn(() => Promise.resolve(newTitle)); await act(async () => { render(<TodoHeader title={title} onInit={mockInitFn} />); }); const element = screen.queryByText(newTitle); expect(element).not.toBeNull(); });Copy the code

If you look at the result of the run, this time it has passedThe other solution, which we actually used in the previous use case, is just thatwaitForThis API

Let’s also rewrite the above case, in fact, will beactChange towaitForAll else being the same

It (' correctly respond to onInit event ', async () => {const title = "title "; Const newTitle = "newTitle "; const mockInitFn = jest.fn(() => Promise.resolve(newTitle)); await waitFor(async () => { render(<TodoHeader title={title} onInit={mockInitFn} />); }); const element = screen.queryByText(newTitle); expect(element).not.toBeNull(); });Copy the code

The results

FireEvent is simple to use

In the above case we have already used fireEvent, which is an API provided by testing Library to simulate some user behavior, such as various clicks, various user interaction events, etc. We have already tried fireEvent.click. Now let’s add one more scenario

Modify the components

We add one more Input, and then update state to modify the title when the Input is modified

import "./index.css"; import { PropsWithChildren, ReactNode, CSSProperties, useCallback, useState, useEffect, } from "react"; import { Input } from "antd"; Interface TodoHeaderProps {// Title: string; // The outermost containerStyle? : CSSProperties; // Whether to finish isFinish? : boolean; // Icon link iconUrl? : string; // Additional information extraInfo? : ReactNode; // Click on the title event onClickTitle? : (title: string) => void; // The initialization method onInit? : () => Promise<string>; } export default function TodoHeader({ title, containerStyle, iconUrl, isFinish = false, children, extraInfo, onClickTitle, onInit, }: PropsWithChildren<TodoHeaderProps>) { const [currentTitle, setCurrentTitle] = useState<string>(title); Const clickTitleFn = useCallback(() => {onClickTitle? .(title); }, [onClickTitle, title]); useEffect(() => { if (onInit) { (async () => { const result = await onInit(); setCurrentTitle(result); }) (); } }, [onInit]); return ( <div className="report-header" style={containerStyle}> {iconUrl && <img src={iconUrl} alt="icon" />} <span className="title" data-testid="todo-header-title" style={{ background: isFinish ? "red" : "white" }} onClick={clickTitleFn} > {currentTitle} </span> <Input type="text" style={{ width: 300, display: "flex" }} value={currentTitle} onChange={e => setCurrentTitle(e.target.value)} /> <span className="extra">{extraInfo}</span> {children} </div> ); }Copy the code

A single measurement to write

Here we will use the act mentioned above. After obtaining the corresponding Input, we will set the changed value through fireEvent. Change, and pay attention to the format of the parameter

It (' handle Input change event correctly ', async () => {const title = "title "; Const newTitle = "newTitle "; const { container } = render(<TodoHeader title={title} />); const inputElement = container.querySelector("input"); expect(inputElement).not.toBeNull(); await act(async () => { fireEvent.change(inputElement! , { target: { value: newTitle } }); }); const element = screen.queryByText(newTitle); expect(element).not.toBeNull(); });Copy the code

At the end

This article builds on the previous blog post in both cases, and for scenarios we use fireEvent to simulate events that are invoked when some action is triggered

The code for this section can be found at github.com/liyixun/rea… Github.com/liyixun/rea… Series of articles: Learn how to react with fireEvent. Learn how to react with fireEvent. Learn how to react with fireEvent. Github.com/liyixun/rea…