Skip to content

Commit

Permalink
Merge pull request #157 from kevinweber/kw--wrapWithWrappingComponent
Browse files Browse the repository at this point in the history
Support wrappingComponent & wrappingComponentProps
  • Loading branch information
robertknight committed Feb 24, 2022
2 parents 0dfc375 + 9fc65e2 commit 4d51be8
Show file tree
Hide file tree
Showing 10 changed files with 352 additions and 25 deletions.
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ declare module 'enzyme' {
// Required methods.
createElement(
type: string | Function,
props: Object,
props: Object | null,
...children: ReactElement[]
): ReactElement;
createRenderer(options: AdapterOptions): Renderer;
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"@types/mocha": "^9.0.0",
"@types/sinon": "^10.0.6",
"chai": "^4.3.4",
"enzyme": "^3.8.0",
"enzyme": "^3.11.0",
"jsdom": "^16.0.1",
"minimist": "^1.2.0",
"mocha": "^9.1.3",
Expand All @@ -29,7 +29,7 @@
"yalc": "^1.0.0-pre.34"
},
"peerDependencies": {
"enzyme": "^3.8.0",
"enzyme": "^3.11.0",
"preact": "^10.0.0"
},
"scripts": {
Expand Down
38 changes: 33 additions & 5 deletions src/Adapter.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import type { AdapterOptions, RSTNode } from 'enzyme';
import type {
AdapterOptions,
MountRendererProps,
RSTNode,
ShallowRendererProps,
} from 'enzyme';
import enzyme from 'enzyme';
import type { ReactElement } from 'react';
import { VNode, h } from 'preact';
import type { VNode } from 'preact';
import { h } from 'preact';

import MountRenderer from './MountRenderer.js';
import ShallowRenderer from './ShallowRenderer.js';
import StringRenderer from './StringRenderer.js';
import { rstNodeFromElement } from './preact10-rst.js';
import wrapWithWrappingComponent from './wrapWithWrappingComponent.js';
import RootFinder from './RootFinder.js';

export const { EnzymeAdapter } = enzyme;

Expand All @@ -31,13 +39,13 @@ export default class Adapter extends EnzymeAdapter {
this.nodeToElement = this.nodeToElement.bind(this);
}

createRenderer(options: AdapterOptions) {
createRenderer(options: AdapterOptions & MountRendererProps) {
switch (options.mode) {
case 'mount':
// The `attachTo` option is only supported for DOM rendering, for
// consistency with React, even though the Preact adapter could easily
// support it for shallow rendering.
return new MountRenderer({ container: options.attachTo });
return new MountRenderer({ ...options, container: options.attachTo });
case 'shallow':
return new ShallowRenderer();
case 'string':
Expand Down Expand Up @@ -91,7 +99,7 @@ export default class Adapter extends EnzymeAdapter {

createElement(
type: string | Function,
props: Object,
props: Object | null,
...children: ReactElement[]
) {
return h(type as any, props, ...children);
Expand All @@ -100,4 +108,24 @@ export default class Adapter extends EnzymeAdapter {
elementToNode(el: ReactElement): RSTNode {
return rstNodeFromElement(el as VNode) as RSTNode;
}

// This function is only called during shallow rendering
wrapWithWrappingComponent = (
node: ReactElement,
/**
* Tip:
* The use of `wrappingComponent` and `wrappingComponentProps` is discouraged.
* Using those props complicates a potential future migration to a different testing library.
* Instead, wrap a component like this:
* ```
* shallow(<Wrapper><Component/></Wrapper>).dive()
* ```
*/
options?: ShallowRendererProps
) => {
return {
RootFinder: RootFinder,
node: wrapWithWrappingComponent(this.createElement, node, options),
};
};
}
37 changes: 31 additions & 6 deletions src/MountRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type { MountRenderer as AbstractMountRenderer, RSTNode } from 'enzyme';
import { VNode, h } from 'preact';
import type {
MountRenderer as AbstractMountRenderer,
MountRendererProps,
RSTNode,
} from 'enzyme';
import { VNode, h, createElement } from 'preact';
import type { ReactElement } from 'react';
import { act } from 'preact/test-utils';

import { render } from './compat.js';
Expand All @@ -14,7 +19,7 @@ import { getDisplayName, withReplacedMethod } from './util.js';

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

export interface Options {
export interface Options extends MountRendererProps {
/**
* The container element to render into.
* If not specified, a detached element (not connected to the body) is used.
Expand All @@ -34,17 +39,32 @@ function constructEvent(type: string, init: EventInit) {
export default class MountRenderer implements AbstractMountRenderer {
private _container: HTMLElement;
private _getNode: typeof getNode;
private _options: Options;

constructor({ container }: Options = {}) {
constructor(options: Options = {}) {
installDebounceHook();

this._container = container || document.createElement('div');
this._container = options.container || document.createElement('div');
this._getNode = getNode;
this._options = options;
}

render(el: VNode, context?: any, callback?: () => any) {
act(() => {
render(el, this._container);
if (!this._options.wrappingComponent) {
render(el, this._container);
return;
}

// `this._options.wrappingComponent` is only available during mount-rendering,
// even though ShallowRenderer uses an instance of MountRenderer under the hood.
// For shallow-rendered components, we need to utilize `wrapWithWrappingComponent`.
const wrappedComponent: ReactElement = createElement(
this._options.wrappingComponent,
this._options.wrappingComponentProps || null,
el
);
render(wrappedComponent, this._container);
});

if (callback) {
Expand All @@ -70,6 +90,7 @@ export default class MountRenderer implements AbstractMountRenderer {
) {
return null;
}

return this._getNode(this._container);
}

Expand Down Expand Up @@ -135,4 +156,8 @@ export default class MountRenderer implements AbstractMountRenderer {
});
return result;
}

getWrappingComponentRenderer() {
return this;
}
}
7 changes: 7 additions & 0 deletions src/RootFinder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Component } from 'preact';

export default class RootFinder extends Component {
render() {
return this.props.children;
}
}
39 changes: 39 additions & 0 deletions src/wrapWithWrappingComponent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { ReactElement } from 'react';
import type { EnzymeAdapter, ShallowRendererProps } from 'enzyme';
import RootFinder from './RootFinder.js';
import { childElements } from './compat.js';
import { cloneElement } from 'preact';

/** Based on the equivalent function in `enzyme-adapter-utils` */
export default function wrapWithWrappingComponent(
createElement: EnzymeAdapter['createElement'],
node: ReactElement,
options: ShallowRendererProps = {}
) {
const { wrappingComponent, wrappingComponentProps = {} } = options;

if (!wrappingComponent) {
return node;
}

let nodeWithValidChildren = node;

if (typeof nodeWithValidChildren.props.children === 'string') {
// This prevents an error when `.dive()` is used:
// `TypeError: ShallowWrapper::dive() can only be called on components`.
// ---------------------------------------------------------------------
// VNode before: `{ type: Widget, props: { children: 'test' }, ... }`
// VNode after: `{ type: Widget, props: { children: ['test'] }, ... }`
nodeWithValidChildren = cloneElement(
nodeWithValidChildren,
nodeWithValidChildren.props,
childElements(nodeWithValidChildren)
);
}

return createElement(
wrappingComponent,
wrappingComponentProps,
createElement(RootFinder, null, nodeWithValidChildren)
);
}
50 changes: 50 additions & 0 deletions test/Adapter_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -215,4 +215,54 @@ describe('Adapter', () => {
});
});
});

describe('#wrapWithWrappingComponent', () => {
function Button() {
return <button>Click me</button>;
}

function WrappingComponent({
children,
...wrappingComponentProps
}: {
children: preact.ComponentChildren;
}) {
return <div {...wrappingComponentProps}>{children}</div>;
}

it('returns original component when not wrapped', () => {
const button = <Button />;
const adapter = new Adapter();
const hostNode = adapter.wrapWithWrappingComponent(button);

assert.ok(hostNode);
assert.typeOf(hostNode.RootFinder, 'function');
assert.deepEqual(hostNode.node, button);
});

it('returns wrapped component', () => {
const button = <Button />;
const wrappingComponentProps = { foo: 'bar' };
const wrappedComponent = (
<WrappingComponent {...wrappingComponentProps}>
{button}
</WrappingComponent>
);

const adapter = new Adapter();
const hostNode = adapter.wrapWithWrappingComponent(button, {
wrappingComponent: WrappingComponent,
wrappingComponentProps,
});

assert.ok(hostNode);

const rootFinderInHostNode = hostNode.node.props.children.type;
assert.equal(hostNode.RootFinder, rootFinderInHostNode);

const buttonInWrappedComponent = wrappedComponent.props.children;
const buttonInHostNode = hostNode.node.props.children.props.children;
assert.equal(buttonInHostNode, buttonInWrappedComponent);
});
});
});
30 changes: 30 additions & 0 deletions test/MountRenderer_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,34 @@ describe('MountRenderer', () => {
assert.equal(result, 'test');
});
});

describe('getNode', () => {
it('can get the node', () => {
const renderedTree = [
{
nodeType: 'host',
type: 'div',
props: {},
key: null,
ref: null,
instance: document.createElement('span'),
rendered: [],
},
];

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

const result = renderer.getWrappingComponentRenderer();

assert.equal(result.getNode()?.rendered.length, 1);

const resultInstance = (result.getNode()?.rendered[0] as RSTNode)
.instance;
const expectedInstance = renderedTree[0].instance;

assert.equal(resultInstance.outerHTML, expectedInstance.outerHTML);
});
});
});
Loading

0 comments on commit 4d51be8

Please sign in to comment.