diff --git a/lighthouse-core/audits/dobetterweb/no-mutation-events.js b/lighthouse-core/audits/dobetterweb/no-mutation-events.js
new file mode 100644
index 000000000000..fec98b7cec04
--- /dev/null
+++ b/lighthouse-core/audits/dobetterweb/no-mutation-events.js
@@ -0,0 +1,77 @@
+/**
+ * @license
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * 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 Audit a page to see if it is using Mutation Events (and suggest
+ * MutationObservers instead).
+ */
+
+'use strict';
+
+const url = require('url');
+const Audit = require('../audit');
+const Formatter = require('../../formatters/formatter');
+
+class NoMutationEventsAudit extends Audit {
+
+ /**
+ * @return {!AuditMeta}
+ */
+ static get meta() {
+ return {
+ category: 'JavaScript',
+ name: 'no-mutation-events',
+ description: 'Site does not Mutation Events in its own scripts',
+ helpText: 'Using Mutation events degrades application performance. They are deprecated in the DOM events spec, replaced by MutationObservers.',
+ requiredArtifacts: ['URL', 'MutationEventUse']
+ };
+ }
+
+ /**
+ * @param {!Artifacts} artifacts
+ * @return {!AuditResult}
+ */
+ static audit(artifacts) {
+ if (typeof artifacts.MutationEventUse === 'undefined' ||
+ artifacts.MutationEventUse === -1) {
+ return NoMutationEventsAudit.generateAuditResult({
+ rawValue: -1,
+ debugString: 'MutationEventUse gatherer did not run'
+ });
+ }
+
+ const pageHost = url.parse(artifacts.URL.finalUrl).host;
+ // Filter usage from other hosts.
+ const results = artifacts.MutationEventUse.usage.filter(err => {
+ return url.parse(err.url).host === pageHost;
+ }).map(err => {
+ return Object.assign({
+ label: `line: ${err.line}, col: ${err.col}`
+ }, err);
+ });
+
+ return NoMutationEventsAudit.generateAuditResult({
+ rawValue: results.length === 0,
+ extendedInfo: {
+ formatter: Formatter.SUPPORTED_FORMATS.URLLIST,
+ value: results
+ }
+ });
+ }
+}
+
+module.exports = NoMutationEventsAudit;
diff --git a/lighthouse-core/config/dobetterweb.json b/lighthouse-core/config/dobetterweb.json
index ee29c4d0f5de..85afe88c2b18 100644
--- a/lighthouse-core/config/dobetterweb.json
+++ b/lighthouse-core/config/dobetterweb.json
@@ -9,6 +9,7 @@
"../gather/gatherers/dobetterweb/console-time-usage",
"../gather/gatherers/dobetterweb/datenow",
"../gather/gatherers/dobetterweb/document-write",
+ "../gather/gatherers/dobetterweb/mutation-events",
"../gather/gatherers/dobetterweb/websql"
]
}],
@@ -18,6 +19,7 @@
"../audits/dobetterweb/no-console-time",
"../audits/dobetterweb/no-datenow",
"../audits/dobetterweb/no-document-write",
+ "../audits/dobetterweb/no-mutation-events",
"../audits/dobetterweb/no-old-flexbox",
"../audits/dobetterweb/no-websql",
"../audits/dobetterweb/uses-http2",
@@ -65,6 +67,9 @@
},
"no-console-time": {
"expectedValue": false
+ },
+ "no-mutation-events": {
+ "expectedValue": false
}
}
}, {
diff --git a/lighthouse-core/gather/gatherers/dobetterweb/mutation-events.js b/lighthouse-core/gather/gatherers/dobetterweb/mutation-events.js
new file mode 100644
index 000000000000..ddb729a031e6
--- /dev/null
+++ b/lighthouse-core/gather/gatherers/dobetterweb/mutation-events.js
@@ -0,0 +1,65 @@
+/**
+ * @license
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * 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 Tests whether the page is using Date.now().
+ */
+
+'use strict';
+
+const Gatherer = require('../gatherer');
+
+const MUTATION_EVENTS = [
+ 'DOMAttrModified',
+ 'DOMAttributeNameChanged',
+ 'DOMCharacterDataModified',
+ 'DOMElementNameChanged',
+ 'DOMNodeInserted',
+ 'DOMNodeInsertedIntoDocument',
+ 'DOMNodeRemoved',
+ 'DOMNodeRemovedFromDocument',
+ 'DOMSubtreeModified'
+];
+
+class MutationEventUse extends Gatherer {
+
+ beforePass(options) {
+ this.collectUsage = options.driver.captureFunctionCallSites('addEventListener');
+ this.collectUsage2 = options.driver.captureFunctionCallSites('document.addEventListener');
+ // TODO: document.body appears to not be defined by this time.
+ // this.collectUsage3 = options.driver.captureFunctionCallSites('document.body.addEventListener');
+ }
+
+ afterPass() {
+ const promise = this.collectUsage().then(results1 => {
+ return this.collectUsage2().then(results2 => results1.concat(results2));
+ });
+
+ return promise.then(uses => {
+ uses = uses.filter(use => {
+ const eventName = use.args[0];
+ return MUTATION_EVENTS.indexOf(eventName) !== -1;
+ });
+ this.artifact.usage = uses;
+ }, _ => {
+ this.artifact = -1;
+ return;
+ });
+ }
+}
+
+module.exports = MutationEventUse;
diff --git a/lighthouse-core/test/audits/dobetterweb/no-mutation-events-test.js b/lighthouse-core/test/audits/dobetterweb/no-mutation-events-test.js
new file mode 100644
index 000000000000..9af18e6da52e
--- /dev/null
+++ b/lighthouse-core/test/audits/dobetterweb/no-mutation-events-test.js
@@ -0,0 +1,69 @@
+/**
+ * Copyright 2016 Google Inc. All rights reserved.
+ *
+ * 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.
+ */
+'use strict';
+
+const NoMutationEventsAudit = require('../../../audits/dobetterweb/no-mutation-events.js');
+const assert = require('assert');
+
+const URL = 'https://example.com';
+
+/* eslint-env mocha */
+
+describe('Page does not use mutation events', () => {
+ it('fails when no input present', () => {
+ const auditResult = NoMutationEventsAudit.audit({});
+ assert.equal(auditResult.rawValue, -1);
+ assert.ok(auditResult.debugString);
+ });
+
+ it('passes when mutation events are not used', () => {
+ const auditResult = NoMutationEventsAudit.audit({
+ MutationEventUse: {usage: []},
+ URL: {finalUrl: URL},
+ });
+ assert.equal(auditResult.rawValue, true);
+ assert.equal(auditResult.extendedInfo.value.length, 0);
+ });
+
+ it('passes when mutation events are used on a different origin', () => {
+ const auditResult = NoMutationEventsAudit.audit({
+ MutationEventUse: {
+ usage: [
+ {url: 'http://different.com/two', line: 2, col: 2},
+ {url: 'http://example2.com/two', line: 2, col: 22}
+ ]
+ },
+ URL: {finalUrl: URL},
+ });
+ assert.equal(auditResult.rawValue, true);
+ assert.equal(auditResult.extendedInfo.value.length, 0);
+ });
+
+ it('fails when mutation events are used on the origin', () => {
+ const auditResult = NoMutationEventsAudit.audit({
+ MutationEventUse: {
+ usage: [
+ {url: 'http://example.com/one', line: 1, col: 1},
+ {url: 'http://example.com/two', line: 10, col: 1},
+ {url: 'http://example2.com/two', line: 2, col: 22}
+ ]
+ },
+ URL: {finalUrl: URL},
+ });
+ assert.equal(auditResult.rawValue, false);
+ assert.equal(auditResult.extendedInfo.value.length, 2);
+ });
+});