mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-01-26 23:23:33 +00:00
Bug 975522 - Add CSS coverage commands; r=harth
This commit is contained in:
parent
2f97ac1d46
commit
d265e4d588
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
@ -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");
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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><link ...></code>
|
||||
&csscoverage.preload2;
|
||||
<code><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><style>
|
||||
<loop foreach="rule in ${page.preloadRules}"
|
||||
onclick="${rule.onclick}">${rule.formattedCssText}</loop></style></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
&csscoverage.footer1;
|
||||
<a target="_blank" href="&csscoverage.footer2;">&csscoverage.footer3;</a>
|
||||
&csscoverage.footer4;
|
||||
</p>
|
||||
<p> </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>
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
136
toolkit/devtools/gcli/commands/csscoverage.js
Normal file
136
toolkit/devtools/gcli/commands/csscoverage.js
Normal 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;
|
||||
}
|
||||
}
|
||||
];
|
79
toolkit/devtools/gcli/source/lib/gcli/l10n.js
Normal file
79
toolkit/devtools/gcli/source/lib/gcli/l10n.js
Normal 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');
|
||||
};
|
718
toolkit/devtools/server/actors/csscoverage.js
Normal file
718
toolkit/devtools/server/actors/csscoverage.js
Normal 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;
|
||||
});
|
||||
};
|
@ -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.
|
||||
*/
|
||||
|
@ -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");
|
||||
}
|
||||
|
46
toolkit/locales/en-US/chrome/global/devtools/csscoverage.dtd
Normal file
46
toolkit/locales/en-US/chrome/global/devtools/csscoverage.dtd
Normal 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.">
|
@ -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
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user