enzyme-adapter-preact-pure
Version:
Enzyme adapter for Preact
240 lines (239 loc) • 9.78 kB
JavaScript
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 = [];
}
}
}