enzyme-adapter-preact-pure
Version:
Enzyme adapter for Preact
128 lines (127 loc) • 4.9 kB
JavaScript
import { h, createElement } from 'preact';
import { act } from 'preact/test-utils';
import { render } from './compat.js';
import { installHook as installDebounceHook, flushRenders, } from './debounce-render-hook.js';
import { eventMap } from './event-map.js';
import { getLastVNodeRenderedIntoContainer } from './preact10-internals.js';
import { getNode } from './preact10-rst.js';
import { getDisplayName, nodeToHostNode, withReplacedMethod } from './util.js';
function constructEvent(type, init) {
const meta = eventMap[type];
const defaultInit = meta?.defaultInit ?? {};
return new Event(type, {
...defaultInit,
...init,
});
}
export default class MountRenderer {
constructor(options = {}) {
installDebounceHook();
this._container = options.container || document.createElement('div');
this._getNode = getNode;
this._options = options;
}
render(el, context, callback) {
act(() => {
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 = createElement(this._options.wrappingComponent, this._options.wrappingComponentProps || null, el);
render(wrappedComponent, this._container);
});
if (callback) {
callback();
}
}
unmount() {
// A custom tag name is used here to work around
// https://github.com/developit/preact/issues/1288.
render(h('unmount-me', {}), this._container);
this._container.innerHTML = '';
}
getNode() {
flushRenders();
const container = this._container;
if (
// If the root component rendered null then the only indicator that content
// has been rendered will be metadata attached to the container.
typeof getLastVNodeRenderedIntoContainer(container) === 'undefined') {
return null;
}
return this._getNode(this._container);
}
simulateError(nodeHierarchy, rootNode, error) {
const errNode = nodeHierarchy[0];
const render = () => {
// Modify the stack to match where the error is thrown. This makes
// debugging easier.
error.stack = new Error().stack;
throw error;
};
withReplacedMethod(errNode.instance, 'render', render, () => {
act(() => {
errNode.instance.forceUpdate();
});
});
}
simulateEvent(node, eventName, args = {}) {
let hostNode;
if (node.nodeType == 'host') {
hostNode = node.instance;
}
else if (this._options.simulateEventsOnComponents) {
const possibleHostNode = nodeToHostNode(node);
if (possibleHostNode == null) {
const name = getDisplayName(node);
throw new Error(`Cannot simulate event on "${name}" which is not a DOM element or contains no DOM element children. ` +
'Find a DOM element or Component that contains a DOM element in the output and simulate an event on that.');
}
hostNode = possibleHostNode;
}
else {
const name = getDisplayName(node);
throw new Error(`Cannot simulate event on "${name}" which is not a DOM element. ` +
'Find a DOM element in the output and simulate an event on that. ' +
'Or, enable the simulateEventsOnComponents option to enable this feature.');
}
// To be more faithful to a real browser, this should use the appropriate
// constructor for the event type. This implementation is good enough for
// many components though.
const { bubbles, composed, cancelable, ...extra } = args;
const init = {};
if (typeof bubbles === 'boolean') {
init.bubbles = bubbles;
}
if (typeof composed === 'boolean') {
init.composed = composed;
}
if (typeof cancelable === 'boolean') {
init.cancelable = cancelable;
}
const event = constructEvent(eventName, init);
Object.assign(event, extra);
act(() => {
hostNode.dispatchEvent(event);
});
}
batchedUpdates(fn) {
fn();
}
container() {
return this._container;
}
wrapInvoke(callback) {
let result;
act(() => {
result = callback();
});
return result;
}
getWrappingComponentRenderer() {
return this;
}
}