Find a bug or typo? Pull requests are welcome.
React creates an object representation of nodes representing a user interface.
This code:
React.createElement("ul", { className: "inline-list" }, [ React.createElement("li", { key: "first", className: "list-item" }, 'First Item') ]);
Becomes something like this:
{ ul: { className: "inline-list" }, [ "li", { key: "first", className: "list-item" }, 'First Item' ] }
A "renderer" converts that object to a useable interface.
ReactDOM.render(<App />, domElement);
ReactDOMServer.renderToString(<App />);
Test Runner
Test Renderers
Assertions
react-scripts
react-scripts test
@wordpress/scripts
wordpress-scripts test
npx create-react-app
And A Web App :)
# install create-react-app npx create-react-app # Run the included test yarn test
Create React App comes with one test.
This is an acceptance test. It tests if anything is broken.
import React from "react"; import ReactDOM from "react-dom"; import App from "./App"; it("renders without crashing", () => { const div = document.createElement("div"); ReactDOM.render(<App />, div); ReactDOM.unmountComponentAtNode(div); });
How do I know the components works?
How do I know the components work together?
What is the most realistic test of the program?
Create a one page app that:
Unit tests:
test()
Syntax//Import React import React from "react"; //Import test renderer import TestRenderer from "react-test-renderer"; //Import component to test import { DisplayValue } from "./DisplayValue"; test("Component renders value", () => {}); test("Component has supplied class name", () => {});
//Import React import React from "react"; //Import test renderer import TestRenderer from "react-test-renderer"; //Import component to test import { DisplayValue } from "./DisplayValue"; describe("EditValue Component", () => { it("Has the supplied value in the input", () => {}); it("Passes string to onChange when changed", () => {}); });
yarn add react-test-renderer
import React from 'react'; export const DisplayValue = ({value,className}) =>( <div className={className}>{value}</div> );
Stores JSON representation of component in file system
With React Test Renderer
test("Component renders correctly", () => { expect( TestRenderer.create( <DisplayValue value={"The Value"} className={"the-class-name"} /> ).toJSON() ).toMatchSnapshot(); });
yarn add @testing-library/react
import { render, cleanup, fireEvent } from "@testing-library/react"; describe("EditValue component", () => { afterEach(cleanup); //reset JSDOM after each test it("Calls the onChange function", () => { //put test here }); it("Has the right value", () => { //put test here }); });
test("Calling the onChange function", () => { const onChange = jest.fn(); const { getByLabelText } = render(<EditValue onChange={onChange} value={""} id={'input-test'} className={"some-class"} />); fireEvent.change(getByLabelText("Set Value"), { target: { value: "New Value" } }); expect(onChange).toHaveBeenCalledTimes(1); });
const onChange = jest.fn(); test("Passes updated value, not event to onChange callback", () => { const onChange = jest.fn(); const { getByDisplayValue } = render(<EditValue onChange={onChange} value={"Old Value"} id="input-test" className={"some-class"} />); fireEvent.change(getByDisplayValue("Old Value"), { target: { value: "New Value" } }); expect(onChange).toHaveBeenCalledWith("New Value"); });
With React Testing Library
test( 'matches snapshot', () => { const {container} = render(<EditValue onChange={jest.fn()} value={"Hi Roy"} id={'some-id'} className={"some-class"} /> ); expect( container ).toMatchSnapshot(); });
Do the two components work together as expected?
it("Displays the updated value when value changes", () => { const { container, getByTestId } = render(<App />); expect(container.querySelector(".display-value").textContent).toBe("Hi Roy"); fireEvent.change(getByTestId("the-input"), { target: { value: "New Value" } }); expect(container.querySelector(".display-value").textContent).toBe( "New Value" ); });
Using dequeue's aXe
# Add react-axe yarn add react-axe --dev # Add react-axe for Jest yarn add jest-axe --dev
This does NOT mean your app is accessible!
import React from "react"; import server from "react-dom/server"; import App from "./App"; import { render, fireEvent, cleanup } from "@testing-library/react"; const { axe, toHaveNoViolations } = require("jest-axe"); expect.extend(toHaveNoViolations); it("Raises no a11y errors", async () => { const html = server.renderToString(<App />); const results = await axe(html); expect(results).toHaveNoViolations(); });
Create a one page app that:
yarn add @wordpress/scripts
And A Plugin
A block for showing some text.
@wordpress/scripts
??Provides:
# Install WordPress scripts yarn add @wordpress/scripts
See README
{ "scripts": { "build": "wp-scripts build", "start": "wp-scripts start", "test:unit": "wp-scripts test-unit-js --config jest.config.js", } }
Testing works the same, we can use same renderers.
@wordpress/scripts
works on top of Jest, webpack, Babel, etc.
One file that registers the block.
import { registerBlockType } from "@wordpress/blocks"; import Edit from './Edit'; import Save from './Save'; const blockConfig = require('./block.json'); registerBlockType(blockConfig.name, { ...blockConfig, edit: Edit, save: Save });
The edit and save callback are composed in separate files.
import { TextControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useBlockProps } from '@wordpress/block-editor'; export const Editor = ({ value, onChange, isSelected }) => ( <> {isSelected ? <TextControl value={value} onChange={onChange} /> : <p>{value}</p> } </> ); export default function Edit({ attributes, setAttributes, isSelected }) { return ( <div {...useBlockProps()}> <Editor isSelected={isSelected} value={attributes.content} onChange={(content) => setAttributes({ content })} /> </div> ); }
//Import component to test import { Editor } from './Edit'; describe("Editor componet", () => { afterEach(cleanup); it('matches snapshot when selected', () => {}); it('matches snapshot when not selected', () => {}); it("Calls the onchange function", () => {}); it("Passes updated value, not event to onChange callback", () => { }); });
it('matches snapshot when selected', () => { const onChange = jest.fn(); const { container } = render(<Editor onChange={onChange} value={'Tacos'} isSelected="true" />); expect(container).toMatchSnapshot(); });
it("Calls the onChange function", () => { const onChange = jest.fn() const { getByDisplayValue } = render(<Editor onChange={onChange} value={"Boring Water"} isSelected="false" />); fireEvent.change(getByDisplayValue("Boring Water"), { target: { value: "Sparkling Wate" } }); expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledWith("Sparkling Water"); });
import { __ } from '@wordpress/i18n'; import { useBlockProps } from '@wordpress/block-editor'; export default function save({ attributes }) { return <div {...useBlockProps.save()}>{attributes.content}</div>; }