From ec0b4aa55abcecfbb1f8a27fcdea4b46fee904ca Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Tue, 26 Oct 2021 12:02:34 -0700 Subject: [PATCH] feat: Replace the tracing and log window implementations Rather than explicitly map out the API of EME and its various structures, configure TraceAnything with a few different options for various parts of the API. The generic tracing engine of TraceAnything will map out the API and any future additions to these classes automatically. New classes or top-level entrypoint can easily be added to the config in the future. The new log window implementation is better organized and written as an ES6 class, and matches the log changes from the tracing part of the extension. Change-Id: Iaf5e0a5da7f4500fad14487f8afe27b6610e8634 --- content-script.js | 2 +- eme-trace-config.js | 219 ++++ eme_listeners.js | 363 ------- log-window.js | 297 +++++ log_constructor.js | 301 ------ manifest.json | 7 +- mutation-summary.js | 1665 ----------------------------- package-lock.json | 39 +- package.json | 3 +- popup.js | 4 +- prototypes.js | 717 ------------- spec/eme_trace_tests.js | 273 ++--- spec/log_window_tests.js | 314 ++++-- spec/support/jasmine-browser.json | 8 +- 14 files changed, 897 insertions(+), 3315 deletions(-) create mode 100644 eme-trace-config.js delete mode 100644 eme_listeners.js create mode 100644 log-window.js delete mode 100644 log_constructor.js delete mode 100644 mutation-summary.js delete mode 100644 prototypes.js diff --git a/content-script.js b/content-script.js index 2c2adda..534bf4b 100644 --- a/content-script.js +++ b/content-script.js @@ -17,7 +17,7 @@ */ // Load required scripts into the current web page. -const urls = ['/mutation-summary.js', '/prototypes.js', '/eme_listeners.js']; +const urls = ['/trace-anything.js', '/eme-trace-config.js']; for (const url of urls) { const absoluteUrl = chrome.extension.getURL(url); diff --git a/eme-trace-config.js b/eme-trace-config.js new file mode 100644 index 0000000..f8ea3c6 --- /dev/null +++ b/eme-trace-config.js @@ -0,0 +1,219 @@ +/** + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Shim and trace calls to EME, and log them to the extension's + * log window. + */ + +/** + * A custom logger to plug into TraceAnything. + */ +function emeLogger(log) { + // Log to the default logger in the JS console first. + TraceAnything.defaultLogger(log); + + // TODO: Discuss instances and properties with xhwang before finalizing + + // This is not needed and can't be easily serialized. + delete log.instance; + + window.postMessage({type: 'emeTraceLog', log: prepLogForMessage(log)}, '*'); +} + +/** + * @param {Object} log + * @return {Object} + */ +function prepLogForMessage(log) { + const clone = {}; + for (const k in log) { + clone[k] = getSerializable(log[k]); + } + return clone; +} + +/** + * @param {*} obj A value that may or may not be serializable. + * @return {*} A value that can be serialized. + */ +function getSerializable(obj) { + // Return primitive types directly. + if (obj == null || typeof obj == 'string' || typeof obj == 'number' || + typeof obj == 'boolean') { + return obj; + } + + // Events are full of garbage, so only serialize the interesting fields. + if (obj instanceof Event) { + const clone = {}; + for (const k in obj) { + // Skip fields that are in the Event base class, as well as "isTrusted". + // These are not interesting for logging. + if (!(k in Event.prototype) && k != 'isTrusted') { + clone[k] = getSerializable(obj[k]); + } + } + return { + __type__: obj.type + ' Event', + __fields__: clone, + }; + } + + // Elements, Nodes, and Windows are dangerous to serialize because they + // contain many fields and circular references. + if (obj instanceof Element) { + return { + __type__: '<' + obj.tagName.toLowerCase() + '> element', + }; + } + if (obj instanceof Node) { + return { + __type__: obj.nodeName.toLowerCase() + ' node', + }; + } + if (obj instanceof Window) { + return { + __type__: 'Window', + }; + } + + // Convert array buffers into views. + // Format views into an object that can be serialized and logged. + if (obj instanceof ArrayBuffer) { + obj = new Uint8Array(obj); + } + if (ArrayBuffer.isView(obj)) { + return { + __type__: obj.constructor.name, + __data__: Array.from(obj), + }; + } + + // Get all key statuses and serialize them. + if (obj instanceof MediaKeyStatusMap) { + const statuses = {}; + obj.forEach((status, arrayBuffer) => { + const keyId = uint8ArrayToHexString(new Uint8Array(arrayBuffer)); + statuses[keyId] = status; + }); + return { + __type__: obj.constructor.name, + __fields__: statuses, + } + } + + // DOMExceptions don't serialize right if done generically. None of their + // properties are their "own". This follows the same format used below for + // serializing other typed objects. + if (obj instanceof DOMException) { + return { + __type__: 'DOMException', + __fields__: { + name: obj.name, + code: obj.code, + message: obj.message, + }, + }; + } + + // Clone the elements of an array into serializable versions. + if (Array.isArray(obj)) { + const clone = []; + for (const k in obj) { + if (typeof obj[k] == 'function') { + clone[k] = {__type__: 'function'}; + } else { + clone[k] = getSerializable(obj[k]); + } + } + return clone; + } + + // Clone the fields of an object into serializable versions. + const clone = {}; + for (const k in obj) { + if (k == '__TraceAnything__' || typeof obj[k] == 'function') { + continue; + } + clone[k] = getSerializable(obj[k]); + } + if (obj.constructor != Object) { + // If it's an object with a type, send that info, too. + return { + __type__: obj.constructor.name, + __fields__: clone, + }; + } + return clone; +} + +function byteToHexString(byte) { + return byte.toString(16).padStart(2, '0'); +} + +function uint8ArrayToHexString(view) { + return Array.from(view).map(byteToHexString).join(''); +} + +function combineOptions(baseOptions, overrideOptions) { + return Object.assign({}, baseOptions, overrideOptions); +} + +// General options for TraceAnything. +const options = { + // When formatting logs and sending them as serialized messages, we need to + // wait for async results to be resolved before we log them. + logAsyncResultsImmediately: false, + + // Our custom logger. Using an arrow function makes it possible to spy on + // emeLogger in our tests without breaking this connection. + logger: (log) => emeLogger(log), + + // Don't bother logging event listener methods. It's not useful. + // We can still log events, though. + skipProperties: [ + 'addEventListener', + 'removeEventListener', + ], +}; + +// These will be shimmed in place. +TraceAnything.traceMember(navigator, 'requestMediaKeySystemAccess', options); + +// These constructors are not used directly, but this registers them to the +// tracing system so that instances we find later will be shimmed. +TraceAnything.traceClass(MediaKeys, options); +TraceAnything.traceClass(MediaKeySystemAccess, options); +TraceAnything.traceClass(MediaKeySession, combineOptions(options, { + // Also skip logging certain noisy properites on MediaKeySession. + skipProperties: options.skipProperties.concat([ + 'expiration', + ]), +})); + +// Trace media element types, and monitor the document for new instances. +const elementOptions = combineOptions(options, { + // Skip all property access on media elements. + // It's a little noisy and unhelpful (currentTime getter, for example). + properties: false, + + // And these specific events are VERY noisy. Skip them. + skipEvents: [ + 'progress', + 'timeupdate', + ], +}); +TraceAnything.traceElement('video', elementOptions); +TraceAnything.traceElement('audio', elementOptions); diff --git a/eme_listeners.js b/eme_listeners.js deleted file mode 100644 index 61f1b1e..0000000 --- a/eme_listeners.js +++ /dev/null @@ -1,363 +0,0 @@ -/** - * Copyright 2015 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @fileoverview Adds listeners to EME elements. - */ - -console.info('eme_logger.js loaded.'); -console.info('Some identifiable information may be in the log. Be careful ' + - 'about posting the log on bug reports.'); - - -/** Set up the EME listeners. */ -function setUp_() { - var listener = new EmeListeners(); - listener.setUpListeners(); -} - - -/** - * Manager for EME event and method listeners. - * @constructor - */ -function EmeListeners() {} - - -/** - * Sets up EME listeners for whichever type of EME is enabled. - */ -EmeListeners.prototype.setUpListeners = function() { - if (!navigator.requestMediaKeySystemAccess) { - console.log('EME not available.'); - return; - } - - this.addListenersToNavigator_(); - this.addListenersToAllEmeElements_(); -}; - - -/** - * Adds listeners to the EME methods on the Navigator object. - * @private - */ -EmeListeners.prototype.addListenersToNavigator_ = function() { - if (navigator.listenersAdded_) { - return; - } - var originalRequestMediaKeySystemAccessFn = EmeListeners.extendEmeMethod( - navigator, - navigator.requestMediaKeySystemAccess, - emeLogger.RequestMediaKeySystemAccessCall); - navigator.requestMediaKeySystemAccess = function() { - var result = originalRequestMediaKeySystemAccessFn.apply(null, arguments); - // Attach listeners to returned MediaKeySystemAccess object - return result.then(function(mediaKeySystemAccess) { - this.addListenersToMediaKeySystemAccess_(mediaKeySystemAccess); - return Promise.resolve(mediaKeySystemAccess); - }.bind(this)); - }.bind(this); - - navigator.listenersAdded_ = true; -}; - - -/** - * Adds listeners to the EME methods on a MediaKeySystemAccess object. - * @param {MediaKeySystemAccess} mediaKeySystemAccess A MediaKeySystemAccess - * object to add listeners to. - * @private - */ -EmeListeners.prototype.addListenersToMediaKeySystemAccess_ = - function(mediaKeySystemAccess) { - if (mediaKeySystemAccess.listenersAdded_) { - return; - } - mediaKeySystemAccess.originalGetConfiguration = - mediaKeySystemAccess.getConfiguration; - mediaKeySystemAccess.getConfiguration = EmeListeners.extendEmeMethod( - mediaKeySystemAccess, - mediaKeySystemAccess.getConfiguration, - emeLogger.GetConfigurationCall); - - var originalCreateMediaKeysFn = EmeListeners.extendEmeMethod( - mediaKeySystemAccess, - mediaKeySystemAccess.createMediaKeys, - emeLogger.CreateMediaKeysCall); - mediaKeySystemAccess.createMediaKeys = function() { - var result = originalCreateMediaKeysFn.apply(null, arguments); - // Attach listeners to returned MediaKeys object - return result.then(function(mediaKeys) { - mediaKeys.keySystem_ = mediaKeySystemAccess.keySystem; - this.addListenersToMediaKeys_(mediaKeys); - return Promise.resolve(mediaKeys); - }.bind(this)); - }.bind(this); - - mediaKeySystemAccess.listenersAdded_ = true; -}; - - -/** - * Adds listeners to the EME methods on a MediaKeys object. - * @param {MediaKeys} mediaKeys A MediaKeys object to add listeners to. - * @private - */ -EmeListeners.prototype.addListenersToMediaKeys_ = function(mediaKeys) { - if (mediaKeys.listenersAdded_) { - return; - } - var originalCreateSessionFn = EmeListeners.extendEmeMethod( - mediaKeys, mediaKeys.createSession, emeLogger.CreateSessionCall); - mediaKeys.createSession = function() { - var result = originalCreateSessionFn.apply(null, arguments); - result.keySystem_ = mediaKeys.keySystem_; - // Attach listeners to returned MediaKeySession object - this.addListenersToMediaKeySession_(result); - return result; - }.bind(this); - - mediaKeys.setServerCertificate = EmeListeners.extendEmeMethod( - mediaKeys, - mediaKeys.setServerCertificate, - emeLogger.SetServerCertificateCall); - mediaKeys.listenersAdded_ = true; -}; - - -/** Adds listeners to the EME methods and events on a MediaKeySession object. - * @param {MediaKeySession} session A MediaKeySession object to add - * listeners to. - * @private - */ -EmeListeners.prototype.addListenersToMediaKeySession_ = function(session) { - if (session.listenersAdded_) { - return; - } - session.generateRequest = EmeListeners.extendEmeMethod( - session, - session.generateRequest, - emeLogger.GenerateRequestCall); - - session.load = EmeListeners.extendEmeMethod( - session, session.load, emeLogger.LoadCall); - - session.update = EmeListeners.extendEmeMethod( - session, - session.update, - emeLogger.UpdateCall); - - session.close = EmeListeners.extendEmeMethod( - session, session.close, emeLogger.CloseCall); - - session.remove = EmeListeners.extendEmeMethod( - session, session.remove, emeLogger.RemoveCall); - - session.addEventListener('message', function(e) { - e.keySystem = session.keySystem_; - EmeListeners.logEvent(emeLogger.MessageEvent, e); - }); - - session.addEventListener('keystatuseschange', - EmeListeners.logEvent.bind(null, emeLogger.KeyStatusesChangeEvent)); - - session.listenersAdded_ = true; -}; - - -/** - * Adds listeners to all currently created media elements and sets up a - * mutation-summary observer to add listeners to any newly created media - * elements. - * @private - */ -EmeListeners.prototype.addListenersToAllEmeElements_ = function() { - this.addEmeListenersToInitialMediaElements_(); - var observer = new MutationSummary({ - callback: function(summaries) { - applyListeners(summaries); - }, - queries: [{element: 'video'}, {element: 'audio'}], - }); - - var applyListeners = function(summaries) { - for (var i = 0; i < summaries.length; i++) { - var elements = summaries[i]; - elements.added.forEach(function(element) { - this.addListenersToEmeElement_(element, true); - }.bind(this)); - } - }.bind(this); -}; - - -/** - * Adds listeners to the EME elements currently in the document. - * @private - */ -EmeListeners.prototype.addEmeListenersToInitialMediaElements_ = function() { - var audioElements = document.getElementsByTagName('audio'); - for (var i = 0; i < audioElements.length; ++i) { - this.addListenersToEmeElement_(audioElements[i], false); - } - var videoElements = document.getElementsByTagName('video'); - for (var i = 0; i < videoElements.length; ++i) { - this.addListenersToEmeElement_(videoElements[i], false); - } -}; - - -/** - * Adds method and event listeners to media element. - * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to. - * @private - */ -EmeListeners.prototype.addListenersToEmeElement_ = function(element) { - this.addEmeEventListeners_(element); - this.addEmeMethodListeners_(element); - console.info('EME listeners successfully added to:', element); -}; - - -/** - * Adds event listeners to a media element. - * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to. - * @private - */ -EmeListeners.prototype.addEmeEventListeners_ = function(element) { - if (element.eventListenersAdded_) { - return; - } - element.addEventListener('encrypted', - EmeListeners.logEvent.bind(null, emeLogger.EncryptedEvent)); - - element.addEventListener('play', - EmeListeners.logEvent.bind(null, emeLogger.PlayEvent)); - - element.addEventListener('error', function(e) { - console.error('Error Event'); - EmeListeners.logEvent(emeLogger.ErrorEvent, e); - }); - - element.eventListenersAdded_ = true; -}; - - -/** - * Adds method listeners to a media element. - * @param {HTMLMediaElement} element A HTMLMedia element to add listeners to. - * @private - */ -EmeListeners.prototype.addEmeMethodListeners_ = function(element) { - if (element.methodListenersAdded_) { - return; - } - element.play = EmeListeners.extendEmeMethod( - element, element.play, emeLogger.PlayCall); - - element.setMediaKeys = EmeListeners.extendEmeMethod( - element, element.setMediaKeys, emeLogger.SetMediaKeysCall); - - element.methodListenersAdded_ = true; -}; - - -/** - * Creates a wrapper function that logs calls to the given method. - * @param {!Object} element An element or object whose function - * call will be logged. - * @param {!Function} originalFn The function to log. - * @param {!Function} constructor The constructor for a logger class that will - * be instantiated to log the originalFn call. - * @return {!Function} The new version, with logging, of orginalFn. - */ -EmeListeners.extendEmeMethod = function(element, originalFn, constructor) { - return function() { - var result = originalFn.apply(element, arguments); - var args = [].slice.call(arguments); - var logObject = EmeListeners.logCall(constructor, args, result, element); - if (result && result.constructor.name == 'Promise') { - var description = logObject.title + ' Promise Result'; - result = result.then(function(resultObject) { - EmeListeners.logPromiseResult(description, 'resolved', resultObject, args); - return Promise.resolve(resultObject); - }).catch(function(error) { - EmeListeners.logPromiseResult(description, 'rejected', error, args); - return Promise.reject(error); - }); - } - return result; - }; -}; - - -/** - * Logs a method call to the console and a separate frame. - * @param {!Function} constructor The constructor for a logger class that will - * be instantiated to log this call. - * @param {Array} args The arguments this call was made with. - * @param {Object} result The result of this method call. - * @param {!Object} target The element this method was called on. - * @return {!emeLogger.EmeMethodCall} The data that has been logged. - */ -EmeListeners.logCall = function(constructor, args, result, target) { - var logObject = new constructor(args, target, result); - EmeListeners.logAndPostMessage_(logObject); - return logObject; -}; - - -/** - * Logs an event to the console and a separate frame. - * @param {!Function} constructor The constructor for a logger class that will - * be instantiated to log this event. - * @param {!Event} event An EME event. - * @return {!emeLogger.EmeEvent} The data that has been logged. - */ -EmeListeners.logEvent = function(constructor, event) { - var logObject = new constructor(event); - EmeListeners.logAndPostMessage_(logObject); - return logObject; -}; - - -/** - * Logs the result of a Promise to the console and a separate frame. - * @param {string} title A short description of this Promise. - * @param {string} status The status of this Promise. - * @param {Object} result The result of this Promise. - * @param {Array} args The arguments that were passed. - * @return {!emeLogger.PromiseResult} The data that has been logged. - */ -EmeListeners.logPromiseResult = function(title, status, result, args) { - var logObject = new emeLogger.PromiseResult(title, status, result, args); - EmeListeners.logAndPostMessage_(logObject); - return logObject; -}; - - -/** - * Logs the object to the console and posts it in a message. - * @param {emeLogger.EmeEvent|emeLogger.EmeMethodCall|emeLogger.PromiseResult} - * logObject The object to log. - * @private - */ -EmeListeners.logAndPostMessage_ = function(logObject) { - var message = emeLogger.getMessagePassableObject(logObject); - window.postMessage({data: message, type: 'emeLogMessage'}, '*'); - console.log(logObject); -}; - -setUp_(); diff --git a/log-window.js b/log-window.js new file mode 100644 index 0000000..e2396d4 --- /dev/null +++ b/log-window.js @@ -0,0 +1,297 @@ +/** + * Copyright 2021 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * @fileoverview Implements the log window. + */ + +class EmeLogWindow { + constructor() { + /** private {Window} */ + this.logWindow_ = null; + + /** @private {FileWriter} */ + this.fileWriter_ = null; + + /** @private {!Promise} */ + this.fileWriterOperation_ = Promise.resolve(); + + /** @private {string} */ + this.downloadUrl_ = ''; + + window.webkitRequestFileSystem( + window.PERSISTENT, + 5 * 1024 * 1024, + (fileSystem) => this.initLogFile_(fileSystem)); + } + + /** Open the log window. */ + open() { + if (!this.isOpen()) { + this.logWindow_ = window.open('log.html', 'EME Log', 'width=700,height=600'); + } + this.logWindow_.focus(); + if (this.downloadUrl_) { + this.initDownloadButton_(); + } + } + + /** @return {boolean} True if the log window is open. */ + isOpen() { + return this.logWindow_ != null && !this.logWindow_.closed; + } + + /** + * Initializes the log file. Any previous data will be cleared from the file. + * @param {FileSystem} fileSystem The FileSystem to contain the log. + * @private + */ + initLogFile_(fileSystem) { + fileSystem.root.getFile('log.txt', {create: true}, (fileEntry) => { + this.downloadUrl_ = fileEntry.toURL(); + if (this.logWindow_) { + this.initDownloadButton_(); + } + + fileEntry.createWriter((fileWriter) => { + this.fileWriter_ = fileWriter; + this.enqueueFileOperation_(() => this.fileWriter_.truncate(0)); + }); + }); + } + + /** + * Initializes the log file download button. + * @private + */ + initDownloadButton_() { + const downloadButton = + this.logWindow_.document.querySelector('#download-button'); + if (!downloadButton) { + // Wait for the document to finish loading, then call this method again. + this.logWindow_.addEventListener('DOMContentLoaded', () => { + this.initDownloadButton_(); + }); + } + + downloadButton.href = this.downloadUrl_; + // Now that a file can be downloaded, stop hiding the download button. + downloadButton.style.display = 'block'; + } + + /** + * Enqueue an operation for the file writer. The API is event-based, but + * asynchronous. We have to maintain the operations in order, and wait until + * the previous operation finishes before starting a new one. We accomplish + * this through a Promise chain. + * + * @param {function()} writeOperation + * @private + */ + enqueueFileOperation_(writeOperation) { + this.fileWriterOperation_ = this.fileWriterOperation_.then( + () => new Promise((resolve, reject) => { + writeOperation(); + this.fileWriter_.onwriteend = resolve; + this.fileWriter_.onerror = reject; + })); + } + + /** + * @param {Object} The serialized log to format in HTML. + */ + appendLog(log) { + if (!this.logWindow_) { + return; + } + + const logElement = this.logWindow_.document.querySelector('#eme-log'); + const li = document.createElement('li'); + logElement.appendChild(li); + + const heading = document.createElement('h3'); + li.appendChild(heading); + + const title = document.createElement('span'); + title.classList.add('title'); + title.style.color = 'blue'; + heading.appendChild(title); + + const timestamp = new Date(log.timestamp); + // ISO date strings look like '2021-10-21T22:54:46.629Z', which is dense a + // little hard to read. Tweak the output a little into something more like + // '2021-10-21 22:54:46.629'. + const formattedTimestamp = + timestamp.toISOString().replace('T', ' ').replace('Z', ''); + const time = document.createTextNode(' ' + formattedTimestamp); + heading.appendChild(time); + + const data = document.createElement('pre'); + data.classList.add('data'); + li.appendChild(data); + + if (log.type == 'Warning') { + title.textContent = 'WARNING'; + title.style.color = 'red'; + data.textContent = log.message; + } + + if (log.type == 'Constructor') { + title.textContent = `new ${log.className}`; + } else if (log.type == 'Method') { + title.textContent = `${log.className}.${log.methodName}`; + } else if (log.type == 'Getter' || log.type == 'Setter') { + title.textContent = `${log.className}.${log.memberName}`; + } else if (log.type == 'Event') { + title.textContent = `${log.className} ${log.eventName} Event`; + } + + if (log.type == 'Constructor' || log.type == 'Method') { + const args = log.args.map(arg => prettyPrint(arg)).join(', '); + data.textContent = `${title.textContent}(${args})`; + + if (log.threw) { + data.textContent += ` threw ${prettyPrint(log.threw)}`; + } else { + data.textContent += ` => ${prettyPrint(log.result)}`; + } + } else if (log.type == 'Getter') { + data.textContent = title.textContent; + + if (log.threw) { + data.textContent += ` threw ${prettyPrint(log.threw)}`; + } else { + data.textContent += ` => ${prettyPrint(log.result)}`; + } + } else if (log.type == 'Setter') { + data.textContent = title.textContent; + + if (log.threw) { + data.textContent += ` threw ${prettyPrint(log.threw)}`; + } else { + data.textContent += ` => ${prettyPrint(log.value)}`; + } + } else if (log.type == 'Event') { + data.textContent = `${log.className} ${prettyPrint(log.event)}`; + if (log.value) { + data.textContent = 'Associated value: ' + prettyPrint(log.value); + } + } + + if (this.fileWriter_) { + const textBasedLog = + formattedTimestamp + '\n\n' + + data.textContent + '\n\n\n\n'; + + this.enqueueFileOperation_(() => { + const blob = new Blob([textBasedLog], {type: 'text/plain'}); + this.fileWriter_.write(blob); + }); + } + } +} + +EmeLogWindow.instance = new EmeLogWindow(); +window.EmeLogWindow = EmeLogWindow; + + +/** + * @param {number} byte + * @return {string} + */ +function byteToHex(byte) { + return '0x' + byte.toString(16).padStart(2, '0'); +} + +/** + * @param {*} obj + * @param {string} indentation + * @return {string} + */ +function prettyPrint(obj, indentation = '') { + if (obj == null) { + return obj; + } + + // If it's a named type, unpack it and attach the name. + if (obj.__type__) { + let format = obj.__type__ + ' instance'; + + // This has fields like an object. + if (obj.__fields__) { + format += ' ' + prettyPrint(obj.__fields__, indentation); + } + + // This has a data array like an ArrayBufferView. + // TODO: Handle formatting for 16-bit and 32-bit values? + if (obj.__data__) { + const data = obj.__data__.slice(); // Make a copy + if (data.length == 0) { + format += '[]'; + } else { + format += ' ' + '[\n'; + while (data.length) { + const row = data.splice(0, 16); + format += indentation + ' '; + format += row.map(byteToHex).join(', '); + format += ',\n'; + } + format += indentation + ']'; + } + } + return format; + } + + if (Array.isArray(obj)) { + if (obj.length == 0) { + return '[]'; + } + let insides = ''; + for (const entry of obj) { + insides += indentation + ' '; + insides += prettyPrint(entry, indentation + ' ') + ',\n'; + } + return `[\n${insides}${indentation}]`; + } + + if (obj.constructor == Object) { + const keys = Object.keys(obj); + if (keys.length == 0) { + return '{}'; + } + let insides = ''; + for (const key of keys) { + insides += indentation + ' ' + key + ': '; + insides += prettyPrint(obj[key], indentation + ' ') + ',\n'; + } + return `{\n${insides}${indentation}}`; + } + + if (typeof obj == 'string') { + return `"${obj}"`; + } + + return obj.toString(); +} + +/** + * Listens for messages from the content script to append a log item to the + * current frame and log file. + */ +if (chrome.runtime) { + chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { + const log = request.log; + EmeLogWindow.instance.appendLog(log); + }); +} diff --git a/log_constructor.js b/log_constructor.js deleted file mode 100644 index ba91880..0000000 --- a/log_constructor.js +++ /dev/null @@ -1,301 +0,0 @@ -/** - * Copyright 2015 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @fileoverview Maintains the separate frame logging. - */ - - -var emeLogConstructor = {}; -var loggingWindow; - - -/** - * The writer used to write to the log file. - * @private {FileWriter} - */ -emeLogConstructor.logFileWriter_; - - -/** - * The URL of the log file. - * @private {string} - */ -emeLogConstructor.logFileUrl_ = ''; - - -/** - * The promise that manages writes to the log file. - * @private {Promise} - */ -emeLogConstructor.p_ = Promise.resolve(); - - -/** - * @typedef {{ - * title: string, - * timestamp: string, - * names: !Array., - * values: !Array.<*> - * }} - */ -emeLogConstructor.LogItemData; - - -/** - * @typedef {{ - * title: string, - * timestamp: string, - * logData: string - * }} - */ -emeLogConstructor.FormattedLogItem; - - -/** - * Builds a formatted representation of some log data. - * @param {!emeLogConstructor.LogItemData} data - * @return {!emeLogConstructor.FormattedLogItem} - */ -emeLogConstructor.buildFormattedLogItem = function(data) { - var logItem = {'title' : data.title, - 'timestamp' : data.timestamp, - 'logData' : emeLogConstructor.buildDataPairs(data, 0)}; - return logItem; -}; - - -/** - * Builds a log string from pairs of data. - * @param {!emeLogConstructor.LogItemData} data - * @param {number} indent Current indentation - * @return {string} A formatted string. - */ -emeLogConstructor.buildDataPairs = function(data, indent) { - var indentString = emeLogConstructor.getIndentString(++indent); - var item = ''; - for (var i = 0; i < data.names.length; ++i) { - var name = data.names[i]; - var value = emeLogConstructor.buildObjectItem(data.values[i], indent); - item += i ? '\n' : ''; - item += indentString + name + ': ' + value; - } - return item; -}; - - -/** - * Builds a formatted string from a data object. - * @param {undefined|number|string|boolean|emeLogConstructor.LogItemData} data - * The data to format. - * @param {number} indent Current indentation. - * @return {string} A formatted string. - */ -emeLogConstructor.buildObjectItem = function(data, indent) { - var getIndentString = emeLogConstructor.getIndentString; - - if (typeof(data) == 'number' || typeof(data) == 'boolean') { - return data.toString(); - } else if (typeof(data) == 'string') { - return data; - } else if (typeof(data) == 'object') { - if (!data) return 'null'; - if (data.names.length == 0) return data.title + '{}'; - - indent++; - var convertedData = ''; - switch (data.title) { - case 'Array': - // Print an array. The array could contain objects, strings or numbers. - convertedData += '['; - for (var i = 0; i < data.values.length - 1; ++i) { - var value = emeLogConstructor.buildObjectItem(data.values[i], indent); - convertedData += value + ', '; - } - if (data.values.length) { - convertedData += emeLogConstructor.buildObjectItem( - data.values[data.values.length - 1], indent); - } - convertedData += ']'; - break; - case 'Uint8Array': - // data.values contains the Uint8Array. This is an array of 8-bit - // unsigned integers. - while (data.values.length > 0) { - convertedData += - '\n' + getIndentString(indent) + data.values.splice(0, 20); - } - break; - case 'Object': - // Print name value pairs without title - convertedData += - '\n' + emeLogConstructor.buildDataPairs(data, --indent); - break; - default: - // Standard Object printing, with object title - var indentString = getIndentString(indent); - convertedData = '\n' + indentString + data.title + ' {\n'; - convertedData += emeLogConstructor.buildDataPairs(data, indent); - convertedData += '\n' + indentString + '}'; - } - return convertedData; - } else { - return 'undefined'; - } -}; - - -/** - * Builds an HTML log item and appends to the current logging frame. - * @param {!emeLogConstructor.FormattedLogItem} data The formatted data to log. - */ -emeLogConstructor.appendHtmlLogItem = function(data) { - var heading = document.createElement('h3'); - var span = document.createElement('span'); - span.style.color = 'blue'; - span.textContent = data.title; - var time = document.createTextNode(' ' + data.timestamp); - heading.appendChild(span); - heading.appendChild(time); - var pre = document.createElement('pre'); - pre.textContent = data.logData; - - var li = document.createElement('li'); - li.appendChild(heading); - li.appendChild(pre); - loggingWindow.document.querySelector('#eme-log').appendChild(li); -}; - - -/** - * @private {number} The number of spaces in a tab. - * @const - */ -emeLogConstructor.NUM_SPACES_ = 4; - - -/** - * Returns a string of spaces, corresponding to a number of tabs. - * @param {number} number The number of tabs to create. - * @return {string} A string of spaces. - */ -emeLogConstructor.getIndentString = function(number) { - return new Array(number * emeLogConstructor.NUM_SPACES_ + 1).join(' '); -}; - - -/** - * Opens a separate logging window. - */ -emeLogConstructor.openWindow = function() { - loggingWindow = window.open('log.html', 'EME Log', 'width=700,height=600'); - const downloadButton = - loggingWindow.document.querySelector('#download-button'); - downloadButton.href = emeLogConstructor.logFileUrl_; -}; - - -/** - * Closes the separate logging window. - */ -emeLogConstructor.closeWindow = function() { - loggingWindow.close(); -}; - - -/** - * Returns true if a separate window for logging is open, or false if it is not. - * @return {boolean} - */ -emeLogConstructor.isWindowOpen = function() { - return loggingWindow && !loggingWindow.closed ? true : false; -}; - - -/** - * Returns the log file URL or an empty string if the file has not been opened. - * @return {string} - */ -emeLogConstructor.getLogFileUrl = function() { - return emeLogConstructor.logFileUrl_; -}; - - -/** - * Listens for messages from the content script to append a log item to the - * current frame and log file. - */ -if (chrome.runtime) { - chrome.runtime.onMessage.addListener( - function(request, sender, sendResponse) { - var formattedData = - emeLogConstructor.buildFormattedLogItem(request.data.data); - if (loggingWindow) { - emeLogConstructor.appendHtmlLogItem(formattedData); - } - if (!emeLogConstructor.logFileWriter_) { - return; - } - emeLogConstructor.p_ = emeLogConstructor.p_.then(function() { - return new Promise(function(ok, fail) { - // Alias. - var fileWriter = emeLogConstructor.logFileWriter_; - - var textItem = - formattedData.title + ' ' + formattedData.timestamp + '\n' + - formattedData.logData + '\n\n'; - var logItem = new Blob([textItem], {type: 'text/plain'}); - fileWriter.write(logItem); - - fileWriter.onwriteend = ok; - fileWriter.onerror = fail; - }); - // TODO (natalieharris) Notify user of error in log file. - }).catch(function() {}); - }); -} - - -/** - * Initializes the log file. Any previous data will be cleared from the file. - * @param {FileSystem} filesystem The FileSystem to contain the log. - */ -emeLogConstructor.initializeLogFile = function(filesystem) { - var initializeFileWriter = function(fileWriter) { - emeLogConstructor.p_ = emeLogConstructor.p_.then(function() { - return new Promise(function(ok, fail) { - fileWriter.truncate(0); - fileWriter.onwriteend = ok; - fileWriter.onerror = fail; - }); - }); - emeLogConstructor.logFileWriter_ = fileWriter; - }; - - filesystem.root.getFile('log.txt', {create: true}, function(fileEntry) { - fileEntry.createWriter(initializeFileWriter); - emeLogConstructor.logFileUrl_ = fileEntry.toURL(); - if (loggingWindow) { - const downloadButton = - loggingWindow.document.querySelector('#download-button'); - downloadButton.href = emeLogConstructor.logFileUrl_; - } - }); -}; - - -window.webkitRequestFileSystem( - window.PERSISTENT, - 5 * 1024 * 1024, - emeLogConstructor.initializeLogFile); diff --git a/manifest.json b/manifest.json index 57e18a2..8cba240 100644 --- a/manifest.json +++ b/manifest.json @@ -22,7 +22,7 @@ "128": "icons/EME_logo_128.png" }, "background" : { - "scripts": ["log_constructor.js"] + "scripts": ["log-window.js"] }, "browser_action": { "default_title": "EME Logger", @@ -30,8 +30,7 @@ }, "permissions": ["unlimitedStorage"], "web_accessible_resources": [ - "mutation-summary.js", - "prototypes.js", - "eme_listeners.js" + "trace-anything.js", + "eme-trace-config.js" ] } diff --git a/mutation-summary.js b/mutation-summary.js deleted file mode 100644 index c5eaea1..0000000 --- a/mutation-summary.js +++ /dev/null @@ -1,1665 +0,0 @@ -// Copyright 2011 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -var __extends = this.__extends || function (d, b) { - for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; - function __() { this.constructor = d; } - __.prototype = b.prototype; - d.prototype = new __(); -}; -var MutationObserverCtor; -if (typeof WebKitMutationObserver !== 'undefined') - MutationObserverCtor = WebKitMutationObserver; -else - MutationObserverCtor = MutationObserver; - -if (MutationObserverCtor === undefined) { - console.error('DOM Mutation Observers are required.'); - console.error('https://developer.mozilla.org/en-US/docs/DOM/MutationObserver'); - throw Error('DOM Mutation Observers are required'); -} - -var NodeMap = (function () { - function NodeMap() { - this.nodes = []; - this.values = []; - } - NodeMap.prototype.isIndex = function (s) { - return +s === s >>> 0; - }; - - NodeMap.prototype.nodeId = function (node) { - var id = node[NodeMap.ID_PROP]; - if (!id) - id = node[NodeMap.ID_PROP] = NodeMap.nextId_++; - return id; - }; - - NodeMap.prototype.set = function (node, value) { - var id = this.nodeId(node); - this.nodes[id] = node; - this.values[id] = value; - }; - - NodeMap.prototype.get = function (node) { - var id = this.nodeId(node); - return this.values[id]; - }; - - NodeMap.prototype.has = function (node) { - return this.nodeId(node) in this.nodes; - }; - - NodeMap.prototype.delete = function (node) { - var id = this.nodeId(node); - delete this.nodes[id]; - this.values[id] = undefined; - }; - - NodeMap.prototype.keys = function () { - var nodes = []; - for (var id in this.nodes) { - if (!this.isIndex(id)) - continue; - nodes.push(this.nodes[id]); - } - - return nodes; - }; - NodeMap.ID_PROP = '__mutation_summary_node_map_id__'; - NodeMap.nextId_ = 1; - return NodeMap; -})(); - -/** -* var reachableMatchableProduct = [ -* // STAYED_OUT, ENTERED, STAYED_IN, EXITED -* [ STAYED_OUT, STAYED_OUT, STAYED_OUT, STAYED_OUT ], // STAYED_OUT -* [ STAYED_OUT, ENTERED, ENTERED, STAYED_OUT ], // ENTERED -* [ STAYED_OUT, ENTERED, STAYED_IN, EXITED ], // STAYED_IN -* [ STAYED_OUT, STAYED_OUT, EXITED, EXITED ] // EXITED -* ]; -*/ -var Movement; -(function (Movement) { - Movement[Movement["STAYED_OUT"] = 0] = "STAYED_OUT"; - Movement[Movement["ENTERED"] = 1] = "ENTERED"; - Movement[Movement["STAYED_IN"] = 2] = "STAYED_IN"; - Movement[Movement["REPARENTED"] = 3] = "REPARENTED"; - Movement[Movement["REORDERED"] = 4] = "REORDERED"; - Movement[Movement["EXITED"] = 5] = "EXITED"; -})(Movement || (Movement = {})); - -function enteredOrExited(changeType) { - return changeType === 1 /* ENTERED */ || changeType === 5 /* EXITED */; -} - -var NodeChange = (function () { - function NodeChange(node, childList, attributes, characterData, oldParentNode, added, attributeOldValues, characterDataOldValue) { - if (typeof childList === "undefined") { childList = false; } - if (typeof attributes === "undefined") { attributes = false; } - if (typeof characterData === "undefined") { characterData = false; } - if (typeof oldParentNode === "undefined") { oldParentNode = null; } - if (typeof added === "undefined") { added = false; } - if (typeof attributeOldValues === "undefined") { attributeOldValues = null; } - if (typeof characterDataOldValue === "undefined") { characterDataOldValue = null; } - this.node = node; - this.childList = childList; - this.attributes = attributes; - this.characterData = characterData; - this.oldParentNode = oldParentNode; - this.added = added; - this.attributeOldValues = attributeOldValues; - this.characterDataOldValue = characterDataOldValue; - this.isCaseInsensitive = this.node.nodeType === Node.ELEMENT_NODE && this.node instanceof HTMLElement && this.node.ownerDocument instanceof HTMLDocument; - } - NodeChange.prototype.getAttributeOldValue = function (name) { - if (!this.attributeOldValues) - return undefined; - if (this.isCaseInsensitive) - name = name.toLowerCase(); - return this.attributeOldValues[name]; - }; - - NodeChange.prototype.getAttributeNamesMutated = function () { - var names = []; - if (!this.attributeOldValues) - return names; - for (var name in this.attributeOldValues) { - names.push(name); - } - return names; - }; - - NodeChange.prototype.attributeMutated = function (name, oldValue) { - this.attributes = true; - this.attributeOldValues = this.attributeOldValues || {}; - - if (name in this.attributeOldValues) - return; - - this.attributeOldValues[name] = oldValue; - }; - - NodeChange.prototype.characterDataMutated = function (oldValue) { - if (this.characterData) - return; - this.characterData = true; - this.characterDataOldValue = oldValue; - }; - - // Note: is it possible to receive a removal followed by a removal. This - // can occur if the removed node is added to an non-observed node, that - // node is added to the observed area, and then the node removed from - // it. - NodeChange.prototype.removedFromParent = function (parent) { - this.childList = true; - if (this.added || this.oldParentNode) - this.added = false; - else - this.oldParentNode = parent; - }; - - NodeChange.prototype.insertedIntoParent = function () { - this.childList = true; - this.added = true; - }; - - // An node's oldParent is - // -its present parent, if its parentNode was not changed. - // -null if the first thing that happened to it was an add. - // -the node it was removed from if the first thing that happened to it - // was a remove. - NodeChange.prototype.getOldParent = function () { - if (this.childList) { - if (this.oldParentNode) - return this.oldParentNode; - if (this.added) - return null; - } - - return this.node.parentNode; - }; - return NodeChange; -})(); - -var ChildListChange = (function () { - function ChildListChange() { - this.added = new NodeMap(); - this.removed = new NodeMap(); - this.maybeMoved = new NodeMap(); - this.oldPrevious = new NodeMap(); - this.moved = undefined; - } - return ChildListChange; -})(); - -var TreeChanges = (function (_super) { - __extends(TreeChanges, _super); - function TreeChanges(rootNode, mutations) { - _super.call(this); - - this.rootNode = rootNode; - this.reachableCache = undefined; - this.wasReachableCache = undefined; - this.anyParentsChanged = false; - this.anyAttributesChanged = false; - this.anyCharacterDataChanged = false; - - for (var m = 0; m < mutations.length; m++) { - var mutation = mutations[m]; - switch (mutation.type) { - case 'childList': - this.anyParentsChanged = true; - for (var i = 0; i < mutation.removedNodes.length; i++) { - var node = mutation.removedNodes[i]; - this.getChange(node).removedFromParent(mutation.target); - } - for (var i = 0; i < mutation.addedNodes.length; i++) { - var node = mutation.addedNodes[i]; - this.getChange(node).insertedIntoParent(); - } - break; - - case 'attributes': - this.anyAttributesChanged = true; - var change = this.getChange(mutation.target); - change.attributeMutated(mutation.attributeName, mutation.oldValue); - break; - - case 'characterData': - this.anyCharacterDataChanged = true; - var change = this.getChange(mutation.target); - change.characterDataMutated(mutation.oldValue); - break; - } - } - } - TreeChanges.prototype.getChange = function (node) { - var change = this.get(node); - if (!change) { - change = new NodeChange(node); - this.set(node, change); - } - return change; - }; - - TreeChanges.prototype.getOldParent = function (node) { - var change = this.get(node); - return change ? change.getOldParent() : node.parentNode; - }; - - TreeChanges.prototype.getIsReachable = function (node) { - if (node === this.rootNode) - return true; - if (!node) - return false; - - this.reachableCache = this.reachableCache || new NodeMap(); - var isReachable = this.reachableCache.get(node); - if (isReachable === undefined) { - isReachable = this.getIsReachable(node.parentNode); - this.reachableCache.set(node, isReachable); - } - return isReachable; - }; - - // A node wasReachable if its oldParent wasReachable. - TreeChanges.prototype.getWasReachable = function (node) { - if (node === this.rootNode) - return true; - if (!node) - return false; - - this.wasReachableCache = this.wasReachableCache || new NodeMap(); - var wasReachable = this.wasReachableCache.get(node); - if (wasReachable === undefined) { - wasReachable = this.getWasReachable(this.getOldParent(node)); - this.wasReachableCache.set(node, wasReachable); - } - return wasReachable; - }; - - TreeChanges.prototype.reachabilityChange = function (node) { - if (this.getIsReachable(node)) { - return this.getWasReachable(node) ? 2 /* STAYED_IN */ : 1 /* ENTERED */; - } - - return this.getWasReachable(node) ? 5 /* EXITED */ : 0 /* STAYED_OUT */; - }; - return TreeChanges; -})(NodeMap); - -var MutationProjection = (function () { - // TOOD(any) - function MutationProjection(rootNode, mutations, selectors, calcReordered, calcOldPreviousSibling) { - this.rootNode = rootNode; - this.mutations = mutations; - this.selectors = selectors; - this.calcReordered = calcReordered; - this.calcOldPreviousSibling = calcOldPreviousSibling; - this.treeChanges = new TreeChanges(rootNode, mutations); - this.entered = []; - this.exited = []; - this.stayedIn = new NodeMap(); - this.visited = new NodeMap(); - this.childListChangeMap = undefined; - this.characterDataOnly = undefined; - this.matchCache = undefined; - - this.processMutations(); - } - MutationProjection.prototype.processMutations = function () { - if (!this.treeChanges.anyParentsChanged && !this.treeChanges.anyAttributesChanged) - return; - - var changedNodes = this.treeChanges.keys(); - for (var i = 0; i < changedNodes.length; i++) { - this.visitNode(changedNodes[i], undefined); - } - }; - - MutationProjection.prototype.visitNode = function (node, parentReachable) { - if (this.visited.has(node)) - return; - - this.visited.set(node, true); - - var change = this.treeChanges.get(node); - var reachable = parentReachable; - - // node inherits its parent's reachability change unless - // its parentNode was mutated. - if ((change && change.childList) || reachable == undefined) - reachable = this.treeChanges.reachabilityChange(node); - - if (reachable === 0 /* STAYED_OUT */) - return; - - // Cache match results for sub-patterns. - this.matchabilityChange(node); - - if (reachable === 1 /* ENTERED */) { - this.entered.push(node); - } else if (reachable === 5 /* EXITED */) { - this.exited.push(node); - this.ensureHasOldPreviousSiblingIfNeeded(node); - } else if (reachable === 2 /* STAYED_IN */) { - var movement = 2 /* STAYED_IN */; - - if (change && change.childList) { - if (change.oldParentNode !== node.parentNode) { - movement = 3 /* REPARENTED */; - this.ensureHasOldPreviousSiblingIfNeeded(node); - } else if (this.calcReordered && this.wasReordered(node)) { - movement = 4 /* REORDERED */; - } - } - - this.stayedIn.set(node, movement); - } - - if (reachable === 2 /* STAYED_IN */) - return; - - for (var child = node.firstChild; child; child = child.nextSibling) { - this.visitNode(child, reachable); - } - }; - - MutationProjection.prototype.ensureHasOldPreviousSiblingIfNeeded = function (node) { - if (!this.calcOldPreviousSibling) - return; - - this.processChildlistChanges(); - - var parentNode = node.parentNode; - var nodeChange = this.treeChanges.get(node); - if (nodeChange && nodeChange.oldParentNode) - parentNode = nodeChange.oldParentNode; - - var change = this.childListChangeMap.get(parentNode); - if (!change) { - change = new ChildListChange(); - this.childListChangeMap.set(parentNode, change); - } - - if (!change.oldPrevious.has(node)) { - change.oldPrevious.set(node, node.previousSibling); - } - }; - - MutationProjection.prototype.getChanged = function (summary, selectors, characterDataOnly) { - this.selectors = selectors; - this.characterDataOnly = characterDataOnly; - - for (var i = 0; i < this.entered.length; i++) { - var node = this.entered[i]; - var matchable = this.matchabilityChange(node); - if (matchable === 1 /* ENTERED */ || matchable === 2 /* STAYED_IN */) - summary.added.push(node); - } - - var stayedInNodes = this.stayedIn.keys(); - for (var i = 0; i < stayedInNodes.length; i++) { - var node = stayedInNodes[i]; - var matchable = this.matchabilityChange(node); - - if (matchable === 1 /* ENTERED */) { - summary.added.push(node); - } else if (matchable === 5 /* EXITED */) { - summary.removed.push(node); - } else if (matchable === 2 /* STAYED_IN */ && (summary.reparented || summary.reordered)) { - var movement = this.stayedIn.get(node); - if (summary.reparented && movement === 3 /* REPARENTED */) - summary.reparented.push(node); - else if (summary.reordered && movement === 4 /* REORDERED */) - summary.reordered.push(node); - } - } - - for (var i = 0; i < this.exited.length; i++) { - var node = this.exited[i]; - var matchable = this.matchabilityChange(node); - if (matchable === 5 /* EXITED */ || matchable === 2 /* STAYED_IN */) - summary.removed.push(node); - } - }; - - MutationProjection.prototype.getOldParentNode = function (node) { - var change = this.treeChanges.get(node); - if (change && change.childList) - return change.oldParentNode ? change.oldParentNode : null; - - var reachabilityChange = this.treeChanges.reachabilityChange(node); - if (reachabilityChange === 0 /* STAYED_OUT */ || reachabilityChange === 1 /* ENTERED */) - throw Error('getOldParentNode requested on invalid node.'); - - return node.parentNode; - }; - - MutationProjection.prototype.getOldPreviousSibling = function (node) { - var parentNode = node.parentNode; - var nodeChange = this.treeChanges.get(node); - if (nodeChange && nodeChange.oldParentNode) - parentNode = nodeChange.oldParentNode; - - var change = this.childListChangeMap.get(parentNode); - if (!change) - throw Error('getOldPreviousSibling requested on invalid node.'); - - return change.oldPrevious.get(node); - }; - - MutationProjection.prototype.getOldAttribute = function (element, attrName) { - var change = this.treeChanges.get(element); - if (!change || !change.attributes) - throw Error('getOldAttribute requested on invalid node.'); - - var value = change.getAttributeOldValue(attrName); - if (value === undefined) - throw Error('getOldAttribute requested for unchanged attribute name.'); - - return value; - }; - - MutationProjection.prototype.attributeChangedNodes = function (includeAttributes) { - if (!this.treeChanges.anyAttributesChanged) - return {}; - - var attributeFilter; - var caseInsensitiveFilter; - if (includeAttributes) { - attributeFilter = {}; - caseInsensitiveFilter = {}; - for (var i = 0; i < includeAttributes.length; i++) { - var attrName = includeAttributes[i]; - attributeFilter[attrName] = true; - caseInsensitiveFilter[attrName.toLowerCase()] = attrName; - } - } - - var result = {}; - var nodes = this.treeChanges.keys(); - - for (var i = 0; i < nodes.length; i++) { - var node = nodes[i]; - - var change = this.treeChanges.get(node); - if (!change.attributes) - continue; - - if (2 /* STAYED_IN */ !== this.treeChanges.reachabilityChange(node) || 2 /* STAYED_IN */ !== this.matchabilityChange(node)) { - continue; - } - - var element = node; - var changedAttrNames = change.getAttributeNamesMutated(); - for (var j = 0; j < changedAttrNames.length; j++) { - var attrName = changedAttrNames[j]; - - if (attributeFilter && !attributeFilter[attrName] && !(change.isCaseInsensitive && caseInsensitiveFilter[attrName])) { - continue; - } - - var oldValue = change.getAttributeOldValue(attrName); - if (oldValue === element.getAttribute(attrName)) - continue; - - if (caseInsensitiveFilter && change.isCaseInsensitive) - attrName = caseInsensitiveFilter[attrName]; - - result[attrName] = result[attrName] || []; - result[attrName].push(element); - } - } - - return result; - }; - - MutationProjection.prototype.getOldCharacterData = function (node) { - var change = this.treeChanges.get(node); - if (!change || !change.characterData) - throw Error('getOldCharacterData requested on invalid node.'); - - return change.characterDataOldValue; - }; - - MutationProjection.prototype.getCharacterDataChanged = function () { - if (!this.treeChanges.anyCharacterDataChanged) - return []; - - var nodes = this.treeChanges.keys(); - var result = []; - for (var i = 0; i < nodes.length; i++) { - var target = nodes[i]; - if (2 /* STAYED_IN */ !== this.treeChanges.reachabilityChange(target)) - continue; - - var change = this.treeChanges.get(target); - if (!change.characterData || target.textContent == change.characterDataOldValue) - continue; - - result.push(target); - } - - return result; - }; - - MutationProjection.prototype.computeMatchabilityChange = function (selector, el) { - if (!this.matchCache) - this.matchCache = []; - if (!this.matchCache[selector.uid]) - this.matchCache[selector.uid] = new NodeMap(); - - var cache = this.matchCache[selector.uid]; - var result = cache.get(el); - if (result === undefined) { - result = selector.matchabilityChange(el, this.treeChanges.get(el)); - cache.set(el, result); - } - return result; - }; - - MutationProjection.prototype.matchabilityChange = function (node) { - var _this = this; - // TODO(rafaelw): Include PI, CDATA? - // Only include text nodes. - if (this.characterDataOnly) { - switch (node.nodeType) { - case Node.COMMENT_NODE: - case Node.TEXT_NODE: - return 2 /* STAYED_IN */; - default: - return 0 /* STAYED_OUT */; - } - } - - // No element filter. Include all nodes. - if (!this.selectors) - return 2 /* STAYED_IN */; - - // Element filter. Exclude non-elements. - if (node.nodeType !== Node.ELEMENT_NODE) - return 0 /* STAYED_OUT */; - - var el = node; - - var matchChanges = this.selectors.map(function (selector) { - return _this.computeMatchabilityChange(selector, el); - }); - - var accum = 0 /* STAYED_OUT */; - var i = 0; - - while (accum !== 2 /* STAYED_IN */ && i < matchChanges.length) { - switch (matchChanges[i]) { - case 2 /* STAYED_IN */: - accum = 2 /* STAYED_IN */; - break; - case 1 /* ENTERED */: - if (accum === 5 /* EXITED */) - accum = 2 /* STAYED_IN */; - else - accum = 1 /* ENTERED */; - break; - case 5 /* EXITED */: - if (accum === 1 /* ENTERED */) - accum = 2 /* STAYED_IN */; - else - accum = 5 /* EXITED */; - break; - } - - i++; - } - - return accum; - }; - - MutationProjection.prototype.getChildlistChange = function (el) { - var change = this.childListChangeMap.get(el); - if (!change) { - change = new ChildListChange(); - this.childListChangeMap.set(el, change); - } - - return change; - }; - - MutationProjection.prototype.processChildlistChanges = function () { - if (this.childListChangeMap) - return; - - this.childListChangeMap = new NodeMap(); - - for (var i = 0; i < this.mutations.length; i++) { - var mutation = this.mutations[i]; - if (mutation.type != 'childList') - continue; - - if (this.treeChanges.reachabilityChange(mutation.target) !== 2 /* STAYED_IN */ && !this.calcOldPreviousSibling) - continue; - - var change = this.getChildlistChange(mutation.target); - - var oldPrevious = mutation.previousSibling; - - function recordOldPrevious(node, previous) { - if (!node || change.oldPrevious.has(node) || change.added.has(node) || change.maybeMoved.has(node)) - return; - - if (previous && (change.added.has(previous) || change.maybeMoved.has(previous))) - return; - - change.oldPrevious.set(node, previous); - } - - for (var j = 0; j < mutation.removedNodes.length; j++) { - var node = mutation.removedNodes[j]; - recordOldPrevious(node, oldPrevious); - - if (change.added.has(node)) { - change.added.delete(node); - } else { - change.removed.set(node, true); - change.maybeMoved.delete(node); - } - - oldPrevious = node; - } - - recordOldPrevious(mutation.nextSibling, oldPrevious); - - for (var j = 0; j < mutation.addedNodes.length; j++) { - var node = mutation.addedNodes[j]; - if (change.removed.has(node)) { - change.removed.delete(node); - change.maybeMoved.set(node, true); - } else { - change.added.set(node, true); - } - } - } - }; - - MutationProjection.prototype.wasReordered = function (node) { - if (!this.treeChanges.anyParentsChanged) - return false; - - this.processChildlistChanges(); - - var parentNode = node.parentNode; - var nodeChange = this.treeChanges.get(node); - if (nodeChange && nodeChange.oldParentNode) - parentNode = nodeChange.oldParentNode; - - var change = this.childListChangeMap.get(parentNode); - if (!change) - return false; - - if (change.moved) - return change.moved.get(node); - - change.moved = new NodeMap(); - var pendingMoveDecision = new NodeMap(); - - function isMoved(node) { - if (!node) - return false; - if (!change.maybeMoved.has(node)) - return false; - - var didMove = change.moved.get(node); - if (didMove !== undefined) - return didMove; - - if (pendingMoveDecision.has(node)) { - didMove = true; - } else { - pendingMoveDecision.set(node, true); - didMove = getPrevious(node) !== getOldPrevious(node); - } - - if (pendingMoveDecision.has(node)) { - pendingMoveDecision.delete(node); - change.moved.set(node, didMove); - } else { - didMove = change.moved.get(node); - } - - return didMove; - } - - var oldPreviousCache = new NodeMap(); - function getOldPrevious(node) { - var oldPrevious = oldPreviousCache.get(node); - if (oldPrevious !== undefined) - return oldPrevious; - - oldPrevious = change.oldPrevious.get(node); - while (oldPrevious && (change.removed.has(oldPrevious) || isMoved(oldPrevious))) { - oldPrevious = getOldPrevious(oldPrevious); - } - - if (oldPrevious === undefined) - oldPrevious = node.previousSibling; - oldPreviousCache.set(node, oldPrevious); - - return oldPrevious; - } - - var previousCache = new NodeMap(); - function getPrevious(node) { - if (previousCache.has(node)) - return previousCache.get(node); - - var previous = node.previousSibling; - while (previous && (change.added.has(previous) || isMoved(previous))) - previous = previous.previousSibling; - - previousCache.set(node, previous); - return previous; - } - - change.maybeMoved.keys().forEach(isMoved); - return change.moved.get(node); - }; - return MutationProjection; -})(); - -var Summary = (function () { - function Summary(projection, query) { - var _this = this; - this.projection = projection; - this.added = []; - this.removed = []; - this.reparented = query.all || query.element ? [] : undefined; - this.reordered = query.all ? [] : undefined; - - projection.getChanged(this, query.elementFilter, query.characterData); - - if (query.all || query.attribute || query.attributeList) { - var filter = query.attribute ? [query.attribute] : query.attributeList; - var attributeChanged = projection.attributeChangedNodes(filter); - - if (query.attribute) { - this.valueChanged = attributeChanged[query.attribute] || []; - } else { - this.attributeChanged = attributeChanged; - if (query.attributeList) { - query.attributeList.forEach(function (attrName) { - if (!_this.attributeChanged.hasOwnProperty(attrName)) - _this.attributeChanged[attrName] = []; - }); - } - } - } - - if (query.all || query.characterData) { - var characterDataChanged = projection.getCharacterDataChanged(); - - if (query.characterData) - this.valueChanged = characterDataChanged; - else - this.characterDataChanged = characterDataChanged; - } - - if (this.reordered) - this.getOldPreviousSibling = projection.getOldPreviousSibling.bind(projection); - } - Summary.prototype.getOldParentNode = function (node) { - return this.projection.getOldParentNode(node); - }; - - Summary.prototype.getOldAttribute = function (node, name) { - return this.projection.getOldAttribute(node, name); - }; - - Summary.prototype.getOldCharacterData = function (node) { - return this.projection.getOldCharacterData(node); - }; - - Summary.prototype.getOldPreviousSibling = function (node) { - return this.projection.getOldPreviousSibling(node); - }; - return Summary; -})(); - -// TODO(rafaelw): Allow ':' and '.' as valid name characters. -var validNameInitialChar = /[a-zA-Z_]+/; -var validNameNonInitialChar = /[a-zA-Z0-9_\-]+/; - -// TODO(rafaelw): Consider allowing backslash in the attrValue. -// TODO(rafaelw): There's got a to be way to represent this state machine -// more compactly??? -function escapeQuotes(value) { - return '"' + value.replace(/"/, '\\\"') + '"'; -} - -var Qualifier = (function () { - function Qualifier() { - } - Qualifier.prototype.matches = function (oldValue) { - if (oldValue === null) - return false; - - if (this.attrValue === undefined) - return true; - - if (!this.contains) - return this.attrValue == oldValue; - - var tokens = oldValue.split(' '); - for (var i = 0; i < tokens.length; i++) { - if (this.attrValue === tokens[i]) - return true; - } - - return false; - }; - - Qualifier.prototype.toString = function () { - if (this.attrName === 'class' && this.contains) - return '.' + this.attrValue; - - if (this.attrName === 'id' && !this.contains) - return '#' + this.attrValue; - - if (this.contains) - return '[' + this.attrName + '~=' + escapeQuotes(this.attrValue) + ']'; - - if ('attrValue' in this) - return '[' + this.attrName + '=' + escapeQuotes(this.attrValue) + ']'; - - return '[' + this.attrName + ']'; - }; - return Qualifier; -})(); - -var Selector = (function () { - function Selector() { - this.uid = Selector.nextUid++; - this.qualifiers = []; - } - Object.defineProperty(Selector.prototype, "caseInsensitiveTagName", { - get: function () { - return this.tagName.toUpperCase(); - }, - enumerable: true, - configurable: true - }); - - Object.defineProperty(Selector.prototype, "selectorString", { - get: function () { - return this.tagName + this.qualifiers.join(''); - }, - enumerable: true, - configurable: true - }); - - Selector.prototype.isMatching = function (el) { - return el[Selector.matchesSelector](this.selectorString); - }; - - Selector.prototype.wasMatching = function (el, change, isMatching) { - if (!change || !change.attributes) - return isMatching; - - var tagName = change.isCaseInsensitive ? this.caseInsensitiveTagName : this.tagName; - if (tagName !== '*' && tagName !== el.tagName) - return false; - - var attributeOldValues = []; - var anyChanged = false; - for (var i = 0; i < this.qualifiers.length; i++) { - var qualifier = this.qualifiers[i]; - var oldValue = change.getAttributeOldValue(qualifier.attrName); - attributeOldValues.push(oldValue); - anyChanged = anyChanged || (oldValue !== undefined); - } - - if (!anyChanged) - return isMatching; - - for (var i = 0; i < this.qualifiers.length; i++) { - var qualifier = this.qualifiers[i]; - var oldValue = attributeOldValues[i]; - if (oldValue === undefined) - oldValue = el.getAttribute(qualifier.attrName); - if (!qualifier.matches(oldValue)) - return false; - } - - return true; - }; - - Selector.prototype.matchabilityChange = function (el, change) { - var isMatching = this.isMatching(el); - if (isMatching) - return this.wasMatching(el, change, isMatching) ? 2 /* STAYED_IN */ : 1 /* ENTERED */; - else - return this.wasMatching(el, change, isMatching) ? 5 /* EXITED */ : 0 /* STAYED_OUT */; - }; - - Selector.parseSelectors = function (input) { - var selectors = []; - var currentSelector; - var currentQualifier; - - function newSelector() { - if (currentSelector) { - if (currentQualifier) { - currentSelector.qualifiers.push(currentQualifier); - currentQualifier = undefined; - } - - selectors.push(currentSelector); - } - currentSelector = new Selector(); - } - - function newQualifier() { - if (currentQualifier) - currentSelector.qualifiers.push(currentQualifier); - - currentQualifier = new Qualifier(); - } - - var WHITESPACE = /\s/; - var valueQuoteChar; - var SYNTAX_ERROR = 'Invalid or unsupported selector syntax.'; - - var SELECTOR = 1; - var TAG_NAME = 2; - var QUALIFIER = 3; - var QUALIFIER_NAME_FIRST_CHAR = 4; - var QUALIFIER_NAME = 5; - var ATTR_NAME_FIRST_CHAR = 6; - var ATTR_NAME = 7; - var EQUIV_OR_ATTR_QUAL_END = 8; - var EQUAL = 9; - var ATTR_QUAL_END = 10; - var VALUE_FIRST_CHAR = 11; - var VALUE = 12; - var QUOTED_VALUE = 13; - var SELECTOR_SEPARATOR = 14; - - var state = SELECTOR; - var i = 0; - while (i < input.length) { - var c = input[i++]; - - switch (state) { - case SELECTOR: - if (c.match(validNameInitialChar)) { - newSelector(); - currentSelector.tagName = c; - state = TAG_NAME; - break; - } - - if (c == '*') { - newSelector(); - currentSelector.tagName = '*'; - state = QUALIFIER; - break; - } - - if (c == '.') { - newSelector(); - newQualifier(); - currentSelector.tagName = '*'; - currentQualifier.attrName = 'class'; - currentQualifier.contains = true; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '#') { - newSelector(); - newQualifier(); - currentSelector.tagName = '*'; - currentQualifier.attrName = 'id'; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '[') { - newSelector(); - newQualifier(); - currentSelector.tagName = '*'; - currentQualifier.attrName = ''; - state = ATTR_NAME_FIRST_CHAR; - break; - } - - if (c.match(WHITESPACE)) - break; - - throw Error(SYNTAX_ERROR); - - case TAG_NAME: - if (c.match(validNameNonInitialChar)) { - currentSelector.tagName += c; - break; - } - - if (c == '.') { - newQualifier(); - currentQualifier.attrName = 'class'; - currentQualifier.contains = true; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '#') { - newQualifier(); - currentQualifier.attrName = 'id'; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '[') { - newQualifier(); - currentQualifier.attrName = ''; - state = ATTR_NAME_FIRST_CHAR; - break; - } - - if (c.match(WHITESPACE)) { - state = SELECTOR_SEPARATOR; - break; - } - - if (c == ',') { - state = SELECTOR; - break; - } - - throw Error(SYNTAX_ERROR); - - case QUALIFIER: - if (c == '.') { - newQualifier(); - currentQualifier.attrName = 'class'; - currentQualifier.contains = true; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '#') { - newQualifier(); - currentQualifier.attrName = 'id'; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '[') { - newQualifier(); - currentQualifier.attrName = ''; - state = ATTR_NAME_FIRST_CHAR; - break; - } - - if (c.match(WHITESPACE)) { - state = SELECTOR_SEPARATOR; - break; - } - - if (c == ',') { - state = SELECTOR; - break; - } - - throw Error(SYNTAX_ERROR); - - case QUALIFIER_NAME_FIRST_CHAR: - if (c.match(validNameInitialChar)) { - currentQualifier.attrValue = c; - state = QUALIFIER_NAME; - break; - } - - throw Error(SYNTAX_ERROR); - - case QUALIFIER_NAME: - if (c.match(validNameNonInitialChar)) { - currentQualifier.attrValue += c; - break; - } - - if (c == '.') { - newQualifier(); - currentQualifier.attrName = 'class'; - currentQualifier.contains = true; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '#') { - newQualifier(); - currentQualifier.attrName = 'id'; - state = QUALIFIER_NAME_FIRST_CHAR; - break; - } - if (c == '[') { - newQualifier(); - state = ATTR_NAME_FIRST_CHAR; - break; - } - - if (c.match(WHITESPACE)) { - state = SELECTOR_SEPARATOR; - break; - } - if (c == ',') { - state = SELECTOR; - break; - } - - throw Error(SYNTAX_ERROR); - - case ATTR_NAME_FIRST_CHAR: - if (c.match(validNameInitialChar)) { - currentQualifier.attrName = c; - state = ATTR_NAME; - break; - } - - if (c.match(WHITESPACE)) - break; - - throw Error(SYNTAX_ERROR); - - case ATTR_NAME: - if (c.match(validNameNonInitialChar)) { - currentQualifier.attrName += c; - break; - } - - if (c.match(WHITESPACE)) { - state = EQUIV_OR_ATTR_QUAL_END; - break; - } - - if (c == '~') { - currentQualifier.contains = true; - state = EQUAL; - break; - } - - if (c == '=') { - currentQualifier.attrValue = ''; - state = VALUE_FIRST_CHAR; - break; - } - - if (c == ']') { - state = QUALIFIER; - break; - } - - throw Error(SYNTAX_ERROR); - - case EQUIV_OR_ATTR_QUAL_END: - if (c == '~') { - currentQualifier.contains = true; - state = EQUAL; - break; - } - - if (c == '=') { - currentQualifier.attrValue = ''; - state = VALUE_FIRST_CHAR; - break; - } - - if (c == ']') { - state = QUALIFIER; - break; - } - - if (c.match(WHITESPACE)) - break; - - throw Error(SYNTAX_ERROR); - - case EQUAL: - if (c == '=') { - currentQualifier.attrValue = ''; - state = VALUE_FIRST_CHAR; - break; - } - - throw Error(SYNTAX_ERROR); - - case ATTR_QUAL_END: - if (c == ']') { - state = QUALIFIER; - break; - } - - if (c.match(WHITESPACE)) - break; - - throw Error(SYNTAX_ERROR); - - case VALUE_FIRST_CHAR: - if (c.match(WHITESPACE)) - break; - - if (c == '"' || c == "'") { - valueQuoteChar = c; - state = QUOTED_VALUE; - break; - } - - currentQualifier.attrValue += c; - state = VALUE; - break; - - case VALUE: - if (c.match(WHITESPACE)) { - state = ATTR_QUAL_END; - break; - } - if (c == ']') { - state = QUALIFIER; - break; - } - if (c == "'" || c == '"') - throw Error(SYNTAX_ERROR); - - currentQualifier.attrValue += c; - break; - - case QUOTED_VALUE: - if (c == valueQuoteChar) { - state = ATTR_QUAL_END; - break; - } - - currentQualifier.attrValue += c; - break; - - case SELECTOR_SEPARATOR: - if (c.match(WHITESPACE)) - break; - - if (c == ',') { - state = SELECTOR; - break; - } - - throw Error(SYNTAX_ERROR); - } - } - - switch (state) { - case SELECTOR: - case TAG_NAME: - case QUALIFIER: - case QUALIFIER_NAME: - case SELECTOR_SEPARATOR: - // Valid end states. - newSelector(); - break; - default: - throw Error(SYNTAX_ERROR); - } - - if (!selectors.length) - throw Error(SYNTAX_ERROR); - - return selectors; - }; - Selector.nextUid = 1; - Selector.matchesSelector = (function () { - var element = document.createElement('div'); - if (typeof element['webkitMatchesSelector'] === 'function') - return 'webkitMatchesSelector'; - if (typeof element['mozMatchesSelector'] === 'function') - return 'mozMatchesSelector'; - if (typeof element['msMatchesSelector'] === 'function') - return 'msMatchesSelector'; - - return 'matchesSelector'; - })(); - return Selector; -})(); - -var attributeFilterPattern = /^([a-zA-Z:_]+[a-zA-Z0-9_\-:\.]*)$/; - -function validateAttribute(attribute) { - if (typeof attribute != 'string') - throw Error('Invalid request opion. attribute must be a non-zero length string.'); - - attribute = attribute.trim(); - - if (!attribute) - throw Error('Invalid request opion. attribute must be a non-zero length string.'); - - if (!attribute.match(attributeFilterPattern)) - throw Error('Invalid request option. invalid attribute name: ' + attribute); - - return attribute; -} - -function validateElementAttributes(attribs) { - if (!attribs.trim().length) - throw Error('Invalid request option: elementAttributes must contain at least one attribute.'); - - var lowerAttributes = {}; - var attributes = {}; - - var tokens = attribs.split(/\s+/); - for (var i = 0; i < tokens.length; i++) { - var name = tokens[i]; - if (!name) - continue; - - var name = validateAttribute(name); - var nameLower = name.toLowerCase(); - if (lowerAttributes[nameLower]) - throw Error('Invalid request option: observing multiple case variations of the same attribute is not supported.'); - - attributes[name] = true; - lowerAttributes[nameLower] = true; - } - - return Object.keys(attributes); -} - -function elementFilterAttributes(selectors) { - var attributes = {}; - - selectors.forEach(function (selector) { - selector.qualifiers.forEach(function (qualifier) { - attributes[qualifier.attrName] = true; - }); - }); - - return Object.keys(attributes); -} - -var MutationSummary = (function () { - function MutationSummary(opts) { - var _this = this; - this.connected = false; - this.options = MutationSummary.validateOptions(opts); - this.observerOptions = MutationSummary.createObserverOptions(this.options.queries); - this.root = this.options.rootNode; - this.callback = this.options.callback; - - this.elementFilter = Array.prototype.concat.apply([], this.options.queries.map(function (query) { - return query.elementFilter ? query.elementFilter : []; - })); - if (!this.elementFilter.length) - this.elementFilter = undefined; - - this.calcReordered = this.options.queries.some(function (query) { - return query.all; - }); - - this.queryValidators = []; // TODO(rafaelw): Shouldn't always define this. - if (MutationSummary.createQueryValidator) { - this.queryValidators = this.options.queries.map(function (query) { - return MutationSummary.createQueryValidator(_this.root, query); - }); - } - - this.observer = new MutationObserverCtor(function (mutations) { - _this.observerCallback(mutations); - }); - - this.reconnect(); - } - MutationSummary.createObserverOptions = function (queries) { - var observerOptions = { - childList: true, - subtree: true - }; - - var attributeFilter; - function observeAttributes(attributes) { - if (observerOptions.attributes && !attributeFilter) - return; - - observerOptions.attributes = true; - observerOptions.attributeOldValue = true; - - if (!attributes) { - // observe all. - attributeFilter = undefined; - return; - } - - // add to observed. - attributeFilter = attributeFilter || {}; - attributes.forEach(function (attribute) { - attributeFilter[attribute] = true; - attributeFilter[attribute.toLowerCase()] = true; - }); - } - - queries.forEach(function (query) { - if (query.characterData) { - observerOptions.characterData = true; - observerOptions.characterDataOldValue = true; - return; - } - - if (query.all) { - observeAttributes(); - observerOptions.characterData = true; - observerOptions.characterDataOldValue = true; - return; - } - - if (query.attribute) { - observeAttributes([query.attribute.trim()]); - return; - } - - var attributes = elementFilterAttributes(query.elementFilter).concat(query.attributeList || []); - if (attributes.length) - observeAttributes(attributes); - }); - - if (attributeFilter) - observerOptions.attributeFilter = Object.keys(attributeFilter); - - return observerOptions; - }; - - MutationSummary.validateOptions = function (options) { - for (var prop in options) { - if (!(prop in MutationSummary.optionKeys)) - throw Error('Invalid option: ' + prop); - } - - if (typeof options.callback !== 'function') - throw Error('Invalid options: callback is required and must be a function'); - - if (!options.queries || !options.queries.length) - throw Error('Invalid options: queries must contain at least one query request object.'); - - var opts = { - callback: options.callback, - rootNode: options.rootNode || document, - observeOwnChanges: !!options.observeOwnChanges, - oldPreviousSibling: !!options.oldPreviousSibling, - queries: [] - }; - - for (var i = 0; i < options.queries.length; i++) { - var request = options.queries[i]; - - // all - if (request.all) { - if (Object.keys(request).length > 1) - throw Error('Invalid request option. all has no options.'); - - opts.queries.push({ all: true }); - continue; - } - - // attribute - if ('attribute' in request) { - var query = { - attribute: validateAttribute(request.attribute) - }; - - query.elementFilter = Selector.parseSelectors('*[' + query.attribute + ']'); - - if (Object.keys(request).length > 1) - throw Error('Invalid request option. attribute has no options.'); - - opts.queries.push(query); - continue; - } - - // element - if ('element' in request) { - var requestOptionCount = Object.keys(request).length; - var query = { - element: request.element, - elementFilter: Selector.parseSelectors(request.element) - }; - - if (request.hasOwnProperty('elementAttributes')) { - query.attributeList = validateElementAttributes(request.elementAttributes); - requestOptionCount--; - } - - if (requestOptionCount > 1) - throw Error('Invalid request option. element only allows elementAttributes option.'); - - opts.queries.push(query); - continue; - } - - // characterData - if (request.characterData) { - if (Object.keys(request).length > 1) - throw Error('Invalid request option. characterData has no options.'); - - opts.queries.push({ characterData: true }); - continue; - } - - throw Error('Invalid request option. Unknown query request.'); - } - - return opts; - }; - - MutationSummary.prototype.createSummaries = function (mutations) { - if (!mutations || !mutations.length) - return []; - - var projection = new MutationProjection(this.root, mutations, this.elementFilter, this.calcReordered, this.options.oldPreviousSibling); - - var summaries = []; - for (var i = 0; i < this.options.queries.length; i++) { - summaries.push(new Summary(projection, this.options.queries[i])); - } - - return summaries; - }; - - MutationSummary.prototype.checkpointQueryValidators = function () { - this.queryValidators.forEach(function (validator) { - if (validator) - validator.recordPreviousState(); - }); - }; - - MutationSummary.prototype.runQueryValidators = function (summaries) { - this.queryValidators.forEach(function (validator, index) { - if (validator) - validator.validate(summaries[index]); - }); - }; - - MutationSummary.prototype.changesToReport = function (summaries) { - return summaries.some(function (summary) { - var summaryProps = [ - 'added', 'removed', 'reordered', 'reparented', - 'valueChanged', 'characterDataChanged']; - if (summaryProps.some(function (prop) { - return summary[prop] && summary[prop].length; - })) - return true; - - if (summary.attributeChanged) { - var attrNames = Object.keys(summary.attributeChanged); - var attrsChanged = attrNames.some(function (attrName) { - return !!summary.attributeChanged[attrName].length; - }); - if (attrsChanged) - return true; - } - return false; - }); - }; - - MutationSummary.prototype.observerCallback = function (mutations) { - if (!this.options.observeOwnChanges) - this.observer.disconnect(); - - var summaries = this.createSummaries(mutations); - this.runQueryValidators(summaries); - - if (this.options.observeOwnChanges) - this.checkpointQueryValidators(); - - if (this.changesToReport(summaries)) - this.callback(summaries); - - // disconnect() may have been called during the callback. - if (!this.options.observeOwnChanges && this.connected) { - this.checkpointQueryValidators(); - this.observer.observe(this.root, this.observerOptions); - } - }; - - MutationSummary.prototype.reconnect = function () { - if (this.connected) - throw Error('Already connected'); - - this.observer.observe(this.root, this.observerOptions); - this.connected = true; - this.checkpointQueryValidators(); - }; - - MutationSummary.prototype.takeSummaries = function () { - if (!this.connected) - throw Error('Not connected'); - - var summaries = this.createSummaries(this.observer.takeRecords()); - return this.changesToReport(summaries) ? summaries : undefined; - }; - - MutationSummary.prototype.disconnect = function () { - var summaries = this.takeSummaries(); - this.observer.disconnect(); - this.connected = false; - return summaries; - }; - MutationSummary.NodeMap = NodeMap; - MutationSummary.parseElementFilter = Selector.parseSelectors; - - MutationSummary.optionKeys = { - 'callback': true, - 'queries': true, - 'rootNode': true, - 'oldPreviousSibling': true, - 'observeOwnChanges': true - }; - return MutationSummary; -})(); diff --git a/package-lock.json b/package-lock.json index 41de7d2..6cf9271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,8 @@ "devDependencies": { "chromedriver": "^95.0.0", "jasmine-browser-runner": "^0.9.0", - "jasmine-core": "^3.10.1" + "jasmine-core": "^3.10.1", + "json5": "^2.2.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -906,6 +907,21 @@ "integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==", "dev": true }, + "node_modules/json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jszip": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", @@ -1018,6 +1034,12 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -2239,6 +2261,15 @@ "integrity": "sha512-ooZWSDVAdh79Rrj4/nnfklL3NQVra0BcuhcuWoAwwi+znLDoUeH87AFfeX8s+YeYi6xlv5nveRyaA1v7CintfA==", "dev": true }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, "jszip": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz", @@ -2324,6 +2355,12 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "dev": true + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/package.json b/package.json index f86855d..484888f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "devDependencies": { "chromedriver": "^95.0.0", "jasmine-browser-runner": "^0.9.0", - "jasmine-core": "^3.10.1" + "jasmine-core": "^3.10.1", + "json5": "^2.2.0" }, "scripts": { "test": "jasmine-browser-runner runSpecs" diff --git a/popup.js b/popup.js index 43eb147..cba93ac 100644 --- a/popup.js +++ b/popup.js @@ -19,12 +19,12 @@ // When the user clicks the extension icon, open the EME log window. document.addEventListener('DOMContentLoaded', () => { const backgroundPage = chrome.extension.getBackgroundPage(); - const emeLogConstructor = backgroundPage.emeLogConstructor; + const emeLogWindow = backgroundPage.EmeLogWindow.instance; // Without a small delay, it opens behind the current window. Adding the // delay makes it a proper popup that takes focus. setTimeout(() => { - emeLogConstructor.openWindow(); + emeLogWindow.open(); window.close(); }, 100); }); diff --git a/prototypes.js b/prototypes.js deleted file mode 100644 index 9f2a0cd..0000000 --- a/prototypes.js +++ /dev/null @@ -1,717 +0,0 @@ -/** - * Copyright 2015 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * @fileoverview Defines prototypes for EME method calls and events. - */ - -var emeLogger = {}; - - -/** - * @typedef {{ - * title: string, - * timestamp: string, - * names: !Array., - * values: !Array.<*> - * }} - */ -emeLogger.LogItemData; - - -/** - * Method call prototype. - * @param {string} title The title used to describe this call. - * @param {!Object} target The element this method was called on. - * @param {Object} result The object returned from this call, if any. - * @constructor - */ -emeLogger.EmeMethodCall = function(title, target, result) { - this.timestamp = emeLogger.timeToString_(new Date().getTime()); - this.title = title; - this.target = target; - this.returned = result; -}; - - -/** - * EME Event prototype. - * @param {string} title The title used to describe this event. - * @param {Event} e An EME event. - * @param {Object} target The element this event was fired on. - * @constructor - */ -emeLogger.EmeEvent = function(title, e, target) { - this.title = title; - this.event = e; - this.timestamp = emeLogger.timeToString_(e.timeStamp); - this.target = target; -}; - - -/** - * Constructs a time string in the format hh:mm:ss:mil. - * @param {number} time The time in milliseconds. - * @return {string} A human readable string respresenting the time. - * @private - */ -emeLogger.timeToString_ = function(time) { - var date = new Date(time); - var hours = emeLogger.padNumber_(date.getHours().toString()); - var minutes = emeLogger.padNumber_(date.getMinutes().toString()); - var seconds = emeLogger.padNumber_(date.getSeconds().toString()); - var milli = emeLogger.padNumber_(date.getMilliseconds().toString()); - return hours.concat(':', minutes, ':', seconds, ':', milli); -}; - - -/** - * Pads a number with leading zeros. - * @param {string} num A string representing the number. - * @param {number} length The desired length of the string. - * @return {string} A string padded with zeros to the desired length. - * @private - */ -emeLogger.padNumber_ = function(num, length) { - while (num.length < length) { - num = '0' + num; - } - return num; -}; - - -/** - * Gets a formatted message from the EME Formatters. - * @param {string} name The name of the Event or Call being logged. - * @param {Object} data The EME data to be parsed from the Event/Call. - * @param {string} keySystem The keySystem used for this Event/Call. - * @return {string|undefined} The formatted message. - */ -emeLogger.getFormattedMessage = function(name, data, keySystem) { - if (!document.emeFormatters) { - return; - } - - var formattedMessage = ''; - for (var i = 0; i < document.emeFormatters.length; i++) { - var formatter = document.emeFormatters[i]; - var formatFunctionName = 'format' + name; - if (!formatter[formatFunctionName]) { - continue; - } - // Only use formatters that support the |keySystem|, if specified. - // (|keySystem| is not specified for some events.) - if (keySystem && !formatter.isKeySystemSupported(keySystem)) { - continue; - } - try { - formattedMessage += formatter[formatFunctionName](data); - if (i > 0) { - formattedMessage += '\n'; - } - } catch (e) { - console.warn('Formatter', formatter, 'failed:', e); - } - } - - if (formattedMessage == '') { - return; - } - - return formattedMessage; -}; - - -/** - * PromiseResult contains the information resulting from a - * resolved/rejected Promise. - * @param {string} title The title used to describe this Promise. - * @param {string} status Status of the Promise. - * @param {Object} result The result of the Promise. - * @param {Array} args The arguments that were passed. - * @constructor - */ -emeLogger.PromiseResult = function(title, status, result, args) { - this.timestamp = emeLogger.timeToString_(new Date().getTime()); - this.title = title; - this.status = status; - this.args = args; - if (result) { - this.result = result.constructor.name == 'MediaKeySystemAccess' ? - new emeLogger.MediaKeySystemAccess(result) : result; - } -}; - - -/** - * Provides a simple representation of obj to be used for messaging. The - * names and values returned in emeLogger.LogItemData will only reflect the - * object's direct properties. - * @param {Object} obj An object to format into emeLogger.LogItemData. - * @return {!emeLogger.LogItemData} A formatted object. - */ -emeLogger.getMessagePassableObject = function(obj) { - var names = []; - var values = []; - for (var prop in obj) { - if (prop == 'title' || prop == 'timestamp') continue; - // We only care about direct properties of the object. Calling - // hasOwnProperty will stop from checking down the object's prototype chain. - if (obj.hasOwnProperty(prop)) { - if (typeof(obj[prop]) == 'function') continue; - names.push(prop); - if (typeof(obj[prop]) == 'object' && obj[prop] != null) { - // Give ArrayBuffers a view so they can be properly logged. - var value = obj[prop].constructor.name == 'ArrayBuffer' ? - new Uint8Array(obj[prop]) : obj[prop]; - values.push(emeLogger.getMessagePassableObject(value)); - } else { - values.push(obj[prop]); - } - } - } - var data = { - title: obj.title || obj.constructor.name, - names: names, - values: values - }; - if (obj.timestamp) { - data.timestamp = obj.timestamp; - } - return data; -}; - - -/** Typed Object Prototypes **/ - -/** - * MediaKeySystemAccess object prototype. Calls getConfiguration to include - * the objects configuration. - * @param {!MediaKeySystemAccess} mksa The MediaKeySystemAccess object - * @constructor - */ -emeLogger.MediaKeySystemAccess = function(mksa) { - this.title = 'MediaKeySystemAccess'; - this.keySystem = mksa.keySystem; - this.configuration = mksa.listenersAdded_ ? - mksa.originalGetConfiguration() : mksa.getConfiguration(); -}; - - -/** - * MediaKeySession object prototype. - * @param {!MediaKeySession} mks The MediaKeySession object. - * @constructor - */ -emeLogger.MediaKeySession = function(mks) { - this.title = 'MediaKeySession'; - this.sessionId = mks.sessionId; - this.expiration = mks.expiration; - this.keyStatuses = []; - mks.keyStatuses.forEach(function(status, keyId) { - this.keyStatuses.push({'keyId' : new Uint8Array(keyId), 'status' : status}); - }.bind(this)); -}; - - -/** - * HTMLMediaElement object prototype. - * @param {!HTMLMediaElement} element The HTMLMediaElement. - * @constructor - */ -emeLogger.HTMLMediaElement = function(element) { - this.title = element.constructor.name; - this.id = element.id; - if (element.classList) { - this.classes = element.classList.toString(); - } -}; - -/** Typed Event Prototypes **/ - -/** - * Message event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.MessageEvent = function(e) { - var mks = new emeLogger.MediaKeySession(e.target); - emeLogger.EmeEvent.apply(this, ['MessageEvent', e, mks]); - // If a formatter extension is available, message will be formatted. - this.message = new Uint8Array(e.message); - this.messageType = e.messageType; - this.formattedMessage = emeLogger.getFormattedMessage( - 'message', e.message, e.keySystem); -}; -emeLogger.MessageEvent.prototype = Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.MessageEvent.prototype.constructor = emeLogger.MessageEvent; - - -/** - * KeyStatusesChange event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.KeyStatusesChangeEvent = function(e) { - var mks = new emeLogger.MediaKeySession(e.target); - emeLogger.EmeEvent.apply(this, ['KeyStatusesChangeEvent', e, mks]); -}; -emeLogger.KeyStatusesChangeEvent.prototype = - Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.KeyStatusesChangeEvent.prototype.constructor = - emeLogger.KeyStatusesChangeEvent; - - -/** - * NeedKey event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.NeedKeyEvent = function(e) { - var element = new emeLogger.HTMLMediaElement(e.target); - emeLogger.EmeEvent.apply(this, ['NeedKeyEvent', e, element]); -}; -emeLogger.NeedKeyEvent.prototype = Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.NeedKeyEvent.prototype.constructor = emeLogger.NeedKeyEvent; - - -/** - * KeyMessage event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.KeyMessageEvent = function(e) { - var element = new emeLogger.HTMLMediaElement(e.target); - emeLogger.EmeEvent.apply(this, ['KeyMessageEvent', e, element]); - // If a formatter extension is available, message will be formatted. - this.formattedMessage = emeLogger.getFormattedMessage( - 'webkitkeymessage', e.message, e.keySystem); -}; -emeLogger.KeyMessageEvent.prototype = - Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.KeyMessageEvent.prototype.constructor = emeLogger.KeyMessageEvent; - - -/** - * KeyAdded event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.KeyAddedEvent = function(e) { - var element = new emeLogger.HTMLMediaElement(e.target); - emeLogger.EmeEvent.apply(this, ['KeyAddedEvent', e, element]); -}; -emeLogger.KeyAddedEvent.prototype = Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.KeyAddedEvent.prototype.constructor = emeLogger.KeyAddedEvent; - - -/** - * KeyError event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.KeyErrorEvent = function(e) { - var element = new emeLogger.HTMLMediaElement(e.target); - emeLogger.EmeEvent.apply(this, ['KeyErrorEvent', e, element]); -}; -emeLogger.KeyErrorEvent.prototype = - Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.KeyErrorEvent.prototype.constructor = emeLogger.KeyErrorEvent; - - -/** - * Encrypted event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.EncryptedEvent = function(e) { - var element = new emeLogger.HTMLMediaElement(e.target); - emeLogger.EmeEvent.apply(this, ['EncryptedEvent', e, element]); - this.initData = new Uint8Array(e.initData); - this.initDataType = e.initDataType; -}; -emeLogger.EncryptedEvent.prototype = - Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.EncryptedEvent.prototype.constructor = emeLogger.EncryptedEvent; - - -/** - * Play event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.PlayEvent = function(e) { - var element = new emeLogger.HTMLMediaElement(e.target); - emeLogger.EmeEvent.apply(this, ['PlayEvent', e, element]); -}; -emeLogger.PlayEvent.prototype = Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.PlayEvent.prototype.constructor = emeLogger.PlayEvent; - - -/** - * Error event prototype. - * @param {Event} e - * @extends {emeLogger.EmeEvent} - * @constructor - */ -emeLogger.ErrorEvent = function(e) { - var element = new emeLogger.HTMLMediaElement(e.target); - emeLogger.EmeEvent.apply(this, ['ErrorEvent', e, element]); - this.errorType = e.target.error; - this.errorCode = this.errorType.code; -}; -emeLogger.ErrorEvent.prototype = Object.create(emeLogger.EmeEvent.prototype); -/** @constructor */ -emeLogger.ErrorEvent.prototype.constructor = emeLogger.ErrorEvent; - - -/** Typed Method Call Prototypes **/ - -/** - * RequestMediaKeySystemAccess call prototype. - * @param {Array} args - * @param {Navigator} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.RequestMediaKeySystemAccessCall = function(args, target, result) { - emeLogger.EmeMethodCall.apply( - this, ['RequestMediaKeySystemAccessCall', target, result]); - this.keySystem = args[0]; - this.supportedConfigurations = args[1]; -}; -emeLogger.RequestMediaKeySystemAccessCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.RequestMediaKeySystemAccessCall.prototype.constructor = - emeLogger.RequestMediaKeySystemAccessCall; - - -/** - * GetConfiguration call prototype. - * @param {Array} args - * @param {MediaKeySystemAccess} target - * @param {MediaKeySystemConfiguration} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.GetConfigurationCall = function(args, target, result) { - var mksa = new emeLogger.MediaKeySystemAccess(target); - emeLogger.EmeMethodCall.apply(this, ['GetConfigurationCall', mksa, result]); -}; -emeLogger.GetConfigurationCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.GetConfigurationCall.prototype.constructor = - emeLogger.GetConfigurationCall; - - -/** - * CreateMediaKeys call prototype. - * @param {Array} args - * @param {MediaKeySystemAccess} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.CreateMediaKeysCall = function(args, target, result) { - var mksa = new emeLogger.MediaKeySystemAccess(target); - emeLogger.EmeMethodCall.apply(this, ['CreateMediaKeysCall', mksa, result]); -}; -emeLogger.CreateMediaKeysCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.CreateMediaKeysCall.prototype.constructor = - emeLogger.CreateMediaKeysCall; - - -/** - * CreateSession call prototype. - * @param {Array} args - * @param {MediaKeys} target - * @param {MediaKeySession} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.CreateSessionCall = function(args, target, result) { - var mks = new emeLogger.MediaKeySession(result); - emeLogger.EmeMethodCall.apply(this, ['CreateSessionCall', target, mks]); - // Temporary is the default session type if none is provided. - this.sessionType = args[0] ? args[0] : 'temporary'; -}; -emeLogger.CreateSessionCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.CreateSessionCall.prototype.constructor = emeLogger.CreateSessionCall; - - -/** - * SetServerCertificate call prototype. - * @param {Array} args - * @param {MediaKeys} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.SetServerCertificateCall = function(args, target, result) { - emeLogger.EmeMethodCall.apply( - this, ['SetServerCertificateCall', target, result]); - this.serverCertificate = new Uint8Array(args[0]); -}; -emeLogger.SetServerCertificateCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.SetServerCertificateCall.prototype.constructor = - emeLogger.SetServerCertificateCall; - - -/** - * GenerateRequest call prototype. - * @param {Array} args - * @param {MediaKeySession} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.GenerateRequestCall = function(args, target, result) { - var mks = new emeLogger.MediaKeySession(target); - emeLogger.EmeMethodCall.apply(this, ['GenerateRequestCall', mks, result]); - this.initDataType = args[0]; - this.initData = new Uint8Array(args[1]); -}; -emeLogger.GenerateRequestCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.GenerateRequestCall.prototype.constructor = - emeLogger.GenerateRequestCall; - - -/** - * Play call prototype. - * @param {Array} args - * @param {HTMLMediaElement} target - * @param {Object} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.PlayCall = function(args, target, result) { - var element = new emeLogger.HTMLMediaElement(target); - emeLogger.EmeMethodCall.apply(this, ['PlayCall', element, result]); -}; -emeLogger.PlayCall.prototype = Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.PlayCall.prototype.constructor = emeLogger.PlayCall; - - -/** - * Load call prototype. - * @param {Array} args - * @param {MediaKeySession} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.LoadCall = function(args, target, result) { - var mks = new emeLogger.MediaKeySession(target); - emeLogger.EmeMethodCall.apply(this, ['LoadCall', mks, result]); - this.sessionId = args[0]; -}; -emeLogger.LoadCall.prototype = Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.LoadCall.prototype.constructor = emeLogger.LoadCall; - - -/** - * Update call prototype. - * @param {Array} args - * @param {MediaKeySession} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.UpdateCall = function(args, target, result) { - var mks = new emeLogger.MediaKeySession(target); - emeLogger.EmeMethodCall.apply(this, ['UpdateCall', mks, result]); - this.response = new Uint8Array(args[0]); - // If a formatter extension is available, response will be formatted. - this.formattedMessage = emeLogger.getFormattedMessage( - 'UpdateCall', this.response, target.keySystem_); -}; -emeLogger.UpdateCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.UpdateCall.prototype.constructor = emeLogger.UpdateCall; - - -/** - * Close call prototype. - * @param {Array} args - * @param {MediaKeySession} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.CloseCall = function(args, target, result) { - var mks = new emeLogger.MediaKeySession(target); - emeLogger.EmeMethodCall.apply(this, ['CloseCall', mks, result]); -}; -emeLogger.CloseCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.CloseCall.prototype.constructor = emeLogger.CloseCall; - - -/** - * Remove call prototype. - * @param {Array} args - * @param {MediaKeySession} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.RemoveCall = function(args, target, result) { - var mks = new emeLogger.MediaKeySession(target); - emeLogger.EmeMethodCall.apply(this, ['RemoveCall', mks, result]); -}; -emeLogger.RemoveCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.RemoveCall.prototype.constructor = emeLogger.RemoveCall; - - -/** - * CanPlayType call prototype. - * @param {Array} args - * @param {HTMLMediaElement} target - * @param {DOMString} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.CanPlayTypeCall = function(args, target, result) { - var element = new emeLogger.HTMLMediaElement(target); - emeLogger.EmeMethodCall.apply(this, ['CanPlayTypeCall', element, result]); - this.type = args[0]; - this.keySystem = args[1]; -}; -emeLogger.CanPlayTypeCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.CanPlayTypeCall.prototype.constructor = emeLogger.CanPlayTypeCall; - - -/** - * GenerateKeyRequest call prototype. - * @param {Array} args - * @param {HTMLMediaElement} target - * @param {Object} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.GenerateKeyRequestCall = function(args, target, result) { - var element = new emeLogger.HTMLMediaElement(target); - emeLogger.EmeMethodCall.apply( - this, ['GenerateKeyRequestCall', element, result]); - this.keySystem = args[0]; - this.initData = args[1]; -}; -emeLogger.GenerateKeyRequestCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.GenerateKeyRequestCall.prototype.constructor = - emeLogger.GenerateKeyRequestCall; - - -/** - * AddKey call prototype. - * @param {Array} args - * @param {HTMLMediaElement} target - * @param {Object} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.AddKeyCall = function(args, target, result) { - var element = new emeLogger.HTMLMediaElement(target); - emeLogger.EmeMethodCall.apply(this, ['AddKeyCall', element, result]); - this.keySystem = args[0]; - this.key = args[1]; - this.initData = args[2]; - this.sessionId = args[3]; - // If a formatter extension is available, key will be formatted. - this.formattedMessage = emeLogger.getFormattedMessage( - 'AddKeyCall', this.key, this.keySystem); -}; -emeLogger.AddKeyCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.AddKeyCall.prototype.constructor = emeLogger.AddKeyCall; - - -/** - * CancelKeyRequest call prototype. - * @param {Array} args - * @param {HTMLMediaElement} target - * @param {Object} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.CancelKeyRequestCall = function(args, target, result) { - var element = new emeLogger.HTMLMediaElement(target); - emeLogger.EmeMethodCall.apply( - this, ['CancelKeyRequestCall', element, result]); - this.keySystem = args[0]; - this.sessionId = args[1]; -}; -emeLogger.CancelKeyRequestCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.CancelKeyRequestCall.prototype.constructor = - emeLogger.CancelKeyRequestCall; - - -/** - * SetMediaKeys call prototype. - * @param {Array} args - * @param {HTMLMediaElement} target - * @param {Promise} result - * @extends {emeLogger.EmeMethodCall} - * @constructor - */ -emeLogger.SetMediaKeysCall = function(args, target, result) { - var element = new emeLogger.HTMLMediaElement(target); - emeLogger.EmeMethodCall.apply(this, ['SetMediaKeysCall', element, result]); - this.mediaKeys = args[0]; -}; -emeLogger.SetMediaKeysCall.prototype = - Object.create(emeLogger.EmeMethodCall.prototype); -/** @constructor */ -emeLogger.SetMediaKeysCall.prototype.constructor = emeLogger.SetMediaKeysCall; - - diff --git a/spec/eme_trace_tests.js b/spec/eme_trace_tests.js index f001f32..4d46bd6 100644 --- a/spec/eme_trace_tests.js +++ b/spec/eme_trace_tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2015 Google Inc. + * Copyright 2021 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,13 @@ describe('EME tracing', () => { beforeEach(() => { - spyOn(EmeListeners, 'logAndPostMessage_').and.callFake((log) => { + spyOn(window, 'emeLogger').and.callFake((log) => { // Validate that the logs can always be serialized. We don't care about // the output at this level. + delete log.instance; + try { - JSON.stringify(log); + JSON.stringify(prepLogForMessage(log)); } catch (exception) { fail(exception); } @@ -56,15 +58,13 @@ describe('EME tracing', () => { const mksa = await navigator.requestMediaKeySystemAccess( keySystem, minimalConfigs); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'RequestMediaKeySystemAccessCall', - // NOTE: This is a bug. The target is not wrapped in this case. - // But since I'm about to replace the internals, let's ignore it for - // now. - 'target': navigator, - 'keySystem': keySystem, - 'supportedConfigurations': minimalConfigs, + 'type': TraceAnything.LogTypes.Method, + 'className': 'Navigator', + 'methodName': 'requestMediaKeySystemAccess', + 'args': [keySystem, minimalConfigs], + 'result': mksa, })); }); }); @@ -75,37 +75,31 @@ describe('EME tracing', () => { beforeEach(async () => { mksa = await navigator.requestMediaKeySystemAccess( keySystem, minimalConfigs); - EmeListeners.logAndPostMessage_.calls.reset(); + emeLogger.calls.reset(); }); it('getConfiguration calls', async () => { const config = mksa.getConfiguration(); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'GetConfigurationCall', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySystemAccess', - }), - 'returned': config, + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeySystemAccess', + 'methodName': 'getConfiguration', + 'args': [], + 'result': config, })); }); it('createMediaKeys calls', async () => { const mediaKeys = await mksa.createMediaKeys(); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'CreateMediaKeysCall', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySystemAccess', - }), - 'returned': jasmine.any(Promise), - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'CreateMediaKeysCall Promise Result', - 'status': 'resolved', + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeySystemAccess', + 'methodName': 'createMediaKeys', + 'args': [], 'result': mediaKeys, })); }); @@ -118,23 +112,19 @@ describe('EME tracing', () => { const mksa = await navigator.requestMediaKeySystemAccess( keySystem, minimalConfigs); mediaKeys = await mksa.createMediaKeys(); - EmeListeners.logAndPostMessage_.calls.reset(); + emeLogger.calls.reset(); }); it('createSession calls', () => { const session = mediaKeys.createSession('temporary'); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'CreateSessionCall', - // NOTE: This is a bug. The target is not wrapped in this case. - // But since I'm about to replace the internals, let's ignore it for - // now. - 'target': mediaKeys, - 'sessionType': 'temporary', - 'returned': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeys', + 'methodName': 'createSession', + 'args': ['temporary'], + 'result': session, })); }); @@ -148,19 +138,13 @@ describe('EME tracing', () => { const serverCertificate = await response.arrayBuffer(); await mediaKeys.setServerCertificate(serverCertificate); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'SetServerCertificateCall', - // NOTE: This is a bug. The target is not wrapped in this case. - // But since I'm about to replace the internals, let's ignore it for - // now. - 'target': mediaKeys, - 'serverCertificate': new Uint8Array(serverCertificate), - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'SetServerCertificateCall Promise Result', - 'status': 'resolved', + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeys', + 'methodName': 'setServerCertificate', + 'args': [serverCertificate], + 'result': true, })); }); }); @@ -173,25 +157,19 @@ describe('EME tracing', () => { keySystem, minimalConfigs); const mediaKeys = await mksa.createMediaKeys(); session = mediaKeys.createSession('temporary'); - EmeListeners.logAndPostMessage_.calls.reset(); + emeLogger.calls.reset(); }); it('generateRequest calls', async () => { await session.generateRequest('cenc', initData); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'GenerateRequestCall', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), - 'initDataType': 'cenc', - 'initData': initData, - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'GenerateRequestCall Promise Result', - 'status': 'resolved', + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeySession', + 'methodName': 'generateRequest', + 'args': ['cenc', initData], + 'result': undefined, })); }); @@ -200,17 +178,13 @@ describe('EME tracing', () => { await session.load('fakeSessionId'); } catch (exception) {} // Will fail with a fake session ID; ignore it - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'LoadCall', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'LoadCall Promise Result', - 'status': 'rejected', + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeySession', + 'methodName': 'load', + 'args': ['fakeSessionId'], + 'threw': jasmine.any(Error), })); }); @@ -220,18 +194,13 @@ describe('EME tracing', () => { await session.update(fakeLicenseResponse); } catch (exception) {} // Will fail with fake data; ignore it - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'UpdateCall', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), - 'response': fakeLicenseResponse, - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'UpdateCall Promise Result', - 'status': 'rejected', + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeySession', + 'methodName': 'update', + 'args': [fakeLicenseResponse], + 'threw': jasmine.any(Error), })); }); @@ -240,17 +209,13 @@ describe('EME tracing', () => { await session.close(); } catch (exception) {} // Will fail due to invalid state; ignore it - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'CloseCall', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'CloseCall Promise Result', - 'status': 'rejected', + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeySession', + 'methodName': 'close', + 'args': [], + 'threw': jasmine.any(Error), })); }); @@ -259,17 +224,13 @@ describe('EME tracing', () => { await session.remove(); } catch (exception) {} // Will fail due to invalid state; ignore it - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'RemoveCall', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'RemoveCall Promise Result', - 'status': 'rejected', + 'type': TraceAnything.LogTypes.Method, + 'className': 'MediaKeySession', + 'methodName': 'remove', + 'args': [], + 'threw': jasmine.any(Error), })); }); @@ -277,21 +238,18 @@ describe('EME tracing', () => { session.dispatchEvent(new Event('message')); session.dispatchEvent(new Event('keystatuseschange')); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'MessageEvent', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), - 'event': jasmine.any(Event), + 'type': TraceAnything.LogTypes.Event, + 'className': 'MediaKeySession', + 'eventName': 'message', })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'KeyStatusesChangeEvent', - 'target': jasmine.objectContaining({ - 'title': 'MediaKeySession', - }), - 'event': jasmine.any(Event), + 'type': TraceAnything.LogTypes.Event, + 'className': 'MediaKeySession', + 'eventName': 'keystatuseschange', + 'value': session.keyStatuses, })); }); }); @@ -299,19 +257,22 @@ describe('EME tracing', () => { describe('logs HTML media elements', () => { var mediaElement; - beforeAll(() => { + beforeAll(async () => { // Make a real video element to log access to. mediaElement = document.createElement('video'); // Set a tiny mp4 data URI as a source, so we can hit play. mediaElement.src = tinyMp4; - // Mute it and hide it visually. + // Mute it. mediaElement.muted = true; - mediaElement.style.display = 'none'; // The element must be in the DOM to be discovered. document.body.appendChild(mediaElement); + + // FIXME: Discovery is not working in this environment. Why? Is it still + // working in a normal page? + TraceAnything.scanDocumentForNewElements(); }); afterAll(() => { @@ -324,35 +285,25 @@ describe('EME tracing', () => { const mediaKeys = await mksa.createMediaKeys(); await mediaElement.setMediaKeys(mediaKeys); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'SetMediaKeysCall', - 'target': jasmine.objectContaining({ - 'title': 'HTMLVideoElement', - }), - 'mediaKeys': mediaKeys, - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'SetMediaKeysCall Promise Result', - 'status': 'resolved', + 'type': TraceAnything.LogTypes.Method, + 'className': 'HTMLVideoElement', + 'methodName': 'setMediaKeys', + 'args': [mediaKeys], + 'result': undefined, })); }); it('play calls', async () => { await mediaElement.play(); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( - jasmine.objectContaining({ - 'title': 'PlayCall', - 'target': jasmine.objectContaining({ - 'title': 'HTMLVideoElement', - }), - })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'PlayCall Promise Result', - 'status': 'resolved', + 'type': TraceAnything.LogTypes.Method, + 'className': 'HTMLVideoElement', + 'methodName': 'play', + 'args': [], })); }); @@ -364,31 +315,33 @@ describe('EME tracing', () => { Object.defineProperty(mediaElement, 'error', {value: {code: 5}}); mediaElement.dispatchEvent(new Event('error')); - mediaElement.dispatchEvent(new Event('encrypted')); + const encryptedEvent = new Event('encrypted'); + encryptedEvent.initDataType = 'webm'; + encryptedEvent.initData = new Uint8Array([1, 2, 3]); + mediaElement.dispatchEvent(encryptedEvent); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'PlayEvent', - 'target': jasmine.objectContaining({ - 'title': 'HTMLVideoElement', - }), - 'event': jasmine.any(Event), + 'type': TraceAnything.LogTypes.Event, + 'className': 'HTMLVideoElement', + 'eventName': 'play', })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'ErrorEvent', - 'target': jasmine.objectContaining({ - 'title': 'HTMLVideoElement', - }), - 'event': jasmine.any(Event), + 'type': TraceAnything.LogTypes.Event, + 'className': 'HTMLVideoElement', + 'eventName': 'error', + 'value': {code: 5}, })); - expect(EmeListeners.logAndPostMessage_).toHaveBeenCalledWith( + expect(emeLogger).toHaveBeenCalledWith( jasmine.objectContaining({ - 'title': 'EncryptedEvent', - 'target': jasmine.objectContaining({ - 'title': 'HTMLVideoElement', + 'type': TraceAnything.LogTypes.Event, + 'className': 'HTMLVideoElement', + 'eventName': 'encrypted', + 'event': jasmine.objectContaining({ + 'initDataType': 'webm', + 'initData': new Uint8Array([1, 2, 3]), }), - 'event': jasmine.any(Event), })); }); }); diff --git a/spec/log_window_tests.js b/spec/log_window_tests.js index c86611f..7f48cf0 100644 --- a/spec/log_window_tests.js +++ b/spec/log_window_tests.js @@ -16,17 +16,13 @@ * @fileoverview Tests for the log window. */ -describe('Log window', function() { +describe('Log window', () => { + let mockDocument; let mockWindow; let mockLogElement; beforeAll(() => { - const mockDocument = document.createElement('div'); - - const mockDownloadButton = document.createElement('a'); - mockDownloadButton.id = 'download-button'; - mockDocument.appendChild(mockDownloadButton); - + mockDocument = document.createElement('div'); document.body.appendChild(mockDocument); mockWindow = { @@ -37,114 +33,240 @@ describe('Log window', function() { }); beforeEach(() => { + // Reset the singleton we're testing. + EmeLogWindow.instance = new EmeLogWindow(); + // Return the mock window when we are supposed to open one. - mockWindow.closed = false; spyOn(window, 'open').and.returnValue(mockWindow); - }); - it('opens the logging window', function() { - emeLogConstructor.openWindow(); - expect(window.open).toHaveBeenCalledWith( - 'log.html', jasmine.any(String), jasmine.any(String)); - }); + // Clear the contents of the document. + while (mockDocument.firstChild) { + mockDocument.firstChild.remove(); + } - it('reports the logging window is open', function() { - emeLogConstructor.openWindow(); - expect(emeLogConstructor.isWindowOpen()).toBe(true); + // Add the element where log items will go. + mockLogElement = document.createElement('ul'); + mockLogElement.id = 'eme-log'; + mockDocument.appendChild(mockLogElement); }); - it('reports the logging window is closed', function() { - mockWindow.closed = true; - emeLogConstructor.openWindow(); - expect(emeLogConstructor.isWindowOpen()).toBe(false); - }); + describe('Window handling', () => { + it('opens the logging window', () => { + EmeLogWindow.instance.open(); + expect(window.open).toHaveBeenCalledWith( + 'log.html', jasmine.any(String), jasmine.any(String)); + }); - it('builds a formatted log item', function() { - var data = { - title: 'Test Data', - names: ['Name 1'], - values: ['Value 1'] - }; - var expectedString = ' Name 1: Value 1'; - var formattedItem = emeLogConstructor.buildFormattedLogItem(data); - expect(formattedItem.title).toEqual(data.title); - expect(formattedItem.logData).toEqual(expectedString); - }); + it('reports the logging window is open', () => { + mockWindow.closed = false; + EmeLogWindow.instance.open(); + expect(EmeLogWindow.instance.isOpen()).toBe(true); + }); - it('builds data pairs', function() { - var data = { - title: 'Test Data', - names: ['Name 1', 'Name 2'], - values: ['Value 1', 'Value 2'] - }; - var expectedText = ' Name 1: Value 1\n' + - ' Name 2: Value 2'; - expect(emeLogConstructor.buildDataPairs(data, 0)).toEqual(expectedText); + it('reports the logging window is closed', () => { + mockWindow.closed = true; + EmeLogWindow.instance.open(); + expect(EmeLogWindow.instance.isOpen()).toBe(false); + }); }); - it('builds a formatted string from a undefined value', function() { - var result = emeLogConstructor.buildObjectItem(undefined, 0); - expect(result).toEqual('undefined'); + it('logs with timestamps', () => { + const date = new Date('July 20, 1969 12:34:56 UTC'); + EmeLogWindow.instance.open(); + EmeLogWindow.instance.appendLog({ + timestamp: date.getTime(), + }); + expect(mockLogElement.querySelector('h3').textContent) + .toContain('1969-07-20 12:34:56'); }); - it('builds a formatted string from a number', function() { - var result = emeLogConstructor.buildObjectItem(12345, 0); - expect(result).toEqual('12345'); - }); + it('shows warnings', () => { + EmeLogWindow.instance.open(); + EmeLogWindow.instance.appendLog({ + timestamp: Date.now(), + type: TraceAnything.LogTypes.Warning, + message: 'Oh no!', + }); - it('builds a formatted string from a boolean', function() { - var result = emeLogConstructor.buildObjectItem(true, 0); - expect(result).toEqual('true'); + expect(mockLogElement.querySelector('.title').textContent) + .toContain('WARNING'); + expect(mockLogElement.querySelector('.title').style.color) + .toBe('red'); + expect(mockLogElement.querySelector('.data').textContent) + .toContain('Oh no!'); }); - it('builds a formatted string from null', function() { - var result = emeLogConstructor.buildObjectItem(null, 0); - expect(result).toEqual('null'); - }); + describe('sets an appropriate title', () => { + beforeEach(() => { + EmeLogWindow.instance.open(); + }); - it('builds a formatted string from an array', function() { - var data = { - title: 'Array', - names: ['0', '1', '2'], - values: ['Value 0', 'Value 1', 'Value 2'] - }; - var expectedText = '[Value 0, Value 1, Value 2]'; - var result = emeLogConstructor.buildObjectItem(data, 0); - expect(result).toEqual(expectedText); - }); + it('for constructors', () => { + EmeLogWindow.instance.appendLog({ + timestamp: Date.now(), + type: TraceAnything.LogTypes.Constructor, + className: 'SomeClass', + args: [], + }); + expect(mockLogElement.querySelector('.title').textContent) + .toContain('new SomeClass'); + }); - it('builds a formatted string from a Uint8Array', function() { - var data = { - title: 'Uint8Array', - names: ['0', '1', '2'], - values: [12, 34, 12, 65, 34, 634, 78, 324, 54, 23, 53] - }; - var expectedText = '\n 12,34,12,65,34,634,78,324,54,23,53'; - var result = emeLogConstructor.buildObjectItem(data, 0); - expect(result).toEqual(expectedText); - }); + it('for methods', () => { + EmeLogWindow.instance.appendLog({ + timestamp: Date.now(), + type: TraceAnything.LogTypes.Method, + className: 'SomeClass', + methodName: 'someMethod', + args: [], + }); + expect(mockLogElement.querySelector('.title').textContent) + .toContain('SomeClass.someMethod'); + }); - it('builds a formatted string from an Object', function() { - var data = { - title: 'Object', - names: ['persistantStateRequired'], - values: ['true'] - }; - var expectedText = '\n persistantStateRequired: true'; - var result = emeLogConstructor.buildObjectItem(data, 0); - expect(result).toEqual(expectedText); + it('for getters', () => { + EmeLogWindow.instance.appendLog({ + timestamp: Date.now(), + type: TraceAnything.LogTypes.Getter, + className: 'SomeClass', + memberName: 'someMember', + }); + expect(mockLogElement.querySelector('.title').textContent) + .toContain('SomeClass.someMember'); + }); + + it('for setters', () => { + EmeLogWindow.instance.appendLog({ + timestamp: Date.now(), + type: TraceAnything.LogTypes.Setter, + className: 'SomeClass', + memberName: 'someMember', + }); + expect(mockLogElement.querySelector('.title').textContent) + .toContain('SomeClass.someMember'); + }); + + it('for events', () => { + EmeLogWindow.instance.appendLog({ + timestamp: Date.now(), + type: TraceAnything.LogTypes.Event, + className: 'SomeClass', + eventName: 'someevent', + }); + expect(mockLogElement.querySelector('.title').textContent) + .toContain('SomeClass someevent Event'); + }); }); - it('builds a formatted string from an Object with a type', function() { - var data = { - title: 'MediaKey', - names: ['keySystem'], - values: ['test.com'] - }; - var expectedText = '\n MediaKey {\n' + - ' keySystem: test.com\n' + - ' }'; - var result = emeLogConstructor.buildObjectItem(data, 0); - expect(result).toEqual(expectedText); + describe('value formatting', () => { + beforeEach(() => { + EmeLogWindow.instance.open(); + }); + + function logResult(result) { + EmeLogWindow.instance.appendLog({ + timestamp: Date.now(), + type: TraceAnything.LogTypes.Getter, + className: 'SomeClass', + memberName: 'someMember', + result, + }); + } + + // This matches the format used in function emeLogger() in + // eme-trace-config.js for serializing complex objects. Emulate it here. + function fakeObjectWithType(type, fields=null, data=null) { + const obj = { + __type__: type, + }; + + // Used for most object types to encode the fields that we serialized and + // send between windows. + if (fields) { + obj.__fields__ = fields; + } + + // Used for ArrayBuffers and ArrayBufferViews like Uint8Array which encode + // an array of data. + if (data) { + obj.__data__ = data; + } + + return obj; + } + + it('builds a formatted string from a undefined value', () => { + logResult(undefined); + expect(mockLogElement.querySelector('.data').textContent) + .toContain('=> undefined'); + }); + + it('builds a formatted string from a number', () => { + logResult(12345); + expect(mockLogElement.querySelector('.data').textContent) + .toContain('=> 12345'); + }); + + it('builds a formatted string from a boolean', () => { + logResult(true); + expect(mockLogElement.querySelector('.data').textContent) + .toContain('=> true'); + }); + + it('builds a formatted string from null', () => { + logResult(null); + expect(mockLogElement.querySelector('.data').textContent) + .toContain('=> null'); + }); + + it('builds a formatted string from an array', () => { + const array = ['Value 0', 'Value 1', 'Value 2']; + logResult(array); + + const text = mockLogElement.querySelector('.data').textContent; + expect(text).toContain('=> [\n'); + + const arrayText = text.split('=> ')[1]; + expect(JSON5.parse(arrayText)).toEqual(array); + }); + + it('builds a formatted string from a Uint8Array', () => { + const array = [12, 34, 12, 65, 34, 634, 78, 324, 54, 23, 53]; + logResult(fakeObjectWithType( + 'Uint8Array', /* fields= */ null, /* data= */ array)); + + const text = mockLogElement.querySelector('.data').textContent; + expect(text).toContain('=> Uint8Array instance [\n'); + + const arrayText = text.split('=> Uint8Array instance ')[1]; + expect(JSON5.parse(arrayText)).toEqual(array); + }); + + it('builds a formatted string from an Object', () => { + const object = { + persistantStateRequired: true, + }; + logResult(object); + + const text = mockLogElement.querySelector('.data').textContent; + expect(text).toContain('=> {\n'); + + const objectText = text.split('=> ')[1]; + expect(JSON5.parse(objectText)).toEqual(object); + }); + + it('builds a formatted string from an Object with a type', () => { + const fields = { + keySystem: 'test.com', + }; + const object = fakeObjectWithType('MediaKeys', fields); + logResult(object); + + const text = mockLogElement.querySelector('.data').textContent; + expect(text).toContain('=> MediaKeys instance {\n'); + + const objectText = text.split('=> MediaKeys instance ')[1]; + expect(JSON5.parse(objectText)).toEqual(fields); + }); }); }); diff --git a/spec/support/jasmine-browser.json b/spec/support/jasmine-browser.json index d7f8320..a106a7b 100644 --- a/spec/support/jasmine-browser.json +++ b/spec/support/jasmine-browser.json @@ -1,10 +1,10 @@ { "srcDir": ".", "srcFiles": [ - "log_constructor.js", - "prototypes.js", - "mutation-summary.js", - "eme_listeners.js" + "node_modules/json5/dist/index.js", + "log-window.js", + "trace-anything.js", + "eme-trace-config.js" ], "specDir": "spec", "specFiles": [