1.0.21 • Published 6 years ago

react-steroids-test-renderer v1.0.21

Weekly downloads
4
License
MIT
Repository
github
Last release
6 years ago

Enhance your react-test-renderers with steroids

Do you like write component tests? Do you like react-test-renderer more than shallow renderer? Do you feel sometimes that it would be great to combine both shallow renderer and react-test-renderer to prevent rendering inner components? Do you feel headcache when writing tests for something like styled-components ?

If answer for these all questions is "Yes" then you come to the right place.

Much often, you want to mock/don't render external components. Almost always these external components are being imported through import statement import A from "./a". And very often you want to full render the inner (helpers or styled) components:

import Button from "./Button";
import AnotherComp from "./AnotherComp";

const SomeText = () => <h1>SomeText</h1>

const SomeLabel = styled.label`
    font-size: 0.8em;
`;

const MyButton = styled(Button)`
    font-size: 2em;
`

const Component = () =>
<div>
    <SomeText />
    <AnotherComp />
    <SomeLabel>Label</SomeLabel>
    <MyButton>Button</MyButton>
</div>

Here you probably want to fully render SomeText , SomeLabel and partially MyButton (render styles but don't render Button itself). AnotherComp should remain non-rendered (so it's internals won't affect the component test).

This is not achievable by standard react-test-renderer, it will give you snapshot similar to this:

.SomeLabelCss {}

.MyButtonCss {}

// Shouldn't be here
.ButtonCss {}

// Shouldn't be here
.AnotherCompCss {}

<div>
    <h1>SomeText</h1>
    // Shouldn't be here
    <div className="AnotherCompCss">AnotherComp internals</div>
    <label className="SomeLabelCss">Label</label>
    // Shouldn't be here
    <button className="MyButtonCss ButtonCss">Button</button>
</div>

Shallow renderer also won't give you desired result:

// No styles since it doesn't unwrap styled-component HOC

<div>
    // Didn't unwrap the internal component
    <SomeText />
    <AnotherComp />
    // Didn't unwrap the internal component
    <SomeLabel>Label</SomeLabel>
    // Didn't unwrap the internal component
    <MyButton>Button</MyButton>
</div>

Ideally, you want this snapshot:

.SomeLabelCss {}

.MyButtonCss {}

<div>
    <h1>SomeText></h1>
    // External component shouldn't be rendered
    <AnotherComp />
    <label className="SomeLabelCss">Label</label>
    // Unwrap styled HOC but don't render further. Button already has dedicated test
    <Button className="MyButtonCss">Button</Button>
</div>

This is achievable by mocking components using jest.mock() but this is boring and repetetive task, especially when you have to mock many components.

Finally, now you can just use react-steroids-test-renderer:

import { create } from "react-steroids-test-renderer"

// same API as in react-test-renderer
const t = create(<Component />);
expect(t.toJSON()).toMatchSnapshot();

and it will you give the snapshot which you want with mocked external dependencies and fully-rendered internal components! and it's not shallow, so the lifecycle, refs etc will continue to work.

Installation and setup

yarn add react-steroids-test-renderer --dev or npm install react-steroids-test-renderer --save-dev

Add to your .babelrc / .babelrc.js (you can use it without babel, see below)

    plugins: [
        "react-steroids-test-renderer/babel",
    ]

Make sure you apply it only for test runs, i.e. for env/NODE_ENV = test or similar

API

react-steroids-test-renderer wraps react-test-renderer so it has same API, only the create() is slightly different:

export function create(initialElement: ReactElement<any>, options: SteroidOptions = {}): TestRenderer.ReactTestRenderer

options are the standard react-test-renderer options (i.e. { createNodeMock () => {} }) with few additions:

export type ElementFilter = (element: ReactElement<any>, isImported: boolean, importedType: any | undefined, replaceMaps: ReplaceMaps) => boolean | string | React.SFC | React.ComponentClass;

interface SteroidOptions extends Partial<TestRenderer.TestRendererOptions> {
    /**
     * Filter function to substitute elements in the tree. Return truthy value to block element rendering
     * Return true to block rendering and substitute component with it's name
     * Return string to block rendering and substitute component with returned string
     * Return component class/SFC to block rendering and render this component instead
     */
    filter?: ElementFilter;
    /**
     * Strips default props from replaced elements
     * @default true
     */
    stripDefaultProps?: boolean;
    /**
     * Wrap root element into another. Useful for stories and theme providers
     */
    wrapperElement?: ReactElement<any>;
    /**
     * Whitelist prop key from stripping. Useful if you pass imported element as prop value into some local component and want to block rendering of it
     */
    whitelistProps?: string[];
}

filter allows you to control which elements you don't want to render in the tree. By default it blocks all imported components:

    // Don't render elements created from imported components or SomeComponent
    const t = create(<Component /> { filter: (element, imported, importType) => imported || element.type === SomeComponent });

imported boolean flag is coming from babel transform. If you're not using babel-transform it will be always false

wrapperElement allows you to additionally wrap the tree into some other component. Ideally for <ThemeProvider />!:

    const t = create(<Component />, { wrapperElement: <ThemeProvider theme={myTheme} /> });

Of course the wrapperElement should render their children

How does it work

1) The babel-transform adds __imported={Ref} prop to every JSX element which identifier has been referenced in import. This also works for HOC components 2) The create() substitutes every non filtered out component with fake one (with same type) with custom render(), which calls original render() to obtain react elements for the tree, then strips __imported flag from the tree completely and calls filter() function for their children. 3) The element.type for filtered out components is being replaced with string with same name 4) The result of previous steps is being passed to original react-test-renderer.create(), wrapping the tree if wrapperElement was defined 5) tree.root.findByType/find/etc... are being replaced with custom lookup functions, so tree.root.findByType(MyCustomComponent /* Function */); will be replaced to tree.root.findByType("MyCustomComponent" /* string */) for filtered out components.

Caveats

1) Since element types were substituted, checking for type equality, for example element.type === MyComponent may not work. Use some static flag attached to the component or similar to do the check. 2) styled-components, emotion, etc... When wrapping another styled-component you may have a problems:

import { Button } from "./button";

const MyButton = styled(Button)`
    color: green;
`;

<MyButton />;

If MyButton renders Button as child, i.e. MyButton -> Button -> Button internals, you're lucky (when processing the rendered tree the imported flag will be true for Button). But few libraries, for example emotion, combines all styled HOCs into one component and renders first component directly (i.e. MyButton -> Button internals). For this reason imported flag in the filter will be always false for such components. You need to write custom filter for this:

    const t = create(<MyButton>, {
        filter: (elem, imported, importType, replaceMaps) => {
            // if importType was set but imported is false it means it was used to construct other component, usually passed into HOC component,
            // in our case it means what component has been created as styled(importType)
            if (!imported && typeof importType === "function" && elem.type && elem.type.withComponent) {
                const newName = importType.displayName || importType.name;
                // add mapping & invertMapping Button <-> "Button", so root.findByType() can be redirected propertly
                replaceMaps.origToReplaced.set(elem.type, newName);
                replaceMaps.replacedToOrig.set(newName, elem.type);
                // Substitute styled(Button) with styled("Button")
                return elem.type.withComponent(newName);
            }
            return imported;
        }
    });

This will hide <Button /> internal details from resulting snapshot

Tip: Wrap create() with your custom filter to make it work across the whole project: export const testRenderer = element => create(element, { filter: ... })

1.0.21

6 years ago

1.0.20

6 years ago

1.0.19

6 years ago

1.0.18

6 years ago

1.0.17

6 years ago

1.0.16

6 years ago

1.0.15

6 years ago

1.0.14

6 years ago

1.0.13

6 years ago

1.0.12

6 years ago

1.0.11

6 years ago

1.0.10

6 years ago

1.0.9

6 years ago

1.0.8

6 years ago

1.0.7

6 years ago

1.0.6

6 years ago

1.0.5

6 years ago

1.0.4

6 years ago