Skip to content

Commit

Permalink
Introducing the mutationEvents runner module
Browse files Browse the repository at this point in the history
  • Loading branch information
Joris-van-der-Wel committed Dec 4, 2017
1 parent dfea279 commit 3495988
Show file tree
Hide file tree
Showing 10 changed files with 486 additions and 0 deletions.
2 changes: 2 additions & 0 deletions building/buildSources.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const buildBundles = async (outputDirectoryPath, buildConfigPath, {instrumentCov
bundle(rootPath('runner-modules/chai/lib/content'), 'chai-content.js'),
bundle(rootPath('runner-modules/contentEvents/lib/script-env'), 'contentEvents-script-env.js'),
bundle(rootPath('runner-modules/contentEvents/lib/content'), 'contentEvents-content.js'),
bundle(rootPath('runner-modules/mutationEvents/lib/script-env'), 'mutationEvents-script-env.js'),
bundle(rootPath('runner-modules/mutationEvents/lib/content'), 'mutationEvents-content.js'),
bundle(rootPath('runner-modules/eventSimulation/lib/script-env'), 'eventSimulation-script-env.js'),
bundle(rootPath('runner-modules/eventSimulation/lib/content'), 'eventSimulation-content.js'),
bundle(rootPath('runner-modules/expect/lib/script-env'), 'expect-script-env.js'),
Expand Down
1 change: 1 addition & 0 deletions runner-modules/background-manifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module.exports = new Map([
['assert', require('./assert/lib/background')],
['chai', require('./chai/lib/background')],
['contentEvents', require('./contentEvents/lib/background')],
['mutationEvents', require('./mutationEvents/lib/background/index')],
['eventSimulation', require('./eventSimulation/lib/background')],
['expect', require('./expect/lib/background')],
['httpEvents', require('./httpEvents/lib/background')],
Expand Down
11 changes: 11 additions & 0 deletions runner-modules/mutationEvents/lib/background/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use strict';
const scriptEnvUrl = browser.extension.getURL('/build/mutationEvents-script-env.js');

module.exports = async script => {
await script.include('runResult'); // required by content
const handleTabsInitializingTabContent = ({executeContentScript}) => {
executeContentScript('mutationEvents', '/build/mutationEvents-content.js');
};
script.on('tabs.initializingTabContent', handleTabsInitializingTabContent);
script.importScripts(scriptEnvUrl);
};
9 changes: 9 additions & 0 deletions runner-modules/mutationEvents/lib/content/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
'use strict';
/* global document:false */

const {observeDocument} = require('./mutationEvents');

openRunnerRegisterRunnerModule('mutationEvents', async ({getModule}) => {
const {scriptResult} = await getModule('runResult');
observeDocument(document, scriptResult);
});
120 changes: 120 additions & 0 deletions runner-modules/mutationEvents/lib/content/mutationEvents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
'use strict';
const {DOCUMENT_NODE, ELEMENT_NODE, addNodeToAncestorSet, getUniqueSelector} = require('../../../../lib/domUtilities');
const log = require('../../../../lib/logger')({hostname: 'content', MODULE: 'mutationEvents/content/mutationEvents'});

const observedDocuments = new WeakSet();

const observeDocument = (document, runResult) => {
if (observedDocuments.has(document)) {
throw Error('The given document is already being observed');
}

const window = document.defaultView;

const observer = new window.MutationObserver(mutations => {
try {
handleMutations(document, runResult, mutations);
}
catch (err) {
log.error({err}, 'Uncaught error in handleMutations()');
}
});

observer.observe(document, {
attributeOldValue: false,
attributes: false, // todo?
characterData: false, // todo?
characterDataOldValue: false,
childList: true,
subtree: true,
});

observedDocuments.add(document);
};

const handleMutations = (document, runResult, mutations) => {
const {TimePoint} = runResult;
const startTime = new TimePoint();
const addedElements = new Set();
const removedElements = new Map(); // removed node -> old parent node
let addedElementRawCount = 0;
let removedElementRawCount = 0;

for (const mutation of mutations) {
if (mutation.type === 'childList') {
const parent = mutation.target; // Either an Element or HTMLDocument
const isParentDisconnected = !document.contains(parent);

for (const addedNode of mutation.addedNodes) {
if (addedNode.nodeType !== ELEMENT_NODE) {
continue;
}

++addedElementRawCount;
removedElements.delete(addedNode);

if (!isParentDisconnected && addedNode.parentNode === parent) {
addNodeToAncestorSet(addedElements, addedNode);
}
}

for (const removedNode of mutation.removedNodes) {
if (removedNode.nodeType !== ELEMENT_NODE) {
continue;
}

++removedElementRawCount;
addedElements.delete(removedNode);

if (!isParentDisconnected) {
// the parent is no longer in the document
// we only care about removed nodes that were the top most ancestor of a "batch" of removes
removedElements.set(removedNode, parent);
}

}
}
}

if (!addedElementRawCount && !removedElementRawCount) {
return;
}

const addedSelectors = new Array(addedElements.size);
const removedSelectors = new Array(removedElements.size);
addedSelectors.length = 0;
removedSelectors.length = 0;

for (const addedElement of addedElements) {
addedSelectors.push(getUniqueSelector(addedElement));
}

for (const [removedElement, parentElement] of removedElements) {
const removedSelector = getUniqueSelector(removedElement, {includeAncestors: false});

if (parentElement.nodeType === DOCUMENT_NODE) {
removedSelectors.push(removedSelector); // probably "html"
}
else if (parentElement.nodeType === ELEMENT_NODE) {
const parentSelector = getUniqueSelector(parentElement);
removedSelectors.push(parentSelector + ' > ' + removedSelector);
}
else {
throw Error(`Assertion Error: Unexpected nodeType ${parentElement.nodeType}`);
}
}

const event = runResult.timePointEvent('content:domMutation', startTime, startTime);
event.shortTitle = 'Mutation';
event.longTitle = 'DOM Mutation';
event.setMetaData('addedElementRawCount', addedElementRawCount);
event.setMetaData('removedElementRawCount', removedElementRawCount);
event.setMetaData('addedElements', addedSelectors);
event.setMetaData('removedElements', removedSelectors);
event.setMetaData('overhead', new TimePoint().diff(startTime));
};


module.exports = {
observeDocument,
};
5 changes: 5 additions & 0 deletions runner-modules/mutationEvents/lib/script-env/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

openRunnerRegisterRunnerModule('mutationEvents', async ({script}) => {
return Object.freeze({});
});
6 changes: 6 additions & 0 deletions runner-modules/runResult/lib/RunResult.js
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ class RunResult {
}
}

// convenience so that we can access all the functionality of a RunResult instance without having to pass the entire module:
RunResult.prototype.TimePoint = TimePoint;
RunResult.prototype.TimePeriod = TimePeriod;
RunResult.prototype.Event = Event;
RunResult.prototype.Transaction = Transaction;

Object.freeze(RunResult);
Object.freeze(RunResult.prototype);

Expand Down
Loading

0 comments on commit 3495988

Please sign in to comment.