Bug 975522 - Add CSS coverage commands; r=harth

This commit is contained in:
Joe Walker 2014-05-22 11:04:47 +01:00
parent 2f97ac1d46
commit d265e4d588
31 changed files with 1962 additions and 72 deletions

View File

@ -13,6 +13,7 @@ const commandModules = [
"gcli/commands/calllog",
"gcli/commands/cmd",
"gcli/commands/cookie",
"gcli/commands/csscoverage",
"gcli/commands/jsb",
"gcli/commands/listen",
"gcli/commands/media",

View File

@ -32,6 +32,25 @@ support-files =
[browser_cmd_cookie.js]
support-files =
browser_cmd_cookie.html
[browser_cmd_csscoverage_oneshot.js]
support-files =
browser_cmd_csscoverage_page1.html
browser_cmd_csscoverage_page2.html
browser_cmd_csscoverage_page3.html
browser_cmd_csscoverage_sheetA.css
browser_cmd_csscoverage_sheetB.css
browser_cmd_csscoverage_sheetC.css
browser_cmd_csscoverage_sheetD.css
[browser_cmd_csscoverage_startstop.js]
support-files =
browser_cmd_csscoverage_page1.html
browser_cmd_csscoverage_page2.html
browser_cmd_csscoverage_page3.html
browser_cmd_csscoverage_sheetA.css
browser_cmd_csscoverage_sheetB.css
browser_cmd_csscoverage_sheetC.css
browser_cmd_csscoverage_sheetD.css
[browser_cmd_csscoverage_util.js]
[browser_cmd_jsb.js]
support-files =
browser_cmd_jsb_script.jsi

View File

@ -0,0 +1,140 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the addon commands works as they should
const csscoverage = require("devtools/server/actors/csscoverage");
const PAGE_1 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page1.html";
const PAGE_2 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page2.html";
const PAGE_3 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page3.html";
const SHEET_A = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetA.css";
const SHEET_B = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetB.css";
const SHEET_C = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetC.css";
const SHEET_D = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetD.css";
let test = asyncTest(function*() {
let options = yield helpers.openTab(PAGE_3);
yield helpers.openToolbar(options);
let usage = yield csscoverage.getUsage(options.target);
yield usage.oneshot();
let running = yield usage._testOnly_isRunning();
ok(!running, "csscoverage not is running");
// Page1
let expectedPage1 = { reports: [] };
let actualPage1 = yield usage.createEditorReport(PAGE_1);
isEqualJson(actualPage1, expectedPage1, 'Page1');
// Page2
let expectedPage2 = { reports: [] };
let actualPage2 = yield usage.createEditorReport(PAGE_2);
isEqualJson(actualPage2, expectedPage2, 'Page2');
// Page3
let expectedPage3 = {
reports: [
{
selectorText: ".page3-test2",
start: { line: 9, column: 5 },
},
{
selectorText: ".page3-test3",
start: { line: 3, column: 5 },
}
]
};
let actualPage3 = yield usage.createEditorReport(PAGE_3);
isEqualJson(actualPage3, expectedPage3, 'Page3');
// SheetA
let expectedSheetA = {
reports: [
{
selectorText: ".sheetA-test2",
start: { line: 8, column: 1 },
},
{
selectorText: ".sheetA-test3",
start: { line: 12, column: 1 },
},
{
selectorText: ".sheetA-test4",
start: { line: 16, column: 1 },
}
]
};
let actualSheetA = yield usage.createEditorReport(SHEET_A);
isEqualJson(actualSheetA, expectedSheetA, 'SheetA');
// SheetB
let expectedSheetB = {
reports: [
{
selectorText: ".sheetB-test2",
start: { line: 6, column: 1 },
},
{
selectorText: ".sheetB-test3",
start: { line: 10, column: 1 },
},
{
selectorText: ".sheetB-test4",
start: { line: 14, column: 1 },
}
]
};
let actualSheetB = yield usage.createEditorReport(SHEET_B);
isEqualJson(actualSheetB, expectedSheetB, 'SheetB');
// SheetC
let expectedSheetC = {
reports: [
{
selectorText: ".sheetC-test2",
start: { line: 6, column: 1 },
},
{
selectorText: ".sheetC-test3",
start: { line: 10, column: 1 },
},
{
selectorText: ".sheetC-test4",
start: { line: 14, column: 1 },
}
]
};
let actualSheetC = yield usage.createEditorReport(SHEET_C);
isEqualJson(actualSheetC, expectedSheetC, 'SheetC');
// SheetD
let expectedSheetD = {
reports: [
{
selectorText: ".sheetD-test2",
start: { line: 6, column: 1 },
},
{
selectorText: ".sheetD-test3",
start: { line: 10, column: 1 },
},
{
selectorText: ".sheetD-test4",
start: { line: 14, column: 1 },
}
]
};
let actualSheetD = yield usage.createEditorReport(SHEET_D);
isEqualJson(actualSheetD, expectedSheetD, 'SheetD');
yield helpers.closeToolbar(options);
yield helpers.closeTab(options);
});
function isEqualJson(o1, o2, msg) {
is(JSON.stringify(o1), JSON.stringify(o2), msg);
}

View File

@ -0,0 +1,83 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<!--
First page of the css coverage test.
* Contains page2 in an iframe
* Forwards to page2 on a timeout
-->
<title>Page 1</title>
<style>
@import url(browser_cmd_csscoverage_sheetD.css);
/* This should match below */
.page1-test1 {
color: #011;
}
/* This should not match below */
.page1-test2 {
color: #012;
}
/* This would match if the mouse was in the right place */
.page1-test3:hover {
color: #013;
}
/* This can't match because it's illegal */
.page1-test4:broken {
color: #014;
}
/* This doesn't match until the event fires */
.page1-test5 {
color: #015;
}
/* TODO: include examples of all CSS rules in
https://developer.mozilla.org/en-US/docs/Web/API/CSSRule
and include tests for rules nested in media rules, etc */
/* We're not testing unparable CSS right now */
</style>
<link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetA.css">
<link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetB.css">
<script type="application/javascript;version=1.8">
/* How quickly do we rush through this? */
let delay = 500;
window.addEventListener("load", () => {
setTimeout(() => {
/* This adds <div class=page1-test5></div> */
let parent = document.querySelector("#page1-test5-holder");
let child = document.createElement("div");
child.classList.add("class=page1-test5");
parent.appendChild(child);
/* Then navigate to the next step */
window.location.href = "browser_cmd_csscoverage_page3.html"
}, delay);
});
</script>
</head>
<body>
<h2>Page 1</h2>
<div class=page1-test1>.page1-test1</div>
<div class=page1-test3>.page1-test3</div>
<div id=page1-test5-holder></div>
<div class=sheetA-test1>.sheetA-test1</div>
<div class=sheetA-test3>.sheetA-test3</div>
<div class=sheetB-test1>.sheetB-test1</div>
<div class=sheetB-test3>.sheetB-test3</div>
<div class=sheetC-test1>.sheetC-test1</div>
<div class=sheetC-test3>.sheetC-test3</div>
<div class=sheetD-test1>.sheetD-test1</div>
<div class=sheetD-test3>.sheetD-test3</div>
<iframe src=browser_cmd_csscoverage_page2.html></iframe>
<p>
<a href="browser_cmd_csscoverage_page3.html">Page 3</a>
</p>
</body>
</html>

View File

@ -0,0 +1,58 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Page 2</title>
<style>
@import url(browser_cmd_csscoverage_sheetD.css);
/* This should match below */
.page2-test1 {
color: #021;
}
/* This should not match below */
.page2-test2 {
color: #022;
}
/* This doesn't match until the event fires */
.page2-test3 {
color: #023;
}
</style>
<link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetA.css">
<link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetB.css">
<script type="application/javascript;version=1.8">
/* How quickly do we rush through this? */
let delay = 500;
window.addEventListener("load", () => {
setTimeout(() => {
/* This adds <div class=page2-test3></div> */
let parent = document.querySelector("#page2-test3-holder");
let child = document.createElement("div");
child.classList.add("class=page2-test3");
parent.appendChild(child);
}, delay);
});
</script>
</head>
<body>
<h2>Page 2</h2>
<div class=page2-test1>.page2-test1</div>
<div class=page2-test3>.page2-test3</div>
<div id=page2-test3-holder></div>
<div class=sheetA-test1>.sheetA-test1</div>
<div class=sheetA-test4>.sheetA-test4</div>
<div class=sheetB-test1>.sheetB-test1</div>
<div class=sheetB-test4>.sheetB-test4</div>
<div class=sheetC-test1>.sheetC-test1</div>
<div class=sheetC-test4>.sheetC-test4</div>
<div class=sheetD-test1>.sheetD-test1</div>
<div class=sheetD-test4>.sheetD-test4</div>
</body>
</html>

View File

@ -0,0 +1,47 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Page 3</title>
<style>
@import url(browser_cmd_csscoverage_sheetD.css);
/* This should match below */
.page3-test1 {
color: #031;
}
/* This should not match below */
.page3-test2 {
color: #032;
}
</style>
<style>
/* This also should not match below, but in a second inline sheet */
.page3-test3 {
color: #033;
}
</style>
<link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetA.css">
<link rel="stylesheet" type="text/css" href="browser_cmd_csscoverage_sheetB.css">
</head>
<body>
<h2>Page 3</h2>
<div class=page3-test1>.page3-test1</div>
<div class=sheetA-test1>.sheetA-test1</div>
<div class=sheetA-test5>.sheetA-test5</div>
<div class=sheetB-test1>.sheetB-test1</div>
<div class=sheetB-test5>.sheetB-test5</div>
<div class=sheetC-test1>.sheetC-test1</div>
<div class=sheetC-test5>.sheetC-test5</div>
<div class=sheetD-test1>.sheetD-test1</div>
<div class=sheetD-test5>.sheetD-test5</div>
<p>
<a href="browser_cmd_csscoverage_page1.html">Page 1</a>
</p>
</body>
</html>

View File

@ -0,0 +1,22 @@
@import url(browser_cmd_csscoverage_sheetC.css);
/* This should match in page 1, 2 and 3 */
.sheetA-test1 {
color: #0A1;
}
/* This should not match anywhere */
.sheetA-test2 {
color: #0A2;
}
/* This should match in page 1 only */
.sheetA-test3 {
color: #0A3;
}
/* This should match in page 2 only */
.sheetA-test4 {
color: #0A4;
}
/* This should match in page 3 only */
.sheetA-test5 {
color: #0A5;
}

View File

@ -0,0 +1,20 @@
/* This should match in page 1, 2 and 3 */
.sheetB-test1 {
color: #0B1;
}
/* This should not match anywhere */
.sheetB-test2 {
color: #0B2;
}
/* This should match in page 1 only */
.sheetB-test3 {
color: #0B3;
}
/* This should match in page 2 only */
.sheetB-test4 {
color: #0B4;
}
/* This should match in page 3 only */
.sheetB-test5 {
color: #0B5;
}

View File

@ -0,0 +1,20 @@
/* This should match in page 1, 2 and 3 */
.sheetC-test1 {
color: #0C1;
}
/* This should not match anywhere */
.sheetC-test2 {
color: #0C2;
}
/* This should match in page 1 only */
.sheetC-test3 {
color: #0C3;
}
/* This should match in page 2 only */
.sheetC-test4 {
color: #0C4;
}
/* This should match in page 3 only */
.sheetC-test5 {
color: #0C5;
}

View File

@ -0,0 +1,20 @@
/* This should match in page 1, 2 and 3 */
.sheetD-test1 {
color: #0D1;
}
/* This should not match anywhere */
.sheetD-test2 {
color: #0D2;
}
/* This should match in page 1 only */
.sheetD-test3 {
color: #0D3;
}
/* This should match in page 2 only */
.sheetD-test4 {
color: #0D4;
}
/* This should match in page 3 only */
.sheetD-test5 {
color: #0D5;
}

View File

@ -0,0 +1,147 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the addon commands works as they should
const csscoverage = require("devtools/server/actors/csscoverage");
const PAGE_1 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page1.html";
const PAGE_2 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page2.html";
const PAGE_3 = TEST_BASE_HTTPS + "browser_cmd_csscoverage_page3.html";
const SHEET_A = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetA.css";
const SHEET_B = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetB.css";
const SHEET_C = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetC.css";
const SHEET_D = TEST_BASE_HTTPS + "browser_cmd_csscoverage_sheetD.css";
let test = asyncTest(function*() {
let options = yield helpers.openTab("about:blank");
yield helpers.openToolbar(options);
let usage = yield csscoverage.getUsage(options.target);
yield usage.start();
let running = yield usage._testOnly_isRunning();
ok(running, "csscoverage is running");
yield helpers.navigate(PAGE_3, options);
yield usage.stop();
running = yield usage._testOnly_isRunning();
ok(!running, "csscoverage not is running");
// Page1
let expectedPage1 = { reports: [] };
let actualPage1 = yield usage.createEditorReport(PAGE_1);
isEqualJson(actualPage1, expectedPage1, 'Page1');
// Page2
let expectedPage2 = { reports: [] };
let actualPage2 = yield usage.createEditorReport(PAGE_2);
isEqualJson(actualPage2, expectedPage2, 'Page2');
// Page3
let expectedPage3 = {
reports: [
{
selectorText: ".page3-test2",
start: { line: 9, column: 5 },
},
{
selectorText: ".page3-test3",
start: { line: 3, column: 5 },
}
]
};
let actualPage3 = yield usage.createEditorReport(PAGE_3);
isEqualJson(actualPage3, expectedPage3, 'Page3');
// SheetA
let expectedSheetA = {
reports: [
{
selectorText: ".sheetA-test2",
start: { line: 8, column: 1 },
},
{
selectorText: ".sheetA-test3",
start: { line: 12, column: 1 },
},
{
selectorText: ".sheetA-test4",
start: { line: 16, column: 1 },
}
]
};
let actualSheetA = yield usage.createEditorReport(SHEET_A);
isEqualJson(actualSheetA, expectedSheetA, 'SheetA');
// SheetB
let expectedSheetB = {
reports: [
{
selectorText: ".sheetB-test2",
start: { line: 6, column: 1 },
},
{
selectorText: ".sheetB-test3",
start: { line: 10, column: 1 },
},
{
selectorText: ".sheetB-test4",
start: { line: 14, column: 1 },
}
]
};
let actualSheetB = yield usage.createEditorReport(SHEET_B);
isEqualJson(actualSheetB, expectedSheetB, 'SheetB');
// SheetC
let expectedSheetC = {
reports: [
{
selectorText: ".sheetC-test2",
start: { line: 6, column: 1 },
},
{
selectorText: ".sheetC-test3",
start: { line: 10, column: 1 },
},
{
selectorText: ".sheetC-test4",
start: { line: 14, column: 1 },
}
]
};
let actualSheetC = yield usage.createEditorReport(SHEET_C);
isEqualJson(actualSheetC, expectedSheetC, 'SheetC');
// SheetD
let expectedSheetD = {
reports: [
{
selectorText: ".sheetD-test2",
start: { line: 6, column: 1 },
},
{
selectorText: ".sheetD-test3",
start: { line: 10, column: 1 },
},
{
selectorText: ".sheetD-test4",
start: { line: 14, column: 1 },
}
]
};
let actualSheetD = yield usage.createEditorReport(SHEET_D);
isEqualJson(actualSheetD, expectedSheetD, 'SheetD');
yield helpers.closeToolbar(options);
yield helpers.closeTab(options);
});
function isEqualJson(o1, o2, msg) {
is(JSON.stringify(o1), JSON.stringify(o2), msg);
}

View File

@ -0,0 +1,24 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
// Tests that the addon commands works as they should
const csscoverage = require("devtools/server/actors/csscoverage");
let test = asyncTest(function*() {
testDeconstructRuleId();
});
function testDeconstructRuleId() {
// This is the easy case
let rule = csscoverage.deconstructRuleId("http://thing/blah|10|20");
is(rule.url, "http://thing/blah", "1 url");
is(rule.line, 10, "1 line");
is(rule.column, 20, "1 column");
// This is the harder case with a URL containing a '|'
let rule = csscoverage.deconstructRuleId("http://thing/blah?q=a|b|11|22");
is(rule.url, "http://thing/blah?q=a|b", "2 url");
is(rule.line, 11, "2 line");
is(rule.column, 22, "2 column");
}

View File

@ -5,6 +5,9 @@
const TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/commandline/test/";
const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/commandline/test/";
var require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
var console = require("resource://gre/modules/devtools/Console.jsm").console;
// Import the GCLI test helper
let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
Services.scriptloader.loadSubScript(testDir + "/helpers.js", this);
@ -34,3 +37,7 @@ registerCleanupFunction(function tearDown() {
.getInterface(Ci.nsIDOMWindowUtils)
.garbageCollect();
});
function asyncTest(generator) {
return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
}

View File

@ -185,13 +185,7 @@ helpers.openTab = function(url, options) {
options.browser = tabbrowser.getBrowserForTab(options.tab);
options.target = TargetFactory.forTab(options.tab);
options.browser.contentWindow.location = url;
return helpers.listenOnce(options.browser, "load", true).then(function() {
options.document = options.browser.contentDocument;
options.window = options.document.defaultView;
return options;
});
return helpers.navigate(url, options);
};
/**
@ -225,13 +219,39 @@ helpers.closeTab = function(options) {
* happens on the new tab
*/
helpers.openToolbar = function(options) {
options = options || {};
options.chromeWindow = options.chromeWindow || window;
return options.chromeWindow.DeveloperToolbar.show(true).then(function() {
var display = options.chromeWindow.DeveloperToolbar.display;
options.automator = createFFDisplayAutomator(display);
options.requisition = display.requisition;
return options;
});
};
/**
* Navigate the current tab to a URL
*/
helpers.navigate = function(url, options) {
options = options || {};
options.chromeWindow = options.chromeWindow || window;
options.tab = options.tab || options.chromeWindow.gBrowser.selectedTab;
var tabbrowser = options.chromeWindow.gBrowser;
options.browser = tabbrowser.getBrowserForTab(options.tab);
var promise = helpers.listenOnce(options.browser, "load", true).then(function() {
options.document = options.browser.contentDocument;
options.window = options.document.defaultView;
return options;
});
options.browser.contentWindow.location = url;
return promise;
};
/**
* Undo the effects of |helpers.openToolbar|
* @param options The options object passed to |helpers.openToolbar|
@ -1128,7 +1148,13 @@ Object.defineProperty(helpers, 'timingSummary', {
* If typeof output is a string then the output should be exactly equal
* to the given string. If the type of output is a RegExp or array of
* RegExps then the output should match all RegExps
* - post: Function to be called after the checks have been run
* - error: If true, then it is expected that this command will fail (that
* is, return a rejected promise or throw an exception)
* - type: A string documenting the expected type of the return value
* - post: Function to be called after the checks have been run, which will be
* passed 2 parameters: the first being output data (with type, data, and
* error properties), and the second being the converted text version of
* the output data
*/
helpers.audit = function(options, audits) {
checkOptions(options);

View File

@ -950,7 +950,11 @@ OutputPanel.prototype._update = function() {
if (this.displayedOutput.data != null) {
let context = this._devtoolbar.display.requisition.conversionContext;
this.displayedOutput.convert('dom', context).then((node) => {
this.displayedOutput.convert('dom', context).then(node => {
if (node == null) {
return;
}
while (this._div.hasChildNodes()) {
this._div.removeChild(this._div.firstChild);
}

View File

@ -28,6 +28,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
const csscoverage = require("devtools/server/actors/csscoverage");
const console = require("resource://gre/modules/devtools/Console.jsm").console;
const LOAD_ERROR = "error-load";
const STYLE_EDITOR_TEMPLATE = "stylesheet";
@ -468,6 +470,14 @@ StyleEditorUI.prototype = {
editor.onShow();
csscoverage.getUsage(this._target).then(usage => {
let href = editor.styleSheet.href || editor.styleSheet.nodeHref;
usage.createEditorReport(href).then(data => {
editor.removeAllUnusedRegions();
editor.addUnusedRegions(data.reports);
});
}, console.error);
this.emit("editor-selected", editor);
}.bind(this)).then(null, Cu.reportError);
}.bind(this)

View File

@ -5,7 +5,7 @@
"use strict";
this.EXPORTED_SYMBOLS = ["StyleSheetEditor"];
this.EXPORTED_SYMBOLS = ["StyleSheetEditor", "prettifyCSS"];
const Cc = Components.classes;
const Ci = Components.interfaces;
@ -41,6 +41,9 @@ const CHECK_LINKED_SHEET_DELAY=500;
// How many times to check for linked file changes
const MAX_CHECK_COUNT=10;
// The classname used to show a line that is not used
const UNUSED_CLASS = "cm-unused-line";
/**
* StyleSheetEditor controls the editor linked to a particular StyleSheet
* object.
@ -209,18 +212,57 @@ StyleSheetEditor.prototype = {
* Start fetching the full text source for this editor's sheet.
*/
fetchSource: function(callback) {
this.styleSheet.getText().then((longStr) => {
return this.styleSheet.getText().then((longStr) => {
longStr.string().then((source) => {
this._state.text = prettifyCSS(source);
this.sourceLoaded = true;
callback(source);
if (callback) {
callback(source);
}
return source;
});
}, e => {
this.emit("error", LOAD_ERROR, this.styleSheet.href);
throw e;
})
},
/**
* Add markup to a region. UNUSED_CLASS is added to specified lines
* @param region An object shaped like
* {
* start: { line: L1, column: C1 },
* end: { line: L2, column: C2 } // optional
* }
*/
addUnusedRegion: function(region) {
this.sourceEditor.addLineClass(region.start.line - 1, UNUSED_CLASS);
if (region.end) {
for (let i = region.start.line; i <= region.end.line; i++) {
this.sourceEditor.addLineClass(i - 1, UNUSED_CLASS);
}
}
},
/**
* As addUnusedRegion except that it takes an array of regions
*/
addUnusedRegions: function(regions) {
for (let region of regions) {
this.addUnusedRegion(region);
}
},
/**
* Remove all the unused markup regions added by addUnusedRegion
*/
removeAllUnusedRegions: function() {
for (let i = 0; i < this.sourceEditor.lineCount(); i++) {
this.sourceEditor.removeLineClass(i, UNUSED_CLASS);
}
},
/**
* Forward property-change event from stylesheet.
*

View File

@ -3,6 +3,23 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#style-editor-chrome {
-moz-box-flex: 1;
}
.csscoverage-report-container {
-moz-box-flex: 1;
}
.csscoverage-report {
-moz-box-orient: horizontal;
}
.csscoverage-report-container {
overflow-x: hidden;
overflow-y: auto;
}
.stylesheet-error-message {
display: none;
}

View File

@ -9,6 +9,8 @@
%editMenuStrings;
<!ENTITY % sourceEditorStrings SYSTEM "chrome://browser/locale/devtools/sourceeditor.dtd">
%sourceEditorStrings;
<!ENTITY % csscoverageDTD SYSTEM "chrome://global/locale/devtools/csscoverage.dtd">
%csscoverageDTD;
]>
<?xml-stylesheet href="chrome://global/skin/global.css" type="text/css"?>
@ -72,62 +74,116 @@
<xul:keyset id="sourceEditorKeys"/>
<xul:box id="style-editor-chrome" class="theme-body splitview-root loading"
context="sidebar-context">
<xul:box class="splitview-controller">
<xul:box class="splitview-main">
<xul:toolbar class="devtools-toolbar">
<xul:toolbarbutton class="style-editor-newButton devtools-toolbarbutton"
accesskey="&newButton.accesskey;"
tooltiptext="&newButton.tooltip;"
label="&newButton.label;"/>
<xul:toolbarbutton class="style-editor-importButton devtools-toolbarbutton"
accesskey="&importButton.accesskey;"
tooltiptext="&importButton.tooltip;"
label="&importButton.label;"/>
</xul:toolbar>
</xul:box>
<xul:box id="splitview-resizer-target" class="theme-sidebar splitview-nav-container"
persist="height">
<ol class="splitview-nav" tabindex="0"></ol>
<div class="splitview-nav placeholder empty">
<p><strong>&noStyleSheet.label;</strong></p>
<p>&noStyleSheet-tip-start.label;
<a href="#"
class="style-editor-newButton">&noStyleSheet-tip-action.label;</a>
&noStyleSheet-tip-end.label;</p>
</div>
</xul:box> <!-- .splitview-nav-container -->
</xul:box> <!-- .splitview-controller -->
<xul:splitter class="devtools-side-splitter splitview-landscape-splitter devtools-invisible-splitter"/>
<xul:box class="splitview-side-details"/>
<xul:stack id="style-editor-chrome" class="loading theme-body">
<div id="splitview-templates" hidden="true">
<li id="splitview-tpl-summary-stylesheet" tabindex="0">
<xul:label class="stylesheet-enabled" tabindex="0"
tooltiptext="&visibilityToggle.tooltip;"
accesskey="&saveButton.accesskey;"></xul:label>
<hgroup class="stylesheet-info">
<h1><a class="stylesheet-name" tabindex="0"><xul:label crop="start"/></a></h1>
<div class="stylesheet-more">
<h3 class="stylesheet-title"></h3>
<h3 class="stylesheet-linked-file"></h3>
<h3 class="stylesheet-rule-count"></h3>
<xul:spacer/>
<h3><xul:label class="stylesheet-saveButton"
tooltiptext="&saveButton.tooltip;"
accesskey="&saveButton.accesskey;">&saveButton.label;</xul:label></h3>
<xul:box class="splitview-root" context="sidebar-context">
<xul:box class="splitview-controller">
<xul:box class="splitview-main">
<xul:toolbar class="devtools-toolbar">
<xul:toolbarbutton class="style-editor-newButton devtools-toolbarbutton"
accesskey="&newButton.accesskey;"
tooltiptext="&newButton.tooltip;"
label="&newButton.label;"/>
<xul:toolbarbutton class="style-editor-importButton devtools-toolbarbutton"
accesskey="&importButton.accesskey;"
tooltiptext="&importButton.tooltip;"
label="&importButton.label;"/>
</xul:toolbar>
</xul:box>
<xul:box id="splitview-resizer-target" class="theme-sidebar splitview-nav-container"
persist="height">
<ol class="splitview-nav" tabindex="0"></ol>
<div class="splitview-nav placeholder empty">
<p><strong>&noStyleSheet.label;</strong></p>
<p>&noStyleSheet-tip-start.label;
<a href="#"
class="style-editor-newButton">&noStyleSheet-tip-action.label;</a>
&noStyleSheet-tip-end.label;</p>
</div>
</hgroup>
</li>
</xul:box> <!-- .splitview-nav-container -->
</xul:box> <!-- .splitview-controller -->
<xul:splitter class="devtools-side-splitter splitview-landscape-splitter devtools-invisible-splitter"/>
<xul:box class="splitview-side-details"/>
<div id="splitview-templates" hidden="true">
<li id="splitview-tpl-summary-stylesheet" tabindex="0">
<xul:label class="stylesheet-enabled" tabindex="0"
tooltiptext="&visibilityToggle.tooltip;"
accesskey="&saveButton.accesskey;"></xul:label>
<hgroup class="stylesheet-info">
<h1><a class="stylesheet-name" tabindex="0"><xul:label crop="start"/></a></h1>
<div class="stylesheet-more">
<h3 class="stylesheet-title"></h3>
<h3 class="stylesheet-linked-file"></h3>
<h3 class="stylesheet-rule-count"></h3>
<xul:spacer/>
<h3><xul:label class="stylesheet-saveButton"
tooltiptext="&saveButton.tooltip;"
accesskey="&saveButton.accesskey;">&saveButton.label;</xul:label></h3>
</div>
</hgroup>
</li>
<xul:box id="splitview-tpl-details-stylesheet" class="splitview-details">
<xul:resizer class="splitview-portrait-resizer"
dir="bottom"
element="splitview-resizer-target"/>
<xul:box class="stylesheet-editor-input textbox"
data-placeholder="&editorTextbox.placeholder;"/>
</xul:box>
</div> <!-- #splitview-templates -->
</xul:box> <!-- .splitview-root -->
<xul:box class="csscoverage-template" hidden="true">
<xul:toolbar class="devtools-toolbar csscoverage-toolbar">
<xul:button class="devtools-toolbarbutton csscoverage-toolbarbutton"
label="&csscoverage.backButton;"
onclick="${onback}"/>
</xul:toolbar>
<!-- The data for this comes from UsageReportActor.createPageReport -->
<div class="csscoverage-report-container">
<div class="csscoverage-report-content">
<h2>&csscoverage.unused;</h2>
<p>&csscoverage.noMatch;</p>
<ul>
<li foreach="rule in ${unusedRules}">
<code>${rule.selectorText}</code>
<span class="link"
title="${rule.url}">(${rule.shortHref} : ${rule.start.line})</span>
</li>
</ul>
<h2>&csscoverage.optimize;</h2>
<p>
&csscoverage.preload1;
<code>&lt;link ...></code>
&csscoverage.preload2;
<code>&lt;style>...</code>
&csscoverage.preload3;
</p>
<div if="${pages.length == 0}">
&csscoverage.noPreload;
</div>
<div if="${pages.length > 0}">
<div foreach="page in ${pages}">
<h3>${page.url}</h3>
<textarea>&lt;style>
<loop foreach="rule in ${page.preloadRules}"
onclick="${rule.onclick}">${rule.formattedCssText}</loop>&lt;/style></textarea>
</div>
</div>
<p>
&csscoverage.footer1;
<a target="_blank" href="&csscoverage.footer2;">&csscoverage.footer3;</a>
&csscoverage.footer4;
</p>
<p>&#160;</p>
</div>
</div>
</xul:box>
<xul:box class="csscoverage-report" hidden="true">
</xul:box>
</xul:stack>
<xul:box id="splitview-tpl-details-stylesheet" class="splitview-details">
<xul:resizer class="splitview-portrait-resizer"
dir="bottom"
element="splitview-resizer-target"/>
<xul:box class="stylesheet-editor-input textbox"
data-placeholder="&editorTextbox.placeholder;"/>
</xul:box>
</div> <!-- #splitview-templates -->
</xul:box> <!-- .splitview-root -->
</xul:window>

View File

@ -101,6 +101,15 @@
background-color: #3689b2;
}
.cm-s-mozilla .cm-unused-line {
text-decoration: line-through;
-moz-text-decoration-color: red;
}
.cm-s-mozilla .cm-executed-line {
background-color: #133c26;
}
.theme-fg-color3,
.cm-s-mozilla .cm-builtin,
.cm-s-mozilla .cm-tag,

View File

@ -74,6 +74,15 @@
border-color: #cddae5;
}
.cm-s-mozilla .cm-unused-line {
text-decoration: line-through;
-moz-text-decoration-color: red;
}
.cm-s-mozilla .cm-executed-line {
background-color: #fcfffc;
}
.theme-fg-color1,
.cm-s-mozilla .cm-number,
.variable-or-property .token-number,

View File

@ -3,6 +3,54 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.theme-light .csscoverage-report {
background: url(background-noise-toolbar.png), #f0f1f2; /* Toolbars */
}
.theme-dark .csscoverage-report {
background: url(background-noise-toolbar.png), #343c45; /* Toolbars */
}
.csscoverage-report-container {
height: 100vh;
padding: 10px;
}
.csscoverage-report-content {
font-size: 13px;
margin: 0 auto;
max-width: 600px;
padding: 0 10px;
}
.csscoverage-report h1,
.csscoverage-report h2,
.csscoverage-report h3 {
font-weight: bold;
}
.csscoverage-report textarea {
width: 100%;
height: 100px;
}
.csscoverage-report > .csscoverage-toolbar {
border: none;
margin: 0;
padding: 0;
}
.csscoverage-report > .csscoverage-toolbarbutton {
min-width: 4em;
min-height: 100vh;
margin: 0;
padding: 0;
border-radius: 0;
border-top: none;
border-bottom: none;
-moz-border-start: none;
}
.stylesheet-title,
.stylesheet-name {
text-decoration: none;
@ -147,4 +195,4 @@ h3 {
.splitview-nav > li > hgroup.stylesheet-info {
-moz-box-align: baseline;
}
}
}

View File

@ -354,23 +354,27 @@ function processForEachMember(state, member, templNode, siblingNode, data, param
try {
var cState = cloneState(state);
handleAsync(member, siblingNode, function(reply, node) {
data[paramName] = reply;
// Clone data because we can't be sure that we can safely mutate it
var newData = Object.create(null);
Object.keys(data).forEach(function(key) {
newData[key] = data[key];
});
newData[paramName] = reply;
if (node.parentNode != null) {
if (templNode.nodeName.toLowerCase() === 'loop') {
for (var i = 0; i < templNode.childNodes.length; i++) {
var clone = templNode.childNodes[i].cloneNode(true);
node.parentNode.insertBefore(clone, node);
processNode(cState, clone, data);
processNode(cState, clone, newData);
}
}
else {
var clone = templNode.cloneNode(true);
clone.removeAttribute('foreach');
node.parentNode.insertBefore(clone, node);
processNode(cState, clone, data);
processNode(cState, clone, newData);
}
}
delete data[paramName];
});
}
finally {

View File

@ -0,0 +1,136 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci } = require("chrome");
const { gDevTools } = require("resource:///modules/devtools/gDevTools.jsm");
const promise = require("resource://gre/modules/Promise.jsm").Promise;
const domtemplate = require("gcli/util/domtemplate");
const csscoverage = require("devtools/server/actors/csscoverage");
const l10n = csscoverage.l10n;
/**
* The commands/converters for GCLI
*/
exports.items = [
{
name: "csscoverage",
hidden: true,
description: l10n.lookup("csscoverageDesc"),
},
{
name: "csscoverage start",
hidden: true,
description: l10n.lookup("csscoverageStartDesc"),
exec: function*(args, context) {
let usage = yield csscoverage.getUsage(context.environment.target);
yield usage.start(context.environment.chromeWindow,
context.environment.target);
}
},
{
name: "csscoverage stop",
hidden: true,
description: l10n.lookup("csscoverageStopDesc"),
exec: function*(args, context) {
let target = context.environment.target;
let usage = yield csscoverage.getUsage(target);
yield usage.stop();
yield gDevTools.showToolbox(target, "styleeditor");
}
},
{
name: "csscoverage oneshot",
hidden: true,
description: l10n.lookup("csscoverageOneShotDesc"),
exec: function*(args, context) {
let target = context.environment.target;
let usage = yield csscoverage.getUsage(target);
yield usage.oneshot();
yield gDevTools.showToolbox(target, "styleeditor");
}
},
{
name: "csscoverage toggle",
hidden: true,
description: l10n.lookup("csscoverageToggleDesc"),
exec: function*(args, context) {
let target = context.environment.target;
let usage = yield csscoverage.getUsage(target);
let running = yield usage.toggle();
if (running) {
return l10n.lookup("csscoverageRunningReply");
}
yield usage.stop();
yield gDevTools.showToolbox(target, "styleeditor");
}
},
{
name: "csscoverage report",
hidden: true,
description: l10n.lookup("csscoverageReportDesc"),
exec: function*(args, context) {
let usage = yield csscoverage.getUsage(context.environment.target);
return {
isTypedData: true,
type: "csscoveragePageReport",
data: yield usage.createPageReport()
};
}
},
{
item: "converter",
from: "csscoveragePageReport",
to: "dom",
exec: function*(csscoveragePageReport, context) {
let target = context.environment.target;
let toolbox = yield gDevTools.showToolbox(target, "styleeditor");
let panel = toolbox.getCurrentPanel();
let host = panel._panelDoc.querySelector(".csscoverage-report");
let templ = panel._panelDoc.querySelector(".csscoverage-template");
templ = templ.cloneNode(true);
templ.hidden = false;
let data = {
pages: csscoveragePageReport.pages,
unusedRules: csscoveragePageReport.unusedRules,
onback: () => {
// The back button clears and hides .csscoverage-report
while (host.hasChildNodes()) {
host.removeChild(host.firstChild);
}
host.hidden = true;
}
};
let addOnClick = rule => {
rule.onclick = () => {
panel.selectStyleSheet(rule.url, rule.start.line);
};
};
data.pages.forEach(page => {
page.preloadRules.forEach(addOnClick);
});
data.unusedRules.forEach(addOnClick);
let options = { allowEval: true, stack: "styleeditor.xul" };
domtemplate.template(templ, data, options);
while (templ.hasChildNodes()) {
host.appendChild(templ.firstChild);
}
host.hidden = false;
}
}
];

View File

@ -0,0 +1,79 @@
/*
* Copyright 2012, Mozilla Foundation and contributors
*
* 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';
var Cc = require('chrome').Cc;
var Ci = require('chrome').Ci;
var Cu = require('chrome').Cu;
var prefSvc = Cc['@mozilla.org/preferences-service;1']
.getService(Ci.nsIPrefService);
var prefBranch = prefSvc.getBranch(null).QueryInterface(Ci.nsIPrefBranch2);
var Services = Cu.import('resource://gre/modules/Services.jsm', {}).Services;
var stringBundle = Services.strings.createBundle(
'chrome://browser/locale/devtools/gclicommands.properties');
/**
* Lookup a string in the GCLI string bundle
*/
exports.lookup = function(name) {
try {
return stringBundle.GetStringFromName(name);
}
catch (ex) {
throw new Error('Failure in lookup(\'' + name + '\')');
}
};
/**
* An alternative to lookup().
* <code>l10n.lookup('BLAH') === l10n.propertyLookup.BLAH</code>
* This is particularly nice for templates because you can pass
* <code>l10n:l10n.propertyLookup</code> in the template data and use it
* like <code>${l10n.BLAH}</code>
*/
exports.propertyLookup = Proxy.create({
get: function(rcvr, name) {
return exports.lookup(name);
}
});
/**
* Lookup a string in the GCLI string bundle
*/
exports.lookupFormat = function(name, swaps) {
try {
return stringBundle.formatStringFromName(name, swaps, swaps.length);
}
catch (ex) {
throw new Error('Failure in lookupFormat(\'' + name + '\')');
}
};
/**
* Allow GCLI users to be hidden by the 'devtools.chrome.enabled' pref.
* Use it in commands like this:
* <pre>
* name: "somecommand",
* hidden: l10n.hiddenByChromePref(),
* exec: function(args, context) { ... }
* </pre>
*/
exports.hiddenByChromePref = function() {
return !prefBranch.prefHasUserValue('devtools.chrome.enabled');
};

View File

@ -0,0 +1,718 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cc, Ci, Cu } = require("chrome");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const Services = require("Services");
const promise = require("resource://gre/modules/Promise.jsm").Promise;
const { getRuleLocation } = require("devtools/server/actors/stylesheets");
const protocol = require("devtools/server/protocol");
const { method, custom, RetVal, Arg } = protocol;
loader.lazyGetter(this, "gDevTools", () => {
return require("resource:///modules/devtools/gDevTools.jsm").gDevTools;
});
loader.lazyGetter(this, "DOMUtils", () => {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils)
});
loader.lazyGetter(this, "prettifyCSS", () => {
return require("resource:///modules/devtools/StyleSheetEditor.jsm").prettifyCSS;
});
const CSSRule = Ci.nsIDOMCSSRule;
const MAX_UNUSED_RULES = 10000;
/**
* Allow: let foo = l10n.lookup("csscoverageFoo");
*/
const l10n = exports.l10n = {
_URI: "chrome://global/locale/devtools/csscoverage.properties",
lookup: function(msg) {
if (this._stringBundle == null) {
this._stringBundle = Services.strings.createBundle(this._URI);
}
return this._stringBundle.GetStringFromName(msg);
}
};
/**
* UsageReport manages the collection of CSS usage data.
* The core of a UsageReport is a JSON-able data structure called _knownRules
* which looks like this:
* This records the CSSStyleRules and their usage.
* The format is:
* Map({
* <CSS-URL>|<START-LINE>|<START-COLUMN>: {
* selectorText: <CSSStyleRule.selectorText>,
* test: <simplify(CSSStyleRule.selectorText)>,
* cssText: <CSSStyleRule.cssText>,
* isUsed: <TRUE|FALSE>,
* presentOn: Set([ <HTML-URL>, ... ]),
* preLoadOn: Set([ <HTML-URL>, ... ]),
* isError: <TRUE|FALSE>,
* }
* })
*
* For example:
* this._knownRules = Map({
* "http://eg.com/styles1.css|15|0": {
* selectorText: "p.quote:hover",
* test: "p.quote",
* cssText: "p.quote { color: red; }",
* isUsed: true,
* presentOn: Set([ "http://eg.com/page1.html", ... ]),
* preLoadOn: Set([ "http://eg.com/page1.html" ]),
* isError: false,
* }, ...
* });
*/
let UsageReportActor = protocol.ActorClass({
typeName: "usageReport",
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this._tabActor = tabActor;
this._running = false;
this._onTabLoad = this._onTabLoad.bind(this);
this._onChange = this._onChange.bind(this);
},
destroy: function() {
this._tabActor = undefined;
delete this._onTabLoad;
delete this._onChange;
protocol.Actor.prototype.destroy.call(this);
},
/**
* Begin recording usage data
*/
start: method(function() {
if (this._running) {
throw new Error(l10n.lookup("csscoverageRunningError"));
}
this._visitedPages = new Set();
this._knownRules = new Map();
this._running = true;
this._tooManyUnused = false;
this._tabActor.browser.addEventListener("load", this._onTabLoad, true);
this._observeMutations(this._tabActor.window.document);
this._populateKnownRules(this._tabActor.window.document);
this._updateUsage(this._tabActor.window.document, false);
}),
/**
* Cease recording usage data
*/
stop: method(function() {
if (!this._running) {
throw new Error(l10n.lookup("csscoverageNotRunningError"));
}
this._tabActor.browser.removeEventListener("load", this._onTabLoad, true);
this._running = false;
}),
/**
* Start/stop recording usage data depending on what we're currently doing.
*/
toggle: method(function() {
return this._running ?
this.stop().then(() => false) :
this.start().then(() => true);
}, {
response: RetVal("boolean"),
}),
/**
* Running start() quickly followed by stop() does a bunch of unnecessary
* work, so this cuts all that out
*/
oneshot: method(function() {
if (this._running) {
throw new Error(l10n.lookup("csscoverageRunningError"));
}
this._visitedPages = new Set();
this._knownRules = new Map();
this._populateKnownRules(this._tabActor.window.document);
this._updateUsage(this._tabActor.window.document, false);
}),
/**
* Called from the tab "load" event
*/
_onTabLoad: function(ev) {
let document = ev.target;
this._populateKnownRules(document);
this._updateUsage(document, true);
this._observeMutations(document);
},
/**
* Setup a MutationObserver on the current document
*/
_observeMutations: function(document) {
let MutationObserver = document.defaultView.MutationObserver;
let observer = new MutationObserver(mutations => {
// It's possible that one of the mutations in this list adds a 'use' of
// a CSS rule, and another takes it away. See Bug 1010189
this._onChange(document);
});
observer.observe(document, {
attributes: true,
childList: true,
characterData: false
});
},
/**
* Event handler for whenever we think the page has changed in a way that
* means the CSS usage might have changed.
*/
_onChange: function(document) {
// Ignore changes pre 'load'
if (!this._visitedPages.has(getURL(document))) {
return;
}
this._updateUsage(document, false);
},
/**
* Called whenever we think the list of stylesheets might have changed so
* we can update the list of rules that we should be checking
*/
_populateKnownRules: function(document) {
let url = getURL(document);
this._visitedPages.add(url);
// Go through all the rules in the current sheets adding them to knownRules
// if needed and adding the current url to the list of pages they're on
for (let rule of getAllSelectorRules(document)) {
let ruleId = ruleToId(rule);
let ruleData = this._knownRules.get(ruleId);
if (ruleData == null) {
ruleData = {
selectorText: rule.selectorText,
cssText: rule.cssText,
test: getTestSelector(rule.selectorText),
isUsed: false,
presentOn: new Set(),
preLoadOn: new Set(),
isError: false
};
this._knownRules.set(ruleId, ruleData);
}
ruleData.presentOn.add(url);
}
},
/**
* Update knownRules with usage information from the current page
*/
_updateUsage: function(document, isLoad) {
let qsaCount = 0;
// Update this._data with matches to say 'used at load time' by sheet X
let url = getURL(document);
for (let [ , ruleData ] of this._knownRules) {
// If it broke before, don't try again selectors don't change
if (ruleData.isError) {
continue;
}
// If it's used somewhere already, don't bother checking again unless
// this is a load event in which case we need to add preLoadOn
if (!isLoad && ruleData.isUsed) {
continue;
}
// Ignore rules that are not present on this page
if (!ruleData.presentOn.has(url)) {
continue;
}
qsaCount++;
if (qsaCount > MAX_UNUSED_RULES) {
console.error("Too many unused rules on " + url + " ");
this._tooManyUnused = true;
continue;
}
try {
let match = document.querySelector(ruleData.test);
if (match != null) {
ruleData.isUsed = true;
if (isLoad) {
ruleData.preLoadOn.add(url);
}
}
}
catch (ex) {
ruleData.isError = true;
}
}
},
/**
* Returns a JSONable structure designed to help marking up the style editor,
* which describes the CSS selector usage.
* Example:
* [
* {
* selectorText: "p#content",
* usage: "unused|used",
* start: { line: 3, column: 0 },
* },
* ...
* ]
*/
createEditorReport: method(function(url) {
if (this._knownRules == null) {
return { reports: [] };
}
let reports = [];
for (let [ruleId, ruleData] of this._knownRules) {
let { url: ruleUrl, line, column } = deconstructRuleId(ruleId);
if (ruleUrl !== url || ruleData.isUsed) {
continue;
}
let ruleReport = {
selectorText: ruleData.selectorText,
start: { line: line, column: column }
};
if (ruleData.end) {
ruleReport.end = ruleData.end;
}
reports.push(ruleReport);
}
return { reports: reports };
}, {
request: { url: Arg(0, "string") },
response: { reports: RetVal("array:json") }
}),
/**
* Returns a JSONable structure designed for the page report which shows
* the recommended changes to a page.
* Example:
* {
* pages: [
* {
* url: http://example.org/page1.html,
* preloadRules: [
* {
* url: "http://example.org/style1.css",
* start: { line: 3, column: 4 },
* selectorText: "p#content",
* formattedCssText: "p#content {\n color: red;\n }\n",
* onclick: function() { // open in style editor }
* },
* ...
* ],
* unusedRules: [
* ...
* ]
* }
* ]
* }
*/
createPageReport: method(function() {
if (this._running) {
throw new Error(l10n.lookup("csscoverageRunningError"));
}
if (this._visitedPages == null) {
throw new Error(l10n.lookup("csscoverageNotRunError"));
}
// Create a JSONable data structure representing a rule
const ruleToRuleReport = function(ruleId, ruleData) {
let { url, line, column } = deconstructRuleId(ruleId);
return {
url: url,
shortHref: url.split("/").slice(-1),
start: { line: line, column: column },
selectorText: ruleData.selectorText,
formattedCssText: prettifyCSS(ruleData.cssText)
};
}
let pages = [];
let unusedRules = [];
// Create a set of the unused rules
for (let [ruleId, ruleData] of this._knownRules) {
if (!ruleData.isUsed) {
let ruleReport = ruleToRuleReport(ruleId, ruleData);
unusedRules.push(ruleReport);
}
}
// Create the set of rules that could be pre-loaded
for (let url of this._visitedPages) {
let page = {
url: url,
shortHref: url.split("/").slice(-1),
preloadRules: []
};
for (let [ruleId, ruleData] of this._knownRules) {
if (ruleData.preLoadOn.has(url)) {
let ruleReport = ruleToRuleReport(ruleId, ruleData);
page.preloadRules.push(ruleReport);
}
}
if (page.preloadRules.length > 0) {
pages.push(page);
}
}
return {
pages: pages,
unusedRules: unusedRules
};
}, {
response: RetVal("json")
}),
/**
* For testing only. Is css coverage running.
*/
_testOnly_isRunning: method(function() {
return this._running;
}, {
response: { value: RetVal("boolean")}
}),
});
exports.UsageReportActor = UsageReportActor;
/**
* Generator that filters the CSSRules out of _getAllRules so it only
* iterates over the CSSStyleRules
*/
function* getAllSelectorRules(document) {
for (let rule of getAllRules(document)) {
if (rule.type === CSSRule.STYLE_RULE && rule.selectorText !== "") {
yield rule;
}
}
}
/**
* Generator to iterate over the CSSRules in all the stylesheets the
* current document (i.e. it includes import rules, media rules, etc)
*/
function* getAllRules(document) {
// sheets is an array of the <link> and <style> element in this document
let sheets = getAllSheets(document);
for (let i = 0; i < sheets.length; i++) {
for (let j = 0; j < sheets[i].cssRules.length; j++) {
yield sheets[i].cssRules[j];
}
}
}
/**
* Get an array of all the stylesheets that affect this document. That means
* the <link> and <style> based sheets, and the @imported sheets (recursively)
* but not the sheets in nested frames.
*/
function getAllSheets(document) {
// sheets is an array of the <link> and <style> element in this document
let sheets = Array.slice(document.styleSheets);
// Add @imported sheets
for (let i = 0; i < sheets.length; i++) {
let subSheets = getImportedSheets(sheets[i]);
sheets = sheets.concat(...subSheets);
}
return sheets;
}
/**
* Recursively find @import rules in the given stylesheet.
* We're relying on the browser giving rule.styleSheet == null to resolve
* @import loops
*/
function getImportedSheets(stylesheet) {
let sheets = [];
for (let i = 0; i < stylesheet.cssRules.length; i++) {
let rule = stylesheet.cssRules[i];
// rule.styleSheet == null with duplicate @imports for the same URL.
if (rule.type === CSSRule.IMPORT_RULE && rule.styleSheet != null) {
sheets.push(rule.styleSheet);
let subSheets = getImportedSheets(rule.styleSheet);
sheets = sheets.concat(...subSheets);
}
}
return sheets;
}
/**
* Get a unique identifier for a rule. This is currently the string
* <CSS-URL>|<START-LINE>|<START-COLUMN>
* @see deconstructRuleId(ruleId)
*/
function ruleToId(rule) {
let loc = getRuleLocation(rule);
return sheetToUrl(rule.parentStyleSheet) + "|" + loc.line + "|" + loc.column;
}
/**
* Convert a ruleId to an object with { url, line, column } properties
* @see ruleToId(rule)
*/
const deconstructRuleId = exports.deconstructRuleId = function(ruleId) {
let split = ruleId.split("|");
if (split.length > 3) {
let replace = split.slice(0, split.length - 3 + 1).join("|");
split.splice(0, split.length - 3 + 1, replace);
}
let [ url, line, column ] = split;
return {
url: url,
line: parseInt(line, 10),
column: parseInt(column, 10)
};
};
/**
* We're only interested in the origin and pathname, because changes to the
* username, password, hash, or query string probably don't significantly
* change the CSS usage properties of a page.
* @param document
*/
const getURL = exports.getURL = function(document) {
let url = new document.defaultView.URL(document.documentURI);
return '' + url.origin + url.pathname;
};
/**
* Pseudo class handling constants:
* We split pseudo-classes into a number of categories so we can decide how we
* should match them. See getTestSelector for how we use these constants.
*
* @see http://dev.w3.org/csswg/selectors4/#overview
* @see https://developer.mozilla.org/en-US/docs/tag/CSS%20Pseudo-class
* @see https://developer.mozilla.org/en-US/docs/Web/CSS/Pseudo-elements
*/
/**
* Category 1: Pseudo-classes that depend on external browser/OS state
* This includes things like the time, locale, position of mouse/caret/window,
* contents of browser history, etc. These can be hard to mimic.
* Action: Remove from selectors
*/
const SEL_EXTERNAL = [
"active", "active-drop", "current", "dir", "focus", "future", "hover",
"invalid-drop", "lang", "past", "placeholder-shown", "target", "valid-drop",
"visited"
];
/**
* Category 2: Pseudo-classes that depend on user-input state
* These are pseudo-classes that arguably *should* be covered by unit tests but
* which probably aren't and which are unlikely to be covered by manual tests.
* We're currently stripping them out,
* Action: Remove from selectors (but consider future command line flag to
* enable them in the future. e.g. 'csscoverage start --strict')
*/
const SEL_FORM = [
"checked", "default", "disabled", "enabled", "fullscreen", "in-range",
"indeterminate", "invalid", "optional", "out-of-range", "required", "valid"
];
/**
* Category 3: Pseudo-elements
* querySelectorAll doesn't return matches with pseudo-elements because there
* is no element to match (they're pseudo) so we have to remove them all.
* (See http://codepen.io/joewalker/pen/sanDw for a demo)
* Action: Remove from selectors (including deprecated single colon versions)
*/
const SEL_ELEMENT = [
"after", "before", "first-letter", "first-line", "selection"
];
/**
* Category 4: Structural pseudo-classes
* This is a category defined by the spec (also called tree-structural and
* grid-structural) for selection based on relative position in the document
* tree that cannot be represented by other simple selectors or combinators.
* Action: Require a page-match
*/
const SEL_STRUCTURAL = [
"empty", "first-child", "first-of-type", "last-child", "last-of-type",
"nth-column", "nth-last-column", "nth-child", "nth-last-child",
"nth-last-of-type", "nth-of-type", "only-child", "only-of-type", "root"
];
/**
* Category 4a: Semi-structural pseudo-classes
* These are not structural according to the spec, but act nevertheless on
* information in the document tree.
* Action: Require a page-match
*/
const SEL_SEMI = [ "any-link", "link", "read-only", "read-write", "scope" ];
/**
* Category 5: Combining pseudo-classes
* has(), not() etc join selectors together in various ways. We take care when
* removing pseudo-classes to convert "not(:hover)" into "not(*)" and so on.
* With these changes the combining pseudo-classes should probably stand on
* their own.
* Action: Require a page-match
*/
const SEL_COMBINING = [ "not", "has", "matches" ];
/**
* Category 6: Media pseudo-classes
* Pseudo-classes that should be ignored because they're only relevant to
* media queries
* Action: Don't need removing from selectors as they appear in media queries
*/
const SEL_MEDIA = [ "blank", "first", "left", "right" ];
/**
* A test selector is a reduced form of a selector that we actually test
* against. This code strips out pseudo-elements and some pseudo-classes that
* we think should not have to match in order for the selector to be relevant.
*/
function getTestSelector(selector) {
let replaceSelector = pseudo => {
selector = selector.replace(" :" + selector, " *")
.replace("(:" + selector, "(*")
.replace(":" + selector, "");
};
SEL_EXTERNAL.forEach(replaceSelector);
SEL_FORM.forEach(replaceSelector);
SEL_ELEMENT.forEach(replaceSelector);
// Pseudo elements work in : and :: forms
SEL_ELEMENT.forEach(pseudo => {
selector = selector.replace("::" + selector, "");
});
return selector;
}
/**
* I've documented all known pseudo-classes above for 2 reasons: To allow
* checking logic and what might be missing, but also to allow a unit test
* that fetches the list of supported pseudo-classes and pseudo-elements from
* the platform and check that they were all represented here.
*/
exports.SEL_ALL = [
SEL_EXTERNAL, SEL_FORM, SEL_ELEMENT, SEL_STRUCTURAL, SEL_SEMI,
SEL_COMBINING, SEL_MEDIA
].reduce(function(prev, curr) { return prev.concat(curr); }, []);
/**
* Find a URL for a given stylesheet
*/
const sheetToUrl = exports.sheetToUrl = function(stylesheet) {
if (stylesheet.href) {
return stylesheet.href;
}
if (stylesheet.ownerNode && stylesheet.ownerNode.baseURI) {
return stylesheet.ownerNode.baseURI;
}
throw new Error("Unknown sheet source");
}
/**
* Front for UsageReportActor
*/
const UsageReportFront = protocol.FrontClass(UsageReportActor, {
initialize: function(client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
this.actorID = form.usageReportActor;
this.manage(this);
},
start: custom(function(chromeWindow, target) {
if (chromeWindow != null) {
let gnb = chromeWindow.document.getElementById("global-notificationbox");
let notification = gnb.getNotificationWithValue("csscoverage-running");
if (!notification) {
let notifyStop = ev => {
if (ev == "removed") {
this.stop();
gDevTools.showToolbox(target, "styleeditor");
}
};
gnb.appendNotification(l10n.lookup("csscoverageRunningReply"),
"csscoverage-running",
"", // i.e. no image
gnb.PRIORITY_INFO_HIGH,
null, // i.e. no buttons
notifyStop);
}
}
return this._start();
}, {
impl: "_start"
})
});
exports.UsageReportFront = UsageReportFront;
/**
* Registration / De-registration
*/
exports.register = function(handle) {
handle.addGlobalActor(UsageReportActor, "usageReportActor");
handle.addTabActor(UsageReportActor, "usageReportActor");
};
exports.unregister = function(handle) {
handle.removeGlobalActor(UsageReportActor, "usageReportActor");
handle.removeTabActor(UsageReportActor, "usageReportActor");
};
const knownFronts = new WeakMap();
/**
* Create a UsageReportFront only when needed (returns a promise)
*/
const getUsage = exports.getUsage = function(target) {
return target.makeRemote().then(() => {
let front = knownFronts.get(target.client)
if (front == null) {
front = new UsageReportFront(target.client, target.form);
knownFronts.set(target.client, front);
}
return front;
});
};

View File

@ -778,6 +778,53 @@ let StyleSheetActor = protocol.ActorClass({
}
})
/**
* Find the line/column for a rule.
* This is like DOMUtils.getRule[Line|Column] except for inline <style> sheets,
* the line number returned here is relative to the <style> tag rather than the
* containing HTML document (which is what DOMUtils does).
* This is hacky, but we don't know of a better implementation right now.
*/
const getRuleLocation = exports.getRuleLocation = function(rule) {
let reply = {
line: DOMUtils.getRuleLine(rule),
column: DOMUtils.getRuleColumn(rule)
};
let sheet = rule.parentStyleSheet;
if (sheet.ownerNode && sheet.ownerNode.localName === "style") {
// For inline sheets, the line is relative to HTML not the stylesheet, so
// Get the location of the first { to know the line num of the first rule,
// relative to this sheet, to get the offset
let text = sheet.ownerNode.textContent;
// Hacky for now, because this will fail if { appears in a comment before
// but better than nothing, and faster than parsing the whole text
let start = text.substring(0, text.indexOf("{"));
let relativeStartLine = start.split("\n").length;
let absoluteStartLine;
let i = 0;
while (absoluteStartLine == null) {
let irule = sheet.cssRules[i];
if (irule instanceof Ci.nsIDOMCSSStyleRule) {
absoluteStartLine = DOMUtils.getRuleLine(irule);
}
else if (irule == null) {
break;
}
i++;
}
if (absoluteStartLine != null) {
let offset = absoluteStartLine - relativeStartLine;
reply.line -= offset;
}
}
return reply;
};
/**
* StyleSheetFront is the client-side counterpart to a StyleSheetActor.
*/

View File

@ -398,6 +398,7 @@ var DebuggerServer = {
this.registerModule("devtools/server/actors/framerate");
this.registerModule("devtools/server/actors/eventlooplag");
this.registerModule("devtools/server/actors/layout");
this.registerModule("devtools/server/actors/csscoverage");
if ("nsIProfiler" in Ci) {
this.addActors("resource://gre/modules/devtools/server/actors/profiler.js");
}

View File

@ -0,0 +1,46 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- LOCALIZATION NOTE : FILE This file contains the CSS Coverage Report
- strings. See the 'csscoverage' command for more information, and
- browser/devtools/styleeditor/styleeditor.xul for context -->
<!-- LOCALIZATION NOTE : FILE The correct localization of this file might be to
- keep it in English, or another language commonly spoken among web developers.
- You want to make that choice consistent across the developer tools.
- A good criteria is the language in which you'd find the best
- documentation on web development on the web. -->
<!-- LOCALIZATION NOTE (csscoverage.backButton):
- Text on the button to go back to the main style editor -->
<!ENTITY csscoverage.backButton "Back">
<!-- LOCALIZATION NOTE (csscoverage.unused, csscoverage.noMatch):
- This is the heading and body text for the CSS usage part of the report -->
<!ENTITY csscoverage.unused "Unused Rules">
<!ENTITY csscoverage.noMatch "The following selectors did not match any elements that we saw during the test so it's possible they can be safely removed.">
<!-- LOCALIZATION NOTE (csscoverage.optimize):
- This is the heading for the CSS optimization part of the report -->
<!ENTITY csscoverage.optimize "Opimizable Pages">
<!-- LOCALIZATION NOTE (csscoverage.preload1, csscoverage.preload2,
- csscoverage.preload3): These 3 are part of a paragraph with 1 and 2
- separated by a styled <link> tag and 2 and 3 separated by a styled
- <style> tag -->
<!ENTITY csscoverage.preload1 "You can sometimes speed up loading by moving all">
<!ENTITY csscoverage.preload2 "tags to the bottom of the page (where they don't stop the page loading) and creating a new inline">
<!ENTITY csscoverage.preload3 "element containing the following rules at the top of the page:">
<!-- LOCALIZATION NOTE (csscoverage.noPreload):
- This is what we say when we have no optimization suggestions -->
<!ENTITY csscoverage.noPreload "All rules are inlined. We don't have suggestions about how to improve performance on this page right now.">
<!-- LOCALIZATION NOTE (csscoverage.footer1, csscoverage.footer2, csscoverage.footer3):
- The text displayed at the bottom of the page, with 2 being the URL opened
- when the link text in 3 is clicked -->
<!ENTITY csscoverage.footer1 "See">
<!ENTITY csscoverage.footer2 "https://developer.mozilla.org/en-US/docs/Tools/CSS_Coverage">
<!ENTITY csscoverage.footer3 "the MDN article on the CSS Coverage Tool">
<!ENTITY csscoverage.footer4 "for caveats in the generation of this report.">

View File

@ -0,0 +1,28 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# LOCALIZATION NOTE These strings are used in the 'csscoverage' command and in
# the user interface that this command creates.
# LOCALIZATION NOTE (csscoverageDesc, csscoverageStartDesc, csscoverageStopDesc,
# csscoverageOneShotDesc, csscoverageToggleDesc, csscoverageReportDesc): Short
# descriptions of the csscoverage commands
csscoverageDesc=Control CSS coverage analysis
csscoverageStartDesc=Begin collecting CSS coverage analysis
csscoverageStopDesc=Stop collecting CSS coverage analysis
csscoverageOneShotDesc=Stop collecting CSS coverage analysis
csscoverageToggleDesc=Toggle collecting CSS coverage analysis
csscoverageReportDesc=Show CSS coverage analysis report
# LOCALIZATION NOTE (csscoverageRunningReply, csscoverageDoneReply): Text that
# describes the current state of the css coverage system
csscoverageRunningReply=Running CSS coverage analysis
csscoverageDoneReply=CSS Coverage analysis completed
# LOCALIZATION NOTE (csscoverageRunningError, csscoverageNotRunningError,
# csscoverageNotRunError): Error message that describe things that can go wrong
# with the css coverage system
csscoverageRunningError=CSS coverage analysis already running
csscoverageNotRunningError=CSS coverage analysis not running
csscoverageNotRunError=CSS coverage analysis has not been run

View File

@ -31,6 +31,8 @@
locale/@AB_CD@/global/customizeToolbar.properties (%chrome/global/customizeToolbar.properties)
locale/@AB_CD@/global/datetimepicker.dtd (%chrome/global/datetimepicker.dtd)
locale/@AB_CD@/global/dateFormat.properties (%chrome/global/dateFormat.properties)
locale/@AB_CD@/global/devtools/csscoverage.properties (%chrome/global/devtools/csscoverage.properties)
locale/@AB_CD@/global/devtools/csscoverage.dtd (%chrome/global/devtools/csscoverage.dtd)
locale/@AB_CD@/global/devtools/debugger.properties (%chrome/global/devtools/debugger.properties)
locale/@AB_CD@/global/devtools/styleinspector.properties (%chrome/global/devtools/styleinspector.properties)
locale/@AB_CD@/global/dialogOverlay.dtd (%chrome/global/dialogOverlay.dtd)