UNPKG

enzyme-adapter-preact-pure

Version:

Enzyme adapter for Preact

240 lines (239 loc) 9.78 kB
import { Component, options as rawOptions, isValidElement } from 'preact'; import { getComponent } from '../../../src/preact10-internals.js'; import { diffComponent, commitRoot, unmount, shallowSetState, shallowForceUpdate, skipUpdateSymbol } from './preact10-shallow-diff.js'; const options = rawOptions; let shallowOptionsInstalled = false; /** Setup options necessary for shallow rendering */ function installOptionsForShallowRender() { if (shallowOptionsInstalled) { return; } /** * Eagerly set _preactShallowRenderer on the component instance so later code * (e.g. the diffed option later in this function) can inspect it to know that * this component is being shallow rendered and can respond accordingly */ const prevRender = options.__r; options.__r = vnode => { prevRender(vnode); const component = getComponent(vnode); if (component && PreactShallowRenderer.current) { component._preactShallowRenderer = PreactShallowRenderer.current; } }; /** * If this component is being shallow rendered, remove all hook effect * callbacks to match how Enzyme shallow render's React */ const prevDiffed = options.diffed; options.diffed = vnode => { const component = getComponent(vnode); if (component && component._preactShallowRenderer) { removeEffectCallbacks(vnode); } prevDiffed?.(vnode); }; shallowOptionsInstalled = true; } /** * Replace existing setState and forceUpdate implementations with new * implementations that detect if a component has been shallow rendered and * ensures renders caused by setState/forceUpdate are also shallow rendered. * * Here, the _preactShallowRenderer property does something similar as React * Component's `updater` property. React Component's `updater` property contains * a reference to the implementations of `setState`, `forceUpdate`, etc. and it * is set by the code that rendered the component (e.g. ReactDOM). Here the * presence of the `_preactShallowRenderer` property (set by the shallow * renderer) indicates `setState` and `forceUpdate` should call into our custom * shallow implementation. */ function installShallowComponentHooks() { const prevSetState = Component.prototype.setState; Component.prototype.setState = function (update, callback) { if (this._preactShallowRenderer) { shallowSetState.call(this, update, callback); } else { prevSetState.call(this, update, callback); } }; const prevForceUpdate = Component.prototype.forceUpdate; Component.prototype.forceUpdate = function (callback) { if (this._preactShallowRenderer) { shallowForceUpdate.call(this, callback); } else { prevForceUpdate.call(this, callback); } }; } /** * This class mirrors ReactShallowRenderer for the purpose of shallow rendering * Preact components. It implements the same API and passes almost the exact * same test suite * * https://github.com/enzymejs/react-shallow-renderer */ export default class PreactShallowRenderer { constructor() { /** * The previous element that was shallowed rendered. It is used to diff an * element that gets updated through props or a setState call * * If render was given a memoed element, this holds the component that was * wrapped in memo. It is used if `setState` is called on the component and it * needs to be diffed directly. */ this._prevElement = null; /** * If render was given a memoed component to render, this is the previous * memoed element that will be used if that memoed element is rerendered */ this._memoElement = null; /** * The component instance that was shallow rendered. If render was given a * memoed element, then this is the instance of the component that was memoed */ this._componentInstance = null; /** * The result of the last call to render. */ this._rendered = null; installShallowComponentHooks(); installOptionsForShallowRender(); this._reset(); } getMountedInstance() { return this._componentInstance; } getRenderOutput() { return this._rendered; } render(element, context = {}) { assertIsComponentVNode(element); if (PreactShallowRenderer.current) { return; } // Determine if we need to diff against a previous render or if this is a // new render. If the given element type matches any of the previously // rendered elements, use it as the old VNode to diff against. let oldVNode; if (this._memoElement?.type === element.type) { oldVNode = this._memoElement; } else if (this._prevElement?.type === element.type) { oldVNode = this._prevElement; } else { // No matching element was found or this is the first render. Start with a // fresh state and reset the renderer. this._reset(); oldVNode = {}; } PreactShallowRenderer.current = this; try { const commitQueue = []; this._rendered = this._diffComponent(element, oldVNode, context, commitQueue); commitRoot(commitQueue, element); this._componentInstance = getComponent(this._prevElement // this._diffComponent sets _prevElement for us ); this._componentInstance._preactShallowRenderer = this; } finally { PreactShallowRenderer.current = null; } return this._rendered; } unmount() { PreactShallowRenderer.current = this; if (this._prevElement) { unmount(this._prevElement); } if (this._memoElement) { unmount(this._memoElement); } PreactShallowRenderer.current = null; this._reset(); } _reset() { this._prevElement = null; this._memoElement = null; this._componentInstance = null; this._rendered = null; PreactShallowRenderer.current = null; } _diffComponent(newVNode, oldVNode, globalContext, commitQueue) { // React's memo function wraps a component. It doesn't create a new VNode in // the virtual tree. To be compatible with React's shallow renderer, we'll // try to mimic this behavior so shallow Enzyme tests don't need to be aware // of this subtle difference between Preact and React. Further, how Preact // implements Memo today may change in the future so we'll try to avoid // exposing that implementation detail to users. // // To mimic React's behavior here, we'll render through memo components. If // `render` is given an element representing a Memoed component, we will // render Memo and the component that it memos (assuming the Memo doesn't // instruct us to skip updating). The public methods on this class should // never return references to the Memo component, but always the component // that it is memoizing. let renderResult; if (isMemo(newVNode)) { this._memoElement = newVNode; renderResult = diffComponent(newVNode, oldVNode, globalContext, commitQueue); if (renderResult == skipUpdateSymbol) { // The memo component told us not to update, so return the result of the // previous render return this._rendered; } if (isComponentVNode(renderResult)) { // The memo component updated, so let's setup to update and render the // component it returned newVNode = renderResult; oldVNode = this._prevElement ?? {}; } else { throw new Error(`Memo rendered a non-component element. Only memo'ed components is currently supported for shallow rendering`); } } this._prevElement = newVNode; renderResult = diffComponent(newVNode, oldVNode, globalContext, commitQueue); // If rendering this component told us not to update, then return the result // of the previous render. Else, return the result of this render return renderResult === skipUpdateSymbol ? this._rendered : renderResult; } } PreactShallowRenderer.createRenderer = function () { return new PreactShallowRenderer(); }; /** The current rendering instance of PreactShallowRenderer */ PreactShallowRenderer.current = null; function isComponentVNode(element) { return isValidElement(element) && typeof element.type === 'function'; } function assertIsComponentVNode(element) { if (!isValidElement(element)) { throw new Error(`PreactShallowRenderer render(): Invalid component element. ${typeof element === 'function' ? 'Instead of passing a component class, make sure to instantiate it by passing it to Preact.createElement.' : ''}`); } // Show a special message for host elements since it's a common case. if (typeof element.type === 'string') { throw new Error(`PreactShallowRenderer render(): Shallow rendering works only with custom components, not primitives (${element.type}). Instead of calling \`.render(el)\` and inspecting the rendered output, look at \`el.props\` directly instead.`); } if (typeof element.type !== 'function') { throw new Error(`PreactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was \`${Array.isArray(element.type) ? 'array' : element.type === null ? 'null' : typeof element.type}\`.`); } } /** Determine if a VNode is a Memo wrapper */ function isMemo(node) { if (node?.type && typeof node.type === 'function') { const nodeType = node.type; return nodeType.__f === true && nodeType.displayName?.startsWith('Memo('); } return false; } function removeEffectCallbacks(vnode) { if (vnode.__c) { const c = vnode.__c; // Filter out useLayoutEffects and useInsertionEffect so they aren't invoked c.__h = c.__h.filter(cb => cb.__ ? false : true); // Clear out the list of useEffects so they aren't invoked const hooks = c.__H; if (hooks) { hooks.__h = []; } } }