UNPKG

enzyme-adapter-preact-pure

Version:

Enzyme adapter for Preact

302 lines (300 loc) 10.2 kB
/** * This file includes modified copies of the Preact source and custom * implementations of Preact functions (e.g. setState) to perform a shallow * render * * The Preact source is copyrighted to Jason Miller and licensed under the MIT * License, found a the link below: * * https://github.com/preactjs/preact/blob/d4089df1263faab9b980a3493a4c7e986f254f8e/LICENSE */ import { Component, options as rawOptions } from 'preact'; const options = rawOptions; /** Symbol to return if a component indicates it should not update */ export const skipUpdateSymbol = Symbol('PreactShallowRenderer skip update'); /** * Shallowly render a component. Much of this function is copied directly from * the Preact 10 source. Differences to the original source are commented. */ export function diffComponent(newVNode, oldVNode, globalContext, commitQueue) { /* eslint-disable */ let tmp, newType = newVNode.type, parentDom = null; if (options.__b) options.__b(newVNode); // ===== START PREACT SOURCE COPY ===== let c, isNew, oldProps, clearProcessingException; let newProps = newVNode.props; // Necessary for createContext api. Setting this property will pass // the context value as `this.context` just for this component. tmp = newType.contextType; let provider = tmp && globalContext[tmp.__c]; let componentContext = tmp ? provider ? provider.props.value : tmp.__ : globalContext; // Get component and set it to `c` if (oldVNode.__c) { c = newVNode.__c = oldVNode.__c; clearProcessingException = c.__ = c.__E; } else { // Instantiate the new component if ('prototype' in newType && newType.prototype.render) { // @ts-ignore The check above verifies that newType is suppose to be constructed newVNode.__c = c = new newType(newProps, componentContext); // eslint-disable-line new-cap } else { // @ts-ignore Trust me, Component implements the interface we want newVNode.__c = c = new Component(newProps, componentContext); c.constructor = newType; c.render = doRender; } if (provider) provider.sub(c); c.props = newProps; if (!c.state) c.state = {}; c.context = componentContext; c.__n = globalContext; isNew = c.__d = true; c.__h = []; c._sb = []; } // Invoke getDerivedStateFromProps if (c.__s == null) { c.__s = c.state; } if (newType.getDerivedStateFromProps != null) { if (c.__s == c.state) { c.__s = assign({}, c.__s); } assign(c.__s, newType.getDerivedStateFromProps(newProps, c.__s)); } oldProps = c.props; // oldState = c.state; // Invoke pre-render lifecycle methods if (isNew) { if (newType.getDerivedStateFromProps == null && c.componentWillMount != null) { c.componentWillMount(); } // == SHALLOW RENDER CHANGE: Don't invoke CDM // if (c.componentDidMount != null) { // c._renderCallbacks.push(c.componentDidMount); // } } else { if (newType.getDerivedStateFromProps == null && newProps !== oldProps && c.componentWillReceiveProps != null) { c.componentWillReceiveProps(newProps, componentContext); } if (!c.__e && c.shouldComponentUpdate != null && c.shouldComponentUpdate(newProps, c.__s, componentContext) === false || newVNode.__v === oldVNode.__v) { c.props = newProps; c.state = c.__s; // More info about this here: https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8 if (newVNode.__v !== oldVNode.__v) c.__d = false; c.__v = newVNode; newVNode.__e = oldVNode.__e; newVNode.__k = oldVNode.__k; newVNode.__k?.forEach(vnode => { if (vnode) vnode.__ = newVNode; }); for (let i = 0; i < c._sb.length; i++) { c.__h.push(c._sb[i]); } c._sb = []; if (c.__h.length) { commitQueue.push(c); } // break outer; return skipUpdateSymbol; // === SHALLOW RENDER CHANGE: return skip update symbol } if (c.componentWillUpdate != null) { c.componentWillUpdate(newProps, c.__s, componentContext); } // == SHALLOW RENDER CHANGE: Don't invoke CDU // if (c.componentDidUpdate != null) { // c._renderCallbacks.push(() => { // c.componentDidUpdate(oldProps, oldState, snapshot); // }); // } } c.context = componentContext; c.props = newProps; c.__v = newVNode; c.__P = parentDom; let renderHook = options.__r, count = 0; if ('prototype' in newType && newType.prototype.render) { c.state = c.__s; c.__d = false; if (renderHook) renderHook(newVNode); tmp = c.render(c.props, c.state, c.context); for (let i = 0; i < c._sb.length; i++) { c.__h.push(c._sb[i]); } c._sb = []; } else { do { c.__d = false; if (renderHook) renderHook(newVNode); tmp = c.render(c.props, c.state, c.context); // Handle setState called in render, see #2553 c.state = c.__s; } while (c.__d && ++count < 25); } // Handle setState called in render, see #2553 c.state = c.__s; if (c.getChildContext != null) { globalContext = assign(assign({}, globalContext), c.getChildContext()); } // == SHALLOW RENDER CHANGE: Don't invoke gSBU // if (!isNew && c.getSnapshotBeforeUpdate != null) { // snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState); // } let renderResult = tmp; // == SHALLOW RENDER CHANGE: Don't skip Fragments returned from components // let isTopLevelFragment = // tmp != null && tmp.type === Fragment && tmp.key == null; // let renderResult = isTopLevelFragment ? tmp.props.children : tmp; // == SHALLOW RENDER CHANGE: Don't render children // diffChildren( // parentDom, // Array.isArray(renderResult) ? renderResult : [renderResult], // newVNode, // oldVNode, // globalContext, // isSvg, // excessDomChildren, // commitQueue, // oldDom, // isHydrating // ); c.base = newVNode.__e; // We successfully rendered this VNode, unset any stored hydration/bailout state: newVNode.__h = null; if (c.__h.length) { commitQueue.push(c); } if (clearProcessingException) { c.__E = c.__ = null; } c.__e = false; // ===== END PREACT SOURCE COPY ===== if (options.diffed) options.diffed(newVNode); return renderResult; } /** * Assign properties from `props` to `obj` * @template O, P The obj and props types * @param {O} obj The object to copy properties to * @param {P} props The object to copy properties from * @returns {O & P} */ function assign(obj, props) { // ===== COPIED FROM PREACT SOURCE ===== // @ts-ignore We change the type of `obj` to be `O & P` for (let i in props) obj[i] = props[i]; return obj; } /** The `.render()` method for a PFC backing instance. */ function doRender(props, state, context) { return this.constructor(props, context); } /** * @param {Array<import('../internal').Component>} commitQueue List of components * which have callbacks to invoke in commitRoot * @param {import('../internal').VNode} root */ export function commitRoot(commitQueue, root) { // ===== COPIED FROM PREACT SOURCE ===== if (options.__c) options.__c(root, commitQueue); commitQueue.some(c => { try { // @ts-ignore Reuse the commitQueue variable here so the type changes commitQueue = c.__h; c.__h = []; commitQueue.some(cb => { // @ts-ignore See above ts-ignore on commitQueue cb.call(c); }); } catch (e) { options.__e(e, c.__v, undefined, {}); } }); } export function unmount(vnode) { // ===== COPIED FROM PREACT SOURCE ===== let r; if (options.unmount) options.unmount(vnode); // == SHALLOW RENDER CHANGE: Don't invoke refs // if ((r = vnode.ref)) { // if (!r.current || r.current === vnode._dom) { // applyRef(r, null, parentVNode); // } // } if ((r = vnode.__c) != null) { if (r.componentWillUnmount) { try { r.componentWillUnmount(); } catch (e) { options.__e(e, vnode.__, undefined, undefined); } } r.base = r.__P = undefined; vnode.__c = null; } // == SHALLOW RENDER CHANGE: Don't unmount children // if ((r = vnode._children)) { // for (let i = 0; i < r.length; i++) { // if (r[i]) { // unmount( // r[i], // parentVNode, // skipRemove || typeof vnode.type !== 'function' // ); // } // } // } // == SHALLOW RENDER CHANGE: Don't remove DOM nodes // if (!skipRemove && vnode._dom != null) { // removeNode(vnode._dom); // } // Must be set to `undefined` to properly clean up `_nextDom` // for which `null` is a valid value. See comment in `create-element.js` vnode.__ = vnode.__e = vnode.__d = undefined; } export function shallowSetState(update, callback) { // ===== COPIED FROM PREACT SOURCE ===== Need to have our own version so our // own `enqueueRender` function is called. The one built-in to Preact always // calls the real client renderer // only clone state when copying to nextState the first time. let s; if (this.__s != null && this.__s !== this.state) { s = this.__s; } else { s = this.__s = assign({}, this.state); } if (typeof update == 'function') { // Some libraries like `immer` mark the current state as readonly, // preventing us from mutating it, so we need to clone it. See #2716 update = update(assign({}, s), this.props); } if (update) { assign(s, update); } // Skip update if updater function returned null if (update == null) return; if (this.__v) { if (callback) { this._sb.push(callback); } enqueueRender(this); } } export function shallowForceUpdate(callback) { // ===== COPIED FROM PREACT SOURCE ===== Need to have our own version so our // own `enqueueRender` function is called. The one built-in to Preact always // calls the real client renderer this.__e = true; if (callback) this.__h.push(callback); enqueueRender(this); } function enqueueRender(component) { let newVNode = assign({}, component.__v); newVNode.__v = NaN; component.__d = true; let renderer = component._preactShallowRenderer; renderer?.render(newVNode, component.__n); }