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 testnpx 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>;
}