From 571d7a86ce5fc3e406566c03ed9c5b44bdd0195d Mon Sep 17 00:00:00 2001 From: Kevin Weber Date: Fri, 18 Feb 2022 17:42:56 +0100 Subject: [PATCH 1/7] Support getWrappingComponentRenderer & wrapWithWrappingComponent --- index.d.ts | 2 +- src/Adapter.ts | 38 +++++++++++++++++++++++++++----- src/MountRenderer.ts | 37 ++++++++++++++++++++++++++----- src/RootFinder.ts | 12 ++++++++++ src/wrapWithWrappingComponent.ts | 38 ++++++++++++++++++++++++++++++++ 5 files changed, 115 insertions(+), 12 deletions(-) create mode 100644 src/RootFinder.ts create mode 100644 src/wrapWithWrappingComponent.ts diff --git a/index.d.ts b/index.d.ts index 36756e1..3d23ffe 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; diff --git a/src/Adapter.ts b/src/Adapter.ts index 4db3544..5f67582 100644 --- a/src/Adapter.ts +++ b/src/Adapter.ts @@ -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; @@ -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': @@ -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); @@ -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() + * ``` + */ + options?: ShallowRendererProps + ) => { + return { + RootFinder: RootFinder, + node: wrapWithWrappingComponent(this.createElement, node, options), + }; + }; } diff --git a/src/MountRenderer.ts b/src/MountRenderer.ts index 90c9cce..404b6d9 100644 --- a/src/MountRenderer.ts +++ b/src/MountRenderer.ts @@ -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'; @@ -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. @@ -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) { @@ -70,6 +90,7 @@ export default class MountRenderer implements AbstractMountRenderer { ) { return null; } + return this._getNode(this._container); } @@ -135,4 +156,8 @@ export default class MountRenderer implements AbstractMountRenderer { }); return result; } + + getWrappingComponentRenderer() { + return this; + } } diff --git a/src/RootFinder.ts b/src/RootFinder.ts new file mode 100644 index 0000000..da2dbae --- /dev/null +++ b/src/RootFinder.ts @@ -0,0 +1,12 @@ +import { Component } from 'preact'; + +export default class RootFinder extends Component<{ context: any }> { + // I'm not sure if this is needed… It might help with legacy context đŸ¤· + getChildContext() { + return this.props.context; + } + + render() { + return this.props.children; + } +} diff --git a/src/wrapWithWrappingComponent.ts b/src/wrapWithWrappingComponent.ts new file mode 100644 index 0000000..e854250 --- /dev/null +++ b/src/wrapWithWrappingComponent.ts @@ -0,0 +1,38 @@ +import type { ReactElement } from 'react'; +import type { EnzymeAdapter, ShallowRendererProps } from 'enzyme'; +import RootFinder from './RootFinder.js'; +import { childElements } from './compat.js'; + +/** Based on the equivalent function in `enzyme-adapter-utils` */ +export default function wrapWithWrappingComponent( + createElement: EnzymeAdapter['createElement'], + node: ReactElement, + options: ShallowRendererProps = {} +) { + const { wrappingComponent, wrappingComponentProps = {}, context } = 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` + nodeWithValidChildren = Object.assign({}, nodeWithValidChildren); + nodeWithValidChildren.props.children = childElements(nodeWithValidChildren); + } + + const rootFinderContext = + (wrappingComponentProps as { context: any })?.context || context; + const rootFinderProps = rootFinderContext + ? { context: rootFinderContext } + : null; + + return createElement( + wrappingComponent, + wrappingComponentProps, + createElement(RootFinder, rootFinderProps, nodeWithValidChildren) + ); +} From 829fae46557b13d3fc1efe7fbcf4dbb9d9c53348 Mon Sep 17 00:00:00 2001 From: Kevin Weber Date: Fri, 18 Feb 2022 17:43:18 +0100 Subject: [PATCH 2/7] Add tests --- test/Adapter_test.tsx | 50 ++++++++++++++++++ test/MountRenderer_test.tsx | 37 +++++++++++++ test/integration_test.tsx | 102 ++++++++++++++++++++++++++++++++---- 3 files changed, 180 insertions(+), 9 deletions(-) diff --git a/test/Adapter_test.tsx b/test/Adapter_test.tsx index d54e103..f3d6ba0 100644 --- a/test/Adapter_test.tsx +++ b/test/Adapter_test.tsx @@ -215,4 +215,54 @@ describe('Adapter', () => { }); }); }); + + describe('#wrapWithWrappingComponent', () => { + function Button() { + return ; + } + + function WrappingComponent({ + children, + ...wrappingComponentProps + }: { + children: preact.ComponentChildren; + }) { + return
{children}
; + } + + it('returns original component when not wrapped', () => { + const button =