-
-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathMountRenderer.ts
More file actions
176 lines (152 loc) · 5 KB
/
Copy pathMountRenderer.ts
File metadata and controls
176 lines (152 loc) · 5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
import type {
MountRenderer as AbstractMountRenderer,
MountRendererProps,
RSTNode,
} from 'enzyme';
import type { VNode } from 'preact';
import { createElement } from 'preact';
import { act } from 'preact/test-utils';
import type { PreactAdapterOptions } from './Adapter.js';
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';
export type EventDetails = { [prop: string]: any };
export interface Options extends MountRendererProps, PreactAdapterOptions {
/**
* The container element to render into.
* If not specified, a detached element (not connected to the body) is used.
*/
container?: HTMLElement;
}
function constructEvent(type: string, init: EventInit) {
const meta = eventMap[type];
const defaultInit = meta?.defaultInit ?? {};
return new Event(type, {
...defaultInit,
...init,
});
}
export default class MountRenderer implements AbstractMountRenderer {
private _container: HTMLElement;
private _getNode: typeof getNode;
private _options: Options;
constructor(options: Options = {}) {
installDebounceHook();
this._container = options.container || document.createElement('div');
this._getNode = getNode;
this._options = options;
}
render(el: VNode, context?: any, callback?: () => any) {
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() {
render(null, this._container);
}
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: RSTNode[], rootNode: RSTNode, error: any) {
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: RSTNode, eventName: string, args: EventDetails = {}) {
let hostNode: Node;
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 = {} as EventInit;
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: () => any) {
fn();
}
container() {
return this._container;
}
wrapInvoke(callback: () => any) {
let result;
act(() => {
result = callback();
});
return result;
}
getWrappingComponentRenderer() {
return this;
}
}