This repository has been archived by the owner on Dec 16, 2023. It is now read-only.
/
jsdom_patches.js
212 lines (183 loc) · 7.19 KB
/
jsdom_patches.js
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// Fix things that JSDOM doesn't do quite right.
const DOM = require('./index');
DOM.HTMLDocument.prototype.__defineGetter__('scripts', function() {
return new DOM.HTMLCollection(this, ()=> this.querySelectorAll('script'));
});
// Default behavior for clicking on links: navigate to new URL if specified.
DOM.HTMLAnchorElement.prototype._eventDefaults =
Object.assign({}, DOM.HTMLElement.prototype._eventDefaults);
DOM.HTMLAnchorElement.prototype._eventDefaults.click = function(event) {
const anchor = event.target;
if (!anchor.href)
return;
const { window } = anchor.ownerDocument;
const { browser } = window;
// Decide which window to open this link in
switch (anchor.target || '_self') {
case '_self': { // navigate same window
window.location = anchor.href;
break;
}
case '_parent': { // navigate parent window
window.parent.location = anchor.href;
break;
}
case '_top': { // navigate top window
window.top.location = anchor.href;
break;
}
default: { // open named window
browser.tabs.open({ name: anchor.target, url: anchor.href });
break;
}
}
browser.emit('link', anchor.href, anchor.target || '_self');
};
// Attempt to load the image, this will trigger a 'load' event when succesful
// jsdom seemed to only queue the 'load' event
DOM.HTMLImageElement.prototype._attrModified = function(name, value, oldVal) {
if (name === 'src') {
const src = DOM.resourceLoader.resolve(this._ownerDocument, value);
if (this.src !== src)
DOM.resourceLoader.load(this, value);
}
};
// Implement insertAdjacentHTML
DOM.HTMLElement.prototype.insertAdjacentHTML = function(position, html) {
const { parentNode } = this;
const container = this.ownerDocument.createElementNS('http://www.w3.org/1999/xhtml', '_');
container.innerHTML = html;
switch (position.toLowerCase()) {
case 'beforebegin': {
while (container.firstChild)
parentNode.insertBefore(container.firstChild, this);
break;
}
case 'afterbegin': {
let firstChild = this.firstChild;
while (container.lastChild)
firstChild = this.insertBefore(container.lastChild, firstChild);
break;
}
case 'beforeend': {
while (container.firstChild)
this.appendChild(container.firstChild);
break;
}
case 'afterend': {
let nextSibling = this.nextSibling;
while (container.lastChild)
nextSibling = parentNode.insertBefore(container.lastChild, nextSibling);
break;
}
}
};
// Implement documentElement.contains
// e.g., if(document.body.contains(el)) { ... }
// See https://developer.mozilla.org/en-US/docs/DOM/Node.contains
DOM.Node.prototype.contains = function(otherNode) {
// DDOPSON-2012-08-16 -- This implementation is stolen from Sizzle's
// implementation of 'contains' (around line 1402).
// We actually can't call Sizzle.contains directly:
// * Because we define Node.contains, Sizzle will configure it's own
// "contains" method to call us. (it thinks we are a native browser
// implementation of "contains")
// * Thus, if we called Sizzle.contains, it would form an infinite loop.
// Instead we use Sizzle's fallback implementation of "contains" based on
// "compareDocumentPosition".
return !!(this.compareDocumentPosition(otherNode) & 16);
};
// Support for opacity style property.
Object.defineProperty(DOM.CSSStyleDeclaration.prototype, 'opacity', {
get() {
const opacity = this.getPropertyValue('opacity');
return Number.isFinite(opacity) ? opacity.toString() : '';
},
set(opacity) {
if (opacity === null || opacity === undefined || opacity === '') {
this.removeProperty('opacity');
} else {
const value = parseFloat(opacity);
if (isFinite(value))
this._setProperty('opacity', value);
}
}
});
// Wrap dispatchEvent to support _windowInScope and error handling.
const jsdomDispatchEvent = DOM.EventTarget.prototype.dispatchEvent;
DOM.EventTarget.prototype.dispatchEvent = function(event) {
// Could be node, window or document
const document = this._ownerDocument || this.document || this;
const window = document.parentWindow;
// Fail miserably on objects that don't have ownerDocument: nodes and XHR
// request have those
const { browser } = window;
browser.emit('event', event, this);
const originalInScope = browser._windowInScope;
try {
// The current window, postMessage and window.close need this
browser._windowInScope = window;
// Inline event handlers rely on window.event
window.event = event;
return jsdomDispatchEvent.call(this, event);
} finally {
delete window.event;
browser._windowInScope = originalInScope;
}
};
// Wrap raise to catch and propagate all errors to window
const jsdomRaise = DOM.Document.prototype.raise;
DOM.Document.prototype.raise = function(type, message, data) {
jsdomRaise.call(this, type, message, data);
const error = data && (data.exception || data.error);
if (!error)
return;
const document = this;
const window = document.parentWindow;
// Deconstruct the stack trace and strip the Zombie part of it
// (anything leading to this file). Add the document location at
// the end.
const partial = [];
// "RangeError: Maximum call stack size exceeded" doesn't have a stack trace
if (error.stack) {
for (let line of error.stack.split('\n')) {
if (~line.indexOf('contextify/lib/contextify.js'))
break;
partial.push(line);
}
}
partial.push(` in ${document.location.href}`);
error.stack = partial.join('\n');
window._eventQueue.onerror(error);
};
// Fix resource loading to keep track of in-progress requests. Need this to wait
// for all resources (mainly JavaScript) to complete loading before terminating
// browser.wait.
DOM.resourceLoader.load = function(element, href, callback) {
const document = element.ownerDocument;
const window = document.parentWindow;
const tagName = element.tagName.toLowerCase();
const loadResource = document.implementation._hasFeature('FetchExternalResources', tagName);
const url = DOM.resourceLoader.resolve(document, href);
if (loadResource) {
// This guarantees that all scripts are executed in order, must add to the
// JSDOM queue before we add to the Zombie event queue.
const enqueued = this.enqueue(element, callback && callback.bind(element), url);
window._eventQueue.http('GET', url, { target: element }, (error, response)=> {
// Since this is used by resourceLoader that doesn't check the response,
// we're responsible to turn anything other than 2xx/3xx into an error
if (response && response.statusCode >= 400)
error = new Error(`Server returned status code ${response.statusCode} from ${url}`);
// Make sure browser gets a hold of this error and adds it to error list
// This is necessary since resource loading (CSS, image, etc) does nothing
// with the callback error
if (error) {
const event = document.createEvent('HTMLEvents');
event.initEvent('error', false, false);
event.error = error;
element.dispatchEvent(event);
} else
enqueued(null, response.body);
});
}
};