Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
80629 views
1
/**
2
* Copyright (c) 2014, Facebook, Inc. All rights reserved.
3
*
4
* This source code is licensed under the BSD-style license found in the
5
* LICENSE file in the root directory of this source tree. An additional grant
6
* of patent rights can be found in the PATENTS file in the same directory.
7
*/
8
'use strict';
9
10
/**
11
* This file contains various hacks and tweaks that were necessary at some
12
* point to get jsdom to behave correctly.
13
*
14
* TODO(benjamn) Periodically purge unnecessary stuff from this file
15
* and/or create upstream pull requests for obvious bugs.
16
*/
17
18
// If this require starts failing in the future, it might be because
19
// cssstyle has matured enough that the hacks below are no longer
20
// necessary, so don't panic.
21
try {
22
var cssPropertyParsers = require('cssstyle/lib/parsers');
23
} catch (err) {
24
// This error probably just means cssstyle is not installed yet, because
25
// we're still in the process of upgrading jsdom. Don't worry about it
26
// until jsdom has been updated to the latest version (v0.8.x).
27
}
28
29
if (cssPropertyParsers) {
30
// The shorthandParser function should never return a string, but it
31
// does when given an empty string. Here we detect that case and make it
32
// return an empty object instead, to work around bugs in later code
33
// that assume the result of shorthandParser is always an object.
34
var shorthandParser = cssPropertyParsers.shorthandParser;
35
cssPropertyParsers.shorthandParser = function() {
36
var result = shorthandParser.apply(this, arguments);
37
return result === '' ? {} : result;
38
};
39
40
// Current versions of the cssstyle parseInteger function can't actually
41
// handle string inputs.
42
var badInt = cssPropertyParsers.parseInteger('5');
43
if (badInt !== '5') {
44
cssPropertyParsers.parseInteger = function parseInteger(val) {
45
return String(parseInt(val, 10));
46
};
47
}
48
49
// Current versions of the cssstyle parseNumber function can't actually
50
// handle string inputs.
51
var badNum = cssPropertyParsers.parseNumber('0.5');
52
if (badNum !== '0.5') {
53
cssPropertyParsers.parseNumber = function parseNumber(val) {
54
return String(parseFloat(val, 10));
55
};
56
}
57
}
58
59
// We can't require jsdom/lib/jsdom/browser/utils directly, because it
60
// requires jsdom, which requires utils circularly, so the utils module
61
// won't be fully populated when its (non-existent) NOT_IMPLEMENTED
62
// property is imported elsewhere. Instead, the only thing that seems to
63
// work is to override the utils module in require.cache, so that we never
64
// have to evaluate the original module.
65
try {
66
var utilsId = require.resolve('jsdom/lib/jsdom/browser/utils');
67
} catch (err) {
68
// Leave utilsId undefined if require.resolve couldn't resolve it.
69
}
70
71
if (utilsId) {
72
require.cache[utilsId] = {
73
id: utilsId,
74
exports: {
75
NOT_IMPLEMENTED: function(target, nameForErrorMessage) {
76
var message = 'NOT IMPLEMENTED' + (
77
nameForErrorMessage ? ': ' + nameForErrorMessage : ''
78
);
79
80
return function() {
81
if (!jsdom.debugMode) {
82
// These two lines have been changed from the original
83
// NOT_IMPLEMENTED function to be more defensive about the
84
// presence/absence of .raise and raise.call.
85
var raise = (target && target.raise) || (this && this.raise);
86
if (raise && raise.call) {
87
raise.call(this, 'error', message);
88
} else {
89
// In case there was no suitable raise function to use, we
90
// still want to throw a meaningful Error (another
91
// improvement over the original NOT_IMPLEMENTED).
92
throw new Error(message);
93
}
94
}
95
};
96
}
97
}
98
};
99
}
100
101
var jsdom = require('jsdom');
102
var define = require('jsdom/lib/jsdom/level2/html').define;
103
var elements = jsdom.defaultLevel;
104
105
function _getTimeRangeDummy() {
106
return {
107
length: 0,
108
start: function() { return 0; },
109
end: function() { return 0; }
110
};
111
}
112
113
if (elements && !elements.HTMLMediaElement) {
114
115
define('HTMLMediaElement', {
116
_init: function() {
117
this._muted = this.defaultMuted;
118
this._volume = 1.0;
119
this.readyState = 0;
120
},
121
proto: {
122
// Implemented accoring to W3C Draft 22 August 2012
123
set defaultPlaybackRate(v) {
124
if (v === 0.0) {
125
throw new elements.DOMException(elements.NOT_SUPPORTED_ERR);
126
}
127
if (this._defaultPlaybackRate !== v) {
128
this._defaultPlaybackRate = v;
129
this._dispatchRateChange();
130
}
131
},
132
_dispatchRateChange: function() {
133
var ev = this._ownerDocument.createEvent('HTMLEvents');
134
ev.initEvent('ratechange', false, false);
135
this.dispatchEvent(ev);
136
},
137
get defaultPlaybackRate() {
138
if (this._defaultPlaybackRate === undefined) {
139
return 1.0;
140
}
141
return this._defaultPlaybackRate;
142
},
143
get playbackRate() {
144
if (this._playbackRate === undefined) {
145
return 1.0;
146
}
147
return this._playbackRate;
148
},
149
set playbackRate(v) {
150
if (v !== this._playbackRate) {
151
this._playbackRate = v;
152
this._dispatchRateChange();
153
}
154
},
155
get muted() {
156
return this._muted;
157
},
158
_dispatchVolumeChange: function() {
159
var ev = this._ownerDocument.createEvent('HTMLEvents');
160
ev.initEvent('volumechange', false, false);
161
this.dispatchEvent(ev);
162
},
163
set muted(v) {
164
if (v !== this._muted) {
165
this._muted = v;
166
this._dispatchVolumeChange();
167
}
168
},
169
get defaultMuted() {
170
return this.getAttribute('muted') !== null;
171
},
172
set defaultMuted(v) {
173
if (v) {
174
this.setAttribute('muted', v);
175
} else {
176
this.removeAttribute('muted');
177
}
178
},
179
get volume() {
180
return this._volume;
181
},
182
set volume(v) {
183
if (v < 0 || v > 1) {
184
throw new elements.DOMException(elements.INDEX_SIZE_ERR);
185
}
186
if (this._volume !== v) {
187
this._volume = v;
188
this._dispatchVolumeChange();
189
}
190
},
191
192
// Not (yet) implemented according to spec
193
// Should return sane default values
194
currentSrc: '',
195
buffered: _getTimeRangeDummy(),
196
networkState: 0,
197
load: function() {
198
},
199
canPlayType: function() {
200
return false;
201
},
202
seeking: false,
203
duration: 0,
204
startDate: NaN,
205
paused: true,
206
played: _getTimeRangeDummy(),
207
seekable: _getTimeRangeDummy(),
208
ended: false,
209
play: function() {
210
},
211
pause: function() {
212
},
213
audioTracks: [],
214
videoTracks: [],
215
textTracks: [],
216
addTextTrack: function() {
217
}
218
},
219
attributes: [
220
{ prop: 'autoplay', type: 'boolean' },
221
{ prop: 'controls', type: 'boolean' },
222
'crossOrigin',
223
'currentTime',
224
'preload',
225
{ prop: 'loop', type: 'boolean' },
226
'mediaGroup',
227
]
228
});
229
}
230
231
if (elements && !elements.HTMLVideoElement) {
232
define('HTMLVideoElement', {
233
tagName: 'VIDEO',
234
parentClass: elements.HTMLMediaElement,
235
attributes: [
236
{ prop: 'height', type: 'long' },
237
'poster',
238
{ prop: 'videoHeight', type: 'long' },
239
{ prop: 'videoWidth', type: 'long' },
240
{ prop: 'width', type: 'long' }
241
]
242
});
243
}
244
245
if (elements && elements.HTMLInputElement) {
246
var proto = elements.HTMLInputElement.prototype;
247
var desc = Object.getOwnPropertyDescriptor(proto, 'checked');
248
if (desc) {
249
// Reimplement the .checked setter to require that two radio buttons
250
// have the same .form in order for their .checked values to be
251
// mutually exclusive. Except for the lines commented below, this code
252
// was borrowed directly from the jsdom implementation:
253
// https://github.com/tmpvar/jsdom/blob/0cf670d6eb/lib/jsdom/level2/html.js#L975-L990
254
desc.set = function(checked) {
255
this._initDefaultChecked();
256
257
// Accept empty strings as truthy values for the .checked attribute.
258
if (checked || (checked === '')) {
259
this.setAttribute('checked', 'checked');
260
261
if (this.type === 'radio') {
262
var elements = this._ownerDocument.getElementsByName(this.name);
263
264
for (var i = 0; i < elements.length; i++) {
265
var other = elements[i];
266
if (other !== this &&
267
other.tagName === 'INPUT' &&
268
other.type === 'radio' &&
269
// This is the condition that is missing from the default
270
// implementation of the .checked setter.
271
other.form === this.form) {
272
other.checked = false;
273
}
274
}
275
}
276
277
} else {
278
this.removeAttribute('checked');
279
}
280
};
281
282
Object.defineProperty(proto, 'checked', desc);
283
}
284
}
285
286
// Make sure we unselect all but the first selected option when a <select>
287
// element has its "multiple" attribute set to false.
288
if (elements && elements.HTMLSelectElement) {
289
var proto = elements.HTMLSelectElement.prototype;
290
var oldAttrModified = proto._attrModified;
291
proto._attrModified = function(name, value) {
292
if (name === 'multiple' && !value) {
293
var leaveNextOptionSelected = true;
294
this.options._toArray().forEach(function(option) {
295
if (option.selected) {
296
if (leaveNextOptionSelected) {
297
leaveNextOptionSelected = false;
298
} else {
299
option.selected = false;
300
}
301
}
302
});
303
}
304
305
return oldAttrModified.apply(this, arguments);
306
};
307
}
308
309
// Require this module if you want to require('jsdom'), to ensure the
310
// above compatibility measures have been implemented.
311
module.exports = jsdom;
312
313