Skip to content

Commit

Permalink
Add tests for "Disable rogue state updates" (#16)
Browse files Browse the repository at this point in the history
  • Loading branch information
sventschui committed Oct 14, 2020
1 parent 52120e1 commit 6001a26
Show file tree
Hide file tree
Showing 3 changed files with 218 additions and 10 deletions.
9 changes: 8 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export default function prepass(
// initialize components in dirty state so setState() doesn't enqueue re-rendering:
c.__d = true;
c.__v = vnode;
/* istanbul ignore else */
if (c.state === undefined) {
c.state = {};
}

// options.render was renamed to _render (mangled to __r)
if (options.render) options.render(vnode);
Expand Down Expand Up @@ -98,6 +102,9 @@ export default function prepass(
c.__v = vnode;
c.props = props;
c.context = context;
if (c.state === undefined) {
c.state = {};
}

// TODO: does react-ssr-prepass call the visitor before lifecycle hooks?
if (nodeName.getDerivedStateFromProps)
Expand All @@ -109,7 +116,7 @@ export default function prepass(

doRender = () => {
try {
return Promise.resolve(c.render(c.props, c.state || {}, c.context));
return Promise.resolve(c.render(c.props, c.state, c.context));
} catch (e) {
if (e && e.then) {
return e.then(doRender, doRender);
Expand Down
213 changes: 207 additions & 6 deletions src/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -244,16 +244,60 @@ describe("prepass", () => {
});

describe("hooks", () => {
it("it should support useState", async () => {
let setStateHoisted;
it("should not enqueue components for re-rendering when using setState", async () => {
let didUpdate = false;

// a re-render would invoke an array sort on the render queue, thus lets check nothing does so
const arraySort = jest.spyOn(Array.prototype, "sort");

function MyHookedComponent() {
const [state, setState] = useState("foo");
setStateHoisted = setState;

if (!didUpdate) {
didUpdate = true;
throw new Promise((resolve) => {
setState("bar");
resolve();
});
}

return <div>{state}</div>;
}

await prepass(<MyHookedComponent />);
const vnode = <MyHookedComponent />;

await prepass(vnode);
expect(arraySort).not.toHaveBeenCalled();

// let's test our test. If something changes in preact and no sort is executed
// before re-rendering our test would be false-positiv, thus we test that sort is called
// when c.__dirty is false

function MyHookedComponent2() {
const c = this;

const [state, setState] = useState("foo");

if (!didUpdate) {
didUpdate = true;
throw new Promise((resolve) => {
setState(() => {
c.__d = false;
return "bar";
});
resolve();
});
}

return <div>{state}</div>;
}

didUpdate = false;
const vnode2 = <MyHookedComponent2 />;

await prepass(vnode2);
expect(arraySort).toHaveBeenCalledTimes(1);
arraySort.mockRestore();
});

it("it should skip useEffect", async () => {
Expand Down Expand Up @@ -309,7 +353,7 @@ describe("prepass", () => {

await prepass(<Outer foo="bar" />);

expect(spy.mock.calls).toEqual([[{ foo: "bar" }, undefined]]);
expect(spy.mock.calls).toEqual([[{ foo: "bar" }, {}]]);
});

it("should call getDerivedStateFromProps on class components with initial state", async () => {
Expand Down Expand Up @@ -364,7 +408,7 @@ describe("prepass", () => {
await prepass(<Outer foo="bar" />);

expect(spyCWM.mock.calls).toEqual([]);
expect(spyGDSFP.mock.calls).toEqual([[{ foo: "bar" }, undefined]]);
expect(spyGDSFP.mock.calls).toEqual([[{ foo: "bar" }, {}]]);
});
});
});
Expand Down Expand Up @@ -844,6 +888,163 @@ describe("prepass", () => {
});
});

describe("Component", () => {
it("should default state to empty object", async () => {
class C1 extends Component {
render() {
return this.state.foo;
}
}
class C2 extends Component {
constructor(props, context) {
super(props, context);
this.state = { foo: "bar" };
}
render() {
return this.state.foo;
}
}

const spyC1render = jest.spyOn(C1.prototype, "render");
const spyC2render = jest.spyOn(C2.prototype, "render");

await prepass(
<Fragment>
<C1 />
<C2 />
</Fragment>
);
expect(spyC1render).toHaveBeenLastCalledWith({}, {}, {});
expect(spyC2render).toHaveBeenLastCalledWith({}, { foo: "bar" }, {});
});

it("should not enqueue components for re-rendering when using setState", async () => {
const setDirtyFromPreactCore = jest.fn();
const setDirtyFromPrepass = jest.fn();

class MyComponent extends Component {
constructor(props) {
super(props);
this.didUpdate = false;
}

render() {
if (!this.didUpdate) {
this.didUpdate = true;
throw new Promise((resolve) => {
this.setState({ foo: "didUpdate" });
resolve();
});
}

return <div>{this.state.foo}</div>;
}

get __d() {
return Boolean(this.dirty);
}

set __d(dirty) {
if (
// this checks whether the call comes from prepass or preact (core)
new Error().stack
.split("\n")[2]
.match(/^\s*at prepass \(.*\/src\/index\.js:[0-9]+:[0-9]+\)$/)
) {
// we want to force the failure case here to test that preact
// didn't change in a way invalidating our shady test method
if (!this.props.forceNotDirty) {
this.dirty = dirty;
setDirtyFromPrepass(dirty);
}
} else {
setDirtyFromPreactCore(dirty);
this.dirty = dirty;
}
}
}

await prepass(<MyComponent forceNotDirty={false} />);
// we expect that preact-ssr-prepass initializes the component as dirty to prevent
// the component to be added to preacts internal rendering queue
expect(setDirtyFromPrepass).toHaveBeenCalledTimes(1);
// we expect preact-core to not mark this component as dirty, as it already was dirty
expect(setDirtyFromPreactCore).toHaveBeenCalledTimes(0);

// now we test our test... sind this is quite a shady test method we need to make sure
// it is not false positive due to internal preact changes
await prepass(<MyComponent forceNotDirty={true} />);
// we expect that we successfully ignored the call of prepass to mark the component as dirty
// thus no additional call is expected here
expect(setDirtyFromPrepass).toHaveBeenCalledTimes(1);
// we expect that precat marks the component as dirty and thus adds it to its internal rendering queue
expect(setDirtyFromPreactCore).toHaveBeenCalledTimes(1);
});

it("should not enqueue components for re-rendering when using forceUpdate", async () => {
const setDirtyFromPreactCore = jest.fn();
const setDirtyFromPrepass = jest.fn();

class MyComponent extends Component {
constructor(props) {
super(props);
this.didUpdate = false;
}

render() {
if (!this.didUpdate) {
this.didUpdate = true;
throw new Promise((resolve) => {
this.forceUpdate();
resolve();
});
}

return <div>{this.state.foo}</div>;
}

get __d() {
return Boolean(this.dirty);
}

set __d(dirty) {
if (
// this checks whether the call comes from prepass or preact (core)
new Error().stack
.split("\n")[2]
.match(/^\s*at prepass \(.*\/src\/index\.js:[0-9]+:[0-9]+\)$/)
) {
// we want to force the failure case here to test that preact
// didn't change in a way invalidating our shady test method
if (!this.props.forceNotDirty) {
this.dirty = dirty;
setDirtyFromPrepass(dirty);
}
} else {
setDirtyFromPreactCore(dirty);
this.dirty = dirty;
}
}
}

await prepass(<MyComponent forceNotDirty={false} />);
// we expect that preact-ssr-prepass initializes the component as dirty to prevent
// the component to be added to preacts internal rendering queue
expect(setDirtyFromPrepass).toHaveBeenCalledTimes(1);
// we expect preact-core to not mark this component as dirty, as it already was dirty
expect(setDirtyFromPreactCore).toHaveBeenCalledTimes(0);

// now we test our test... sind this is quite a shady test method we need to make sure
// it is not false positive due to internal preact changes
await prepass(<MyComponent forceNotDirty={true} />);
// we expect that we successfully ignored the call of prepass to mark the component as dirty
// thus no additional call is expected here
expect(setDirtyFromPrepass).toHaveBeenCalledTimes(1);
// we expect that precat marks the component as dirty and thus adds it to its internal rendering queue
expect(setDirtyFromPreactCore).toHaveBeenCalledTimes(1);
});
});

describe("visitor", () => {
class MyClassComp extends Component {
render(props) {
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5648,9 +5648,9 @@ preact-render-to-string@^5.1.10:
pretty-format "^3.8.0"

preact@10:
version "10.5.2"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.2.tgz#4c07c27f4239666840e0d637ec7c110cfcae181d"
integrity sha512-4y2Q6kMiJtMONMJR7z+o8P5tGkMzVItyy77AXGrUdusv+dk4jwoS3KrpCBkFloY2xsScRJYwZQZrx89tTjDkOw==
version "10.5.4"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.5.4.tgz#1e4d148f949fa54656df6c9bc9218bd4e12016e3"
integrity sha512-u0LnVtL9WWF61RLzIbEsVFOdsahoTQkQqeRwyf4eWuLMFrxTH/C47tqcnizbUH54E4KG8UzuuZaMc9KarHmpqQ==

prelude-ls@~1.1.2:
version "1.1.2"
Expand Down

0 comments on commit 6001a26

Please sign in to comment.