Skip to content

Commit

Permalink
Flush effects after initial render
Browse files Browse the repository at this point in the history
Flush any effects scheduled with `useEffect` using the `act` helper
on the initial render.

This means that in a test a developer can write:

```
const wrapper = mount(<ComponentThatUsesEffects/>)
```

And know that effects will have been run before they start interacting
with the result wrapper.

See enzymejs/enzyme#2034 for corresponding change
to the React adapter.
  • Loading branch information
robertknight committed Apr 14, 2019
1 parent e95a673 commit 48fbb27
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 5 deletions.
24 changes: 21 additions & 3 deletions src/MountRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import { VNode, h } from 'preact';

import { getNode as getNodeClassic } from './preact8-rst';
import { getNode as getNodeV10 } from './preact10-rst';
import { getDisplayName, isPreact10 } from './util';
import { getDisplayName, isPreact10, withReplacedMethod } from './util';
import { render } from './compat';
import { getLastVNodeRenderedIntoContainer } from './preact10-internals';
import { withReplacedMethod } from './util';

type EventDetails = { [prop: string]: any };

Expand All @@ -18,6 +17,23 @@ export interface Options {
container?: HTMLElement;
}

let actImpl: (cb: () => any) => void;
if (isPreact10()) {
actImpl = require('preact/test-utils').act;
}

/**
* Invoke `callback` and then immediately flush any effects or pending renders
* which were scheduled during the callback.
*/
function act(callback: () => any) {
if (actImpl) {
actImpl(callback);
} else {
callback();
}
}

export default class MountRenderer implements EnzymeRenderer {
private _container: HTMLElement;
private _getNode: typeof getNodeClassic;
Expand All @@ -33,7 +49,9 @@ export default class MountRenderer implements EnzymeRenderer {
}

render(el: VNode, context?: any, callback?: () => any) {
render(el, this._container);
act(() => {
render(el, this._container);
});
const rootNode = this._getNode(this._container);

// Monkey-patch the component's `setState` to make it force an update after
Expand Down
46 changes: 45 additions & 1 deletion test/MountRenderer_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { assert } from 'chai';
import * as sinon from 'sinon';

import MountRenderer from '../src/MountRenderer';
import { isPreact10 } from '../src/util';

describe('MountRenderer', () => {
describe('#render', () => {
Expand All @@ -18,7 +19,7 @@ describe('MountRenderer', () => {
it('renders the element into the provided container', () => {
const container = document.createElement('div');
const renderer = new MountRenderer({ container });
renderer.render(<button/>);
renderer.render(<button />);
assert.ok(container.querySelector('button'));
});

Expand Down Expand Up @@ -55,6 +56,49 @@ describe('MountRenderer', () => {
const container = renderer.getNode()!.instance.base!;
assert.equal(container.innerHTML, '1');
});

if (isPreact10()) {
const { useEffect, useLayoutEffect } = require('preact/hooks');

it('executes effect hooks on initial render', () => {
let effectCount = 0;
let layoutEffectCount = 0;

function TestComponent() {
useLayoutEffect(() => {
++layoutEffectCount;
});
useEffect(() => {
++effectCount;
});
return null;
}

const renderer = new MountRenderer();
renderer.render(<TestComponent />);

assert.equal(layoutEffectCount, 1);
assert.equal(effectCount, 1);
});

it('executes hook cleanup on unmount', () => {
let effectRemoved = false;

function TestComponent() {
useEffect(() => {
return () => (effectRemoved = true);
});
return null;
}

const renderer = new MountRenderer();
renderer.render(<TestComponent />);

assert.equal(effectRemoved, false);
renderer.unmount();
assert.equal(effectRemoved, true);
});
}
});

describe('#unmount', () => {
Expand Down
4 changes: 3 additions & 1 deletion test/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import minimist from 'minimist';

// Setup DOM globals required by Preact rendering.
function setupJSDOM() {
const dom = new JSDOM();
// Enable `requestAnimationFrame` which Preact 10 uses for scheduling hooks.
const dom = new JSDOM('', { pretendToBeVisual: true });
const g = global as any;
g.Event = dom.window.Event;
g.Node = dom.window.Node;
g.window = dom.window;
g.document = dom.window.document;
g.requestAnimationFrame = dom.window.requestAnimationFrame;
}
setupJSDOM();

Expand Down

0 comments on commit 48fbb27

Please sign in to comment.