Skip to content

Commit

Permalink
Added .props() and .render() to trigger re-renders
Browse files Browse the repository at this point in the history
  • Loading branch information
franciscop committed Oct 29, 2022
1 parent 93b4669 commit 0d94f46
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 9 deletions.
2 changes: 1 addition & 1 deletion index.min.js

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions src/index.js
Expand Up @@ -20,6 +20,8 @@ import "./methods/is";
import "./methods/map";
import "./methods/not";
import "./methods/parent";
import "./methods/props";
import "./methods/render/index.js";
import "./methods/siblings";
import "./methods/submit";
import "./methods/text";
Expand Down
11 changes: 7 additions & 4 deletions src/jest-matchers/toHaveHtml/index.js
@@ -1,20 +1,23 @@
import { normalize, getPlainTag } from "../../helpers";
import { normalize } from "../../helpers";

export default function (frag, html) {
this.affirmative = !this.isNot;
frag = normalize(frag);
if (typeof html !== "string") {
const msg = `Second argument of .toHaveHtml() needs to be a string`;
return { pass: false, message: () => msg };
}

for (let el of frag) {
const base = getPlainTag(el);
const hasHTML = el.outerHTML.includes(html.trim());

if (this.affirmative && !hasHTML) {
const msg = `Expected ${base} to have \`${html}\``;
const msg = `Expected ${el.outerHTML} to include ${html}`;
return { pass: false, message: () => msg };
}

if (this.isNot && hasHTML) {
const msg = `Expected ${base} not to have \`${html}\``;
const msg = `Expected ${el.outerHTML} not to include ${html}`;
return { pass: true, message: () => msg };
}
}
Expand Down
14 changes: 10 additions & 4 deletions src/jest-matchers/toHaveHtml/test.js
Expand Up @@ -12,14 +12,16 @@ const $div = $(
</div>
);

const expectString = `<div><span>I am a span</span><span>Im also here</span><span><b>here</b></span></div>`;

describe(".toHaveHtml()", () => {
it("works for a simple case", () => {
expect($div).toHaveHtml("<span>I am a span</span>");
});

it("requires valid html", () => {
expect(() => expect($div).toHaveHtml("<h1>header</h1>")).toThrow(
"Expected <div> to have `<h1>header</h1>`"
`Expected ${expectString} to include <h1>header</h1>`
);
});

Expand All @@ -40,7 +42,9 @@ describe(".toHaveHtml()", () => {
expect($div).not.toHaveHtml("<h1>header</h1>");
expect(() =>
expect($div).not.toHaveHtml("<span>I am a span</span>")
).toThrow("Expected <div> not to have `<span>I am a span</span>`");
).toThrow(
`Expected ${expectString} not to include <span>I am a span</span>`
);
});

describe("multiple elements", () => {
Expand All @@ -55,6 +59,8 @@ describe(".toHaveHtml()", () => {
</section>
).find("div");

const strDivs = `<div id="div-1"><span>span text</span></div>`;

const validInnerHTMLs = ["<span>span text</span>", "span text"];
const invalidInnerHTMLs = ["<p></p>", "<li></li>", "<span>div</span>"];

Expand All @@ -66,7 +72,7 @@ describe(".toHaveHtml()", () => {
// Throws on first child with non-existent HTML
for (const invalidHTML of invalidInnerHTMLs) {
expect(() => expect($divs).toHaveHtml(invalidHTML)).toThrow(
`Expected <div id="div-1"> to have \`${invalidHTML}\``
`Expected ${strDivs} to include ${invalidHTML}`
);
}
});
Expand All @@ -79,7 +85,7 @@ describe(".toHaveHtml()", () => {
// Throws on first child with valid HTML
for (const validHTML of validInnerHTMLs) {
expect(() => expect($divs).not.toHaveHtml(validHTML)).toThrow(
`Expected <div id="div-1"> not to have \`${validHTML}\``
`Expected ${strDivs} not to include ${validHTML}`
);
}
});
Expand Down
14 changes: 14 additions & 0 deletions src/methods/props/index.js
@@ -0,0 +1,14 @@
import $ from "../constructor";
import { act } from "react-dom/test-utils";

$.prototype.props = function (props) {
const container = this.nodes[0].closest("#root");
const component = container.component;
const root = container.root;
if (typeof props === "function") {
props = props(component.props);
}
act(() => root.render({ ...component, props }));
this.nodes = [...container.childNodes];
return this;
};
49 changes: 49 additions & 0 deletions src/methods/props/readme.md
@@ -0,0 +1,49 @@
### .props()

```js
.props(newProps) -> $
```

Rerender the component with the new specified props.

```js
const Demo = (props) => <div {...props}>world</div>;

it("can force-update the props on the root", () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
// Rerender with a new className on the top component:
demo.props({ className: "bye" });
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});
```

#### Parameters

`newProps`: the props to pass to the component in a new re-render. It can be either a plain object, or a function that will receive the old props and should return the new props.

#### Return

An instance of React Test that has re-rendered with the new props.

#### Examples

Update the prop className:

```js
const Demo = ({ className }) => <div className={className}>world</div>;

it("can inject new props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.props({ className: "bye" });
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});

it("can accept the old props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.props((p) => ({ className: p.className + "-bye" }));
expect(demo).toHaveHtml(`<div class="hello-bye">world</div>`);
});
```
39 changes: 39 additions & 0 deletions src/methods/props/test.js
@@ -0,0 +1,39 @@
import React, { useEffect, useState } from "react";
import $ from "../../";
import "babel-polyfill";

const Demo = ({ className }) => <div className={className}>world</div>;

describe(".props()", () => {
it("can render a simple string", async () => {
expect($(<Demo />)).toHaveHtml("<div>world</div>");
});

it("can inject new props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.props({ className: "bye" });
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});

it("can accept the old props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.props((p) => ({ className: p.className + "-bye" }));
expect(demo).toHaveHtml(`<div class="hello-bye">world</div>`);
});

it("has continuity", () => {
const Demo = ({ className }) => {
const [state, setState] = useState(0);
useEffect(() => {
setState((s) => s + 1);
}, [className]);
return <div>{state}</div>;
};
const demo = $(<Demo className="hello" />);
expect(demo).toHaveText(`1`);
demo.props({ className: "bye" });
expect(demo).toHaveText(`2`);
});
});
2 changes: 2 additions & 0 deletions src/methods/readme.md
Expand Up @@ -19,6 +19,8 @@ expect(button.text()).toBe("Hello world");
| [.parent()](#parent) | [.text()](#text) | |
| [.siblings()](#siblings) | | |

Others: [.props()](#props) and [.render()](#render).

Since the API is inspired on jQuery we call React Test `$`, but you can call it `render` or anything you prefer.

You _cannot_ modify the DOM directly with this library, but you _can_ trigger events that, depending on your React components, might modify the DOM:
Expand Down
2 changes: 2 additions & 0 deletions src/methods/render.js
Expand Up @@ -17,8 +17,10 @@ global.IS_REACT_ACT_ENVIRONMENT = true;
const render = (component) => {
const container = window.document.createElement("div");
container.id = "root";
container.component = component;
window.document.body.appendChild(container);
const root = createRoot(container);
container.root = root;
act(() => root.render(component));
return [...container.childNodes];
};
Expand Down
10 changes: 10 additions & 0 deletions src/methods/render/index.js
@@ -0,0 +1,10 @@
import $ from "../constructor";
import { act } from "react-dom/test-utils";

$.prototype.render = function (component) {
const container = this.nodes[0].closest("#root");
const root = container.root;
act(() => root.render(component));
this.nodes = [...container.childNodes];
return this;
};
55 changes: 55 additions & 0 deletions src/methods/render/readme.md
@@ -0,0 +1,55 @@
### .render()

```js
.render(newComponent) -> $
```

Rerender the component as specified with the new value, or unmount/mount the new component.

```js
const Demo = (props) => <div {...props}>world</div>;

it("can force-update the props on the root", () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
// Rerender with a new className on the top component:
demo.props(<Demo className="bye" />);
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});
```

> Note: if you only want to re-render changing the props, you might prefer using [`.props()`](#props).
#### Parameters

`newComponent`: the new component to render in place of the old one. If it's the same component, it'll trigger a re-render, otherwise it'll unmount the old one and mount the new one.

#### Return

An instance of React Test that has re-rendered with the new component.

#### Examples

Rerender with a new prop:

```js
const Demo = ({ className }) => <div className={className}>world</div>;

it("can inject new props", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.render(<Demo className="bye" />);
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});

it("can render a different component", () => {
const demo = $(<div>Hello</div>);
expect(demo).toHaveHtml(`<div>Hello</div>`);
demo.render(<span>Bye</span>);
expect(demo).toHaveHtml(`<span>Bye</span>`);
});
```

#### Notes

Do not reuse a root node (by using this `.render()`) more than necessary (e.g. testing rerenders); instead create a raw instance of a component with `$()` as usual for testing new components.
39 changes: 39 additions & 0 deletions src/methods/render/test.js
@@ -0,0 +1,39 @@
import React, { useEffect, useState } from "react";
import $ from "../../";
import "babel-polyfill";

const Demo = ({ className }) => <div className={className}>world</div>;

describe(".render()", () => {
it("can render a simple string", async () => {
expect($(<Demo />)).toHaveHtml("<div>world</div>");
});

it("can render with new data", async () => {
const demo = $(<Demo className="hello" />);
expect(demo).toHaveHtml(`<div class="hello">world</div>`);
demo.render(<Demo className="bye" />);
expect(demo).toHaveHtml(`<div class="bye">world</div>`);
});

it("has continuity", () => {
const Demo = ({ className }) => {
const [state, setState] = useState(0);
useEffect(() => {
setState((s) => s + 1);
}, [className]);
return <div>{state}</div>;
};
const demo = $(<Demo className="hello" />);
expect(demo).toHaveText(`1`);
demo.render(<Demo className="bye" />);
expect(demo).toHaveText(`2`);
});

it("can render a different component", () => {
const demo = $(<div>Hello</div>);
expect(demo).toHaveHtml(`<div>Hello</div>`);
demo.render(<span>Bye</span>);
expect(demo).toHaveHtml(`<span>Bye</span>`);
});
});

0 comments on commit 0d94f46

Please sign in to comment.