· 6 years ago · Oct 17, 2019, 04:02 PM
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3 typeof define === 'function' && define.amd ? define(['exports'], factory) :
4 (global = global || self, factory(global.EMESpy = {}));
5}(this, function (exports) { 'use strict';
6
7 /**
8 * Store information about every EME Calls stubbed in this file.
9 * @type {Object}
10 */
11 var EME_CALLS = {};
12
13 function getEMECalls() {
14 return EME_CALLS;
15 }
16
17 function resetEMECalls() {
18 Object.keys(EME_CALLS).forEach(function (key) {
19 delete EME_CALLS[key];
20 });
21 }
22
23 var NativeMediaKeys = window.MSMediaKeys;
24 var NativeMediaKeySession = window.MSMediaKeySession;
25 var NativeMediaKeySystemAccess = window.MediaKeySystemAccess;
26
27 /**
28 * Define the logger for the MSE-spy.
29 * Allows to re-define a specific logger on runtime / before applying this
30 * script.
31 * @type {Object}
32 */
33 var Logger = window.MSESpyLogger || {
34 /* eslint-disable no-console */
35
36 /**
37 * Triggered each time a property is accessed.
38 * @param {string} pathString - human-readable path to the property.
39 * @param {*} value - the value it currently has.
40 */
41 onPropertyAccess: function onPropertyAccess(pathString, value) {
42 console.debug(">>> Getting ".concat(pathString, ":"), value);
43 },
44
45 /**
46 * Triggered each time a property is set.
47 * @param {string} pathString - human-readable path to the property.
48 * @param {*} value - the value it is set to.
49 */
50 onSettingProperty: function onSettingProperty(pathString, value) {
51 console.debug(">> Setting ".concat(pathString, ":"), value);
52 },
53
54 /**
55 * Triggered when some object is instanciated (just before).
56 * @param {string} objectName - human-readable name for the concerned object.
57 * @param {Array.<*>} args - Arguments given to the constructor
58 */
59 onObjectInstanciation: function onObjectInstanciation(objectName, args) {
60 if (args.length) {
61 console.debug(">>> Creating ".concat(objectName, " with arguments:"), args);
62 } else {
63 console.debug(">>> Creating ".concat(objectName));
64 }
65 },
66
67 /**
68 * Triggered when an Object instanciation failed.
69 * @param {string} objectName - human-readable name for the concerned object.
70 * @param {Error} error - Error thrown by the constructor
71 */
72 onObjectInstanciationError: function onObjectInstanciationError(objectName, error) {
73 console.error(">> ".concat(objectName, " creation failed:"), error);
74 },
75
76 /**
77 * Triggered when an Object instanciation succeeded.
78 * @param {string} objectName - human-readable name for the concerned object.
79 * @param {*} value - The corresponding object instanciated.
80 */
81 onObjectInstanciationSuccess: function onObjectInstanciationSuccess(objectName, value) {
82 console.debug(">>> ".concat(objectName, " created:"), value);
83 },
84
85 /**
86 * Triggered when some method/function is called.
87 * @param {string} pathName - human-readable path for the concerned function.
88 * @param {Array.<*>} args - Arguments given to this function.
89 */
90 onFunctionCall: function onFunctionCall(pathName, args) {
91 if (args.length) {
92 console.debug(">>> ".concat(pathName, " called with arguments:"), args);
93 } else {
94 console.debug(">>> ".concat(pathName, " called"));
95 }
96 },
97
98 /**
99 * Triggered when a function call fails.
100 * @param {string} pathName - human-readable path for the concerned function.
101 * @param {Error} error - Error thrown by the call
102 */
103 onFunctionCallError: function onFunctionCallError(pathName, error) {
104 console.error(">> ".concat(pathName, " failed:"), error);
105 },
106
107 /**
108 * Triggered when a function call succeeded.
109 * @param {string} pathName - human-readable path for the concerned function.
110 * @param {*} value - The result of the function
111 */
112 onFunctionCallSuccess: function onFunctionCallSuccess(pathName, value) {
113 console.info(">>> ".concat(pathName, " succeeded:"), value);
114 },
115
116 /**
117 * Triggered when a function returned a Promise and that promise resolved.
118 * @param {string} pathName - human-readable path for the concerned function.
119 * @param {*} value - The value when the function resolved.
120 */
121 onFunctionPromiseResolve: function onFunctionPromiseResolve(pathName, value) {
122 console.info(">>> ".concat(pathName, " resolved:"), value);
123 },
124
125 /**
126 * Triggered when a function returned a Promise and that promise rejected.
127 * @param {string} pathName - human-readable path for the concerned function.
128 * @param {*} value - The error when the function's promise rejected.
129 */
130 onFunctionPromiseReject: function onFunctionPromiseReject(pathName, value) {
131 console.error(">>> ".concat(pathName, " rejected:"), value);
132 }
133 };
134
135 function _setPrototypeOf(o, p) {
136 _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
137 o.__proto__ = p;
138 return o;
139 };
140
141 return _setPrototypeOf(o, p);
142 }
143
144 function isNativeReflectConstruct() {
145 if (typeof Reflect === "undefined" || !Reflect.construct) return false;
146 if (Reflect.construct.sham) return false;
147 if (typeof Proxy === "function") return true;
148
149 try {
150 Date.prototype.toString.call(Reflect.construct(Date, [], function () {}));
151 return true;
152 } catch (e) {
153 return false;
154 }
155 }
156
157 function _construct(Parent, args, Class) {
158 if (isNativeReflectConstruct()) {
159 _construct = Reflect.construct;
160 } else {
161 _construct = function _construct(Parent, args, Class) {
162 var a = [null];
163 a.push.apply(a, args);
164 var Constructor = Function.bind.apply(Parent, a);
165 var instance = new Constructor();
166 if (Class) _setPrototypeOf(instance, Class.prototype);
167 return instance;
168 };
169 }
170
171 return _construct.apply(null, arguments);
172 }
173
174 var id = 0;
175 /**
176 * Generate a new number each time it is called.
177 * /!\ Never check for an upper-bound. Please do not use if you can reach
178 * `Number.MAX_VALUE`
179 * @returns {number}
180 */
181
182 function generateId() {
183 return id++;
184 }
185
186 /**
187 * Log multiple method calls for an object.
188 * Also populates an object with multiple data at the time of the call.
189 *
190 * @param {Object} baseObject - Object in which the method/function is.
191 * For example to spy on the Date method `toLocaleDateString`, you will have to
192 * set here `Date.prototype`.
193 * @param {Array.<string>} methodNames - Every methods you want to spy on
194 * @param {string} humanReadablePath - Path to the method. Used for logging
195 * purposes.
196 * For example `"Date.prototype"`, for spies of Date's methods.
197 * @param {Object} logObject - Object where infos about the method calls will be
198 * added.
199 * The methods' name will be the key of the object.
200 *
201 * The values will be an array of object with the following properties:
202 *
203 * - self {Object}: Reference to the baseObject argument.
204 *
205 * - id {number}: a uniquely generated ascending ID for any stubbed
206 * property/methods with this library.
207 *
208 * - date {number}: Timestamp at the time of the call.
209 *
210 * - args {Array}: Array of arguments given to the function
211 *
212 * - response {*}: Response of the function.
213 * The property is not defined if the function did not respond yet or was on
214 * error.
215 *
216 * - responseDate {number}: Timestamp at the time of the response.
217 * The property is not defined if the function did not respond yet or was on
218 * error.
219 *
220 * - error {*}: Error thrown by the function, if one.
221 * The property is not defined if the function did not throw.
222 *
223 * - errorDate {number} Timestamp at the time of the error.
224 * The property is not defined if the function did not throw.
225 *
226 * - responseResolved {*}: When the returned value (the response) is a promise
227 * and that promise resolved, this property contains the value emitted by
228 * the resolve. Else, that property is not set.
229 *
230 * - responseResolvedDate {number}: When the returned value (the response) is
231 * a promise and that promise resolved, this property contains the date at
232 * which the promise resolved. Else, that property is not set.
233 *
234 * - responseRejected {*}: When the returned value (the response) is a promise
235 * and that promise rejected, this property contains the error emitted by
236 * the reject. Else, that property is not set.
237 *
238 * - responseRejectedDate {number}: When the returned value (the response) is
239 * a promise and that promise rejected, this property contains the date at
240 * which the promise rejected. Else, that property is not set.
241 *
242 * @returns {Function} - function which deactivates the spy when called.
243 */
244
245 function spyOnMethods(baseObject, methodNames, humanReadablePath, logObject) {
246 var baseObjectMethods = methodNames.reduce(function (acc, methodName) {
247 acc[methodName] = baseObject[methodName];
248 return acc;
249 }, {});
250
251 var _loop = function _loop(i) {
252 var methodName = methodNames[i];
253 var completePath = humanReadablePath + "." + methodName;
254 var oldMethod = baseObject[methodName];
255
256 if (!oldMethod) {
257 throw new Error("No method in " + completePath);
258 }
259
260 baseObject[methodName] = function () {
261 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
262 args[_key] = arguments[_key];
263 }
264
265 Logger.onFunctionCall(completePath, args);
266 var currentLogObject = {
267 self: baseObject,
268 id: generateId(),
269 date: Date.now(),
270 args: args
271 };
272
273 if (!logObject[methodName]) {
274 logObject[methodName] = [];
275 }
276
277 logObject[methodName].push(currentLogObject);
278 var res;
279
280 try {
281 res = oldMethod.apply(this, args);
282 } catch (e) {
283 Logger.onFunctionCallError(completePath, e);
284 currentLogObject.error = e;
285 currentLogObject.errorDate = Date.now();
286 throw e;
287 }
288
289 Logger.onFunctionCallSuccess(completePath, res);
290 currentLogObject.response = res;
291 currentLogObject.responseDate = Date.now();
292
293 if (res instanceof Promise) {
294 res.then( // on success
295 function (value) {
296 Logger.onFunctionPromiseResolve(completePath, value);
297 currentLogObject.responseResolved = value;
298 currentLogObject.responseResolvedDate = Date.now();
299 }, // on error
300 function (err) {
301 Logger.onFunctionPromiseReject(completePath, err);
302 currentLogObject.responseRejected = err;
303 currentLogObject.responseRejectedDate = Date.now();
304 });
305 }
306
307 return res;
308 };
309 };
310
311 for (var i = 0; i < methodNames.length; i++) {
312 _loop(i);
313 }
314
315 return function stopSpyingOnMethods() {
316 for (var i = 0; i < methodNames.length; i++) {
317 var methodName = methodNames[i];
318 baseObject[methodName] = baseObjectMethods[methodName];
319 }
320 };
321 }
322
323 /**
324 * Spy access and updates of an Object's read-only properties:
325 * - log every access/updates
326 * - add entries in a logging object
327 *
328 * @param {Object} baseObject - Object in which the property is.
329 * For example to spy on the HTMLMediaElement property `currentTime`, you will
330 * have to set here `HTMLMediaElement.prototype`.
331 * @param {Object} baseDescriptors - Descriptors for the spied properties.
332 * The keys are the properties' names, the values are the properties'
333 * descriptors.
334 * @param {Array.<string>} propertyNames - Every properties you want to spy on.
335 * @param {string} humanReadablePath - Path to the property. Used for logging
336 * purposes.
337 * For example `"HTMLMediaElement.prototype"`, for spies of HTMLMediaElement's
338 * class properties.
339 * @param {Object} logObject - Object where infos about the properties access
340 * will be added.
341 * The methods' name will be the key of the object.
342 *
343 * The values will be an object with a single key ``get``, corresponding to
344 * property accesses
345 *
346 * This key will then have as value an array of object.
347 *
348 * - self {Object}: Reference to the baseObject argument.
349 *
350 * - id {number}: a uniquely generated ID for any stubbed property/methods with
351 * this library.
352 *
353 * - date {number}: Timestamp at the time of the property access.
354 *
355 * - value {*}: value of the property at the time of access.
356 *
357 * @returns {Function} - function which deactivates the spy when called.
358 */
359
360 function spyOnReadOnlyProperties(baseObject, baseDescriptors, propertyNames, humanReadablePath, logObject) {
361 var _loop = function _loop(i) {
362 var propertyName = propertyNames[i];
363 var baseDescriptor = baseDescriptors[propertyName];
364 var completePath = humanReadablePath + "." + propertyName;
365
366 if (!baseDescriptor) {
367 throw new Error("No descriptor for property " + completePath);
368 }
369
370 Object.defineProperty(baseObject, propertyName, {
371 get: function get() {
372 var value = baseDescriptor.get.bind(this)();
373 Logger.onPropertyAccess(completePath, value);
374 var currentLogObject = {
375 self: this,
376 id: generateId(),
377 date: Date.now(),
378 value: value
379 };
380
381 if (!logObject[propertyName]) {
382 logObject[propertyName] = {
383 get: []
384 };
385 }
386
387 logObject[propertyName].get.push(currentLogObject);
388 return value;
389 }
390 });
391 };
392
393 for (var i = 0; i < propertyNames.length; i++) {
394 _loop(i);
395 }
396
397 return function stopSpyingOnReadOnlyProperties() {
398 Object.defineProperties(baseObject, propertyNames.reduce(function (acc, propertyName) {
399 acc[propertyName] = baseDescriptors[propertyName];
400 return acc;
401 }, {}));
402 };
403 }
404
405 /**
406 * Spy access and updates of an Object's read & write properties:
407 * - log every access/updates
408 * - add entries in a logging object
409 *
410 * @param {Object} baseObject - Object in which the property is.
411 * For example to spy on the HTMLMediaElement property `currentTime`, you will
412 * have to set here `HTMLMediaElement.prototype`.
413 * @param {Object} baseDescriptors - Descriptors for the spied properties.
414 * The keys are the properties' names, the values are the properties'
415 * descriptors.
416 * @param {Array.<string>} propertyNames - Every properties you want to spy on.
417 * @param {string} humanReadablePath - Path to the property. Used for logging
418 * purposes.
419 * For example `"HTMLMediaElement.prototype"`, for spies of HTMLMediaElement's
420 * class properties.
421 * @param {Object} logObject - Object where infos about the properties access
422 * will be added.
423 * The methods' name will be the key of the object.
424 *
425 * The values will be an object with two keys ``get`` and ``set``, respectively
426 * for property accesses and property updates.
427 *
428 * Each one of those properties will then have as values an array of object.
429 * Those objects are under the following form:
430 *
431 * 1. for `get` (property access):
432 *
433 * - self {Object}: Reference to the baseObject argument.
434 *
435 * - id {number}: a uniquely generated ascending ID for any stubbed
436 * property/methods with this library.
437 *
438 * - date {number}: Timestamp at the time of the property access.
439 *
440 * - value {*}: value of the property at the time of access.
441 *
442 *
443 * 2. for `set` (property updates):
444 *
445 * - self {Object}: Reference to the baseObject argument.
446 *
447 * - id {number}: a uniquely generated ascending ID for any stubbed
448 * property/methods with this library.
449 *
450 * - date {number}: Timestamp at the time of the property update.
451 *
452 * - value {*}: new value the property is set to
453 *
454 * @returns {Function} - function which deactivates the spy when called.
455 */
456
457 function spyOnProperties(baseObject, baseDescriptors, propertyNames, humanReadablePath, logObject) {
458 var _loop = function _loop(i) {
459 var propertyName = propertyNames[i];
460 var baseDescriptor = baseDescriptors[propertyName];
461 var completePath = humanReadablePath + "." + propertyName;
462
463 if (!baseDescriptor) {
464 throw new Error("No descriptor for property " + completePath);
465 }
466
467 Object.defineProperty(baseObject, propertyName, {
468 get: function get() {
469 var value = baseDescriptor.get.bind(this)();
470 Logger.onPropertyAccess(completePath, value);
471 var currentLogObject = {
472 self: this,
473 id: generateId(),
474 date: Date.now(),
475 value: value
476 };
477
478 if (!logObject[propertyName]) {
479 logObject[propertyName] = {
480 set: [],
481 get: []
482 };
483 }
484
485 logObject[propertyName].get.push(currentLogObject);
486 return value;
487 },
488 set: function set(value) {
489 Logger.onSettingProperty(completePath, value);
490 var currentLogObject = {
491 self: this,
492 id: generateId(),
493 date: Date.now(),
494 value: value
495 };
496
497 if (!logObject[propertyName]) {
498 logObject[propertyName] = {
499 set: [],
500 get: []
501 };
502 }
503
504 logObject[propertyName].set.push(currentLogObject);
505 baseDescriptor.set.bind(this)(value);
506 }
507 });
508 };
509
510 for (var i = 0; i < propertyNames.length; i++) {
511 _loop(i);
512 }
513
514 return function stopSpyingOnProperties() {
515 Object.defineProperties(baseObject, propertyNames.reduce(function (acc, propertyName) {
516 acc[propertyName] = baseDescriptors[propertyName];
517 return acc;
518 }, {}));
519 };
520 }
521
522 function spyOnWholeObject(BaseObject, objectName, readOnlyPropertyNames, propertyNames, staticMethodNames, methodNames, loggingObject) {
523 if (BaseObject == null || !BaseObject.prototype) {
524 throw new Error("Invalid object");
525 }
526
527 if (loggingObject[objectName] == null) {
528 loggingObject[objectName] = {
529 "new": [],
530 methods: {},
531 staticMethods: {},
532 properties: {},
533 eventListeners: {} // TODO
534
535 };
536 }
537
538 function StubbedObject() {
539 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
540 args[_key] = arguments[_key];
541 }
542
543 Logger.onObjectInstanciation(objectName, args);
544 var now = Date.now();
545 var spyObj = {
546 date: now,
547 args: args
548 };
549 loggingObject[objectName]["new"].push(spyObj);
550 var baseObject;
551
552 try {
553 baseObject = _construct(BaseObject, args);
554 } catch (e) {
555 Logger.onObjectInstanciationError(objectName, e);
556 spyObj.error = e;
557 spyObj.errorDate = Date.now();
558 throw e;
559 }
560
561 Logger.onObjectInstanciationSuccess(objectName, baseObject);
562 spyObj.response = baseObject;
563 spyObj.responseDate = Date.now();
564 return baseObject;
565 }
566
567 var unspyStaticMethods = spyOnMethods(BaseObject, staticMethodNames, objectName, loggingObject[objectName].staticMethods);
568 staticMethodNames.forEach(function (method) {
569 StubbedObject[method] = BaseObject[method].bind(BaseObject);
570 });
571 var BaseObjectProtoDescriptors = Object.getOwnPropertyDescriptors(BaseObject.prototype);
572 var unspyReadOnlyProps = spyOnReadOnlyProperties(BaseObject.prototype, BaseObjectProtoDescriptors, readOnlyPropertyNames, "".concat(objectName, ".prototype"), loggingObject[objectName].properties);
573 var unspyProps = spyOnProperties(BaseObject.prototype, BaseObjectProtoDescriptors, propertyNames, "".concat(objectName, ".prototype"), loggingObject[objectName].properties);
574 var unspyMethods = spyOnMethods(BaseObject.prototype, methodNames, "".concat(objectName, ".prototype"), loggingObject[objectName].methods);
575 window[objectName] = StubbedObject;
576 return function stopSpying() {
577 unspyReadOnlyProps();
578 unspyProps();
579 unspyStaticMethods();
580 unspyMethods();
581 window[objectName] = BaseObject;
582 };
583 }
584
585 function spyOnMediaKeySession() {
586 return spyOnWholeObject( // Object to spy on
587 NativeMediaKeySession, // name in window
588 "MSMediaKeySession", // read-only properties
589 ["sessionId", "keySystem", "error"], // regular properties
590 [], // static methods
591 [], // methods
592 ["close", "update"], // global logging object
593 EME_CALLS);
594 }
595
596 var resetSpies = null;
597 /**
598 * Start/restart spying on EME API calls.
599 */
600
601 function start() {
602 if (resetSpies != null) {
603 resetSpies();
604 }
605
606 var resetSpyFunctions = [spyOnMediaKeySession()].filter(function (cb) {
607 return cb;
608 });
609
610 resetSpies = function resetEverySpies() {
611 resetSpyFunctions.forEach(function (fn) {
612 fn && fn();
613 });
614 resetSpyFunctions.length = 0;
615 resetSpies = null;
616 };
617 }
618 /**
619 * Stop spying on EME API calls.
620 */
621
622
623 function stop() {
624 if (resetSpies != null) {
625 resetSpies();
626 }
627 }
628
629 exports.Logger = Logger;
630 exports.getEMECalls = getEMECalls;
631 exports.resetEMECalls = resetEMECalls;
632 exports.start = start;
633 exports.stop = stop;
634
635 Object.defineProperty(exports, '__esModule', { value: true });
636
637}));