Skip to content
This repository was archived by the owner on Jul 23, 2024. It is now read-only.

Commit 9499fd5

Browse files
authored
Merge pull request #22 from firefox-devtools/issue-grouping
Issue grouping
2 parents 20f9801 + 2e115c8 commit 9499fd5

8 files changed

Lines changed: 287 additions & 11 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"use strict";
2+
3+
// We can not call any experimental APIs in devtools page context.
4+
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/docs/basics.rst#155-156
5+
// Thus, introduce api-server / api-client that provides pseudo api to make experimental
6+
// api accessible even in devtools page context.
7+
//
8+
// This script should run on devtools page context.
9+
10+
(async function install() {
11+
if (browser.experiments && browser.experiments.highlighter) {
12+
return;
13+
}
14+
15+
if (!browser.experiments) {
16+
browser.experiments = {};
17+
}
18+
19+
const port = browser.runtime.connect();
20+
21+
browser.experiments.highlighter = {
22+
async highlight(selector) {
23+
this._invoke("highlight", selector);
24+
},
25+
26+
async _invoke(method, parameter) {
27+
return new Promise(resolve => {
28+
const timestamp = Date.now();
29+
30+
const listener = response => {
31+
if (method === response.method && timestamp === response.timestamp) {
32+
port.onMessage.removeListener(listener);
33+
resolve(response.result);
34+
}
35+
};
36+
37+
port.onMessage.addListener(listener);
38+
port.postMessage({
39+
namespace: "browser.experiments.highlighter",
40+
method,
41+
parameter,
42+
timestamp,
43+
});
44+
});
45+
}
46+
};
47+
})();
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use strict";
2+
3+
// We can not call any experimental APIs in devtools page context.
4+
// https://searchfox.org/mozilla-central/source/toolkit/components/extensions/docs/basics.rst#155-156
5+
// Thus, introduce api-server / api-client that provides pseudo api to make experimental
6+
// api accessible even in devtools page context.
7+
//
8+
// This script should run on addon parent context (as background).
9+
10+
browser.runtime.onConnect.addListener(port => {
11+
const onMessage = async ({ namespace, method, parameter, timestamp }) => {
12+
if (namespace !== "browser.experiments.highlighter") {
13+
return;
14+
}
15+
16+
const result = await browser.experiments.highlighter[method](parameter);
17+
port.postMessage({ method, timestamp, result });
18+
};
19+
port.onMessage.addListener(onMessage);
20+
21+
const onDisconnect = () => {
22+
browser.experiments.inspectedNode.onChange.removeListener(onChange);
23+
port.onDisconnect.removeListener(onDisconnect);
24+
port.onMessage.removeListener(onMessage);
25+
};
26+
port.onDisconnect.addListener(onDisconnect);
27+
});
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use strict";
2+
3+
this.highlighter = class extends ExtensionAPI {
4+
getAPI(context) {
5+
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
6+
const { require } = Cu.import("resource://devtools/shared/Loader.jsm");
7+
const { gDevTools } = require("devtools/client/framework/devtools");
8+
9+
async function _highlight(selector) {
10+
const navigator = Services.wm.getMostRecentWindow("navigator:browser");
11+
const tab = navigator.gBrowser.selectedTab;
12+
const target = await gDevTools.getTargetForTab(tab);
13+
const toolbox = gDevTools.getToolbox(target);
14+
const inspector = toolbox.getPanel("inspector");
15+
inspector.searchBox.value = selector;
16+
inspector.search.doFullTextSearch(selector, false);
17+
}
18+
19+
return {
20+
experiments: {
21+
highlighter: {
22+
async highlight(selector) {
23+
await _highlight(selector);
24+
}
25+
},
26+
},
27+
};
28+
}
29+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[
2+
{
3+
"namespace": "experiments.highlighter",
4+
"functions": [
5+
{
6+
"name": "highlight",
7+
"type": "function",
8+
"parameters": [
9+
{
10+
"type": "string",
11+
"name": "selector"
12+
}
13+
],
14+
"async": true
15+
}
16+
]
17+
}
18+
]

extension/experiments/inspectedNode/api.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ this.inspectedNode = class extends ExtensionAPI {
6666
const { actorID: ruleId } = rule;
6767
let { declarations } = rule;
6868
declarations = declarations.filter(d => !d.commentOffsets);
69-
return declarations.length ? { ruleId, declarations } : null;
69+
return declarations.length
70+
? { ruleId, declarations, node: _getNodeInfo(node) }
71+
: null;
7072
}).filter(rule => !!rule);
7173
};
7274

@@ -98,8 +100,23 @@ this.inspectedNode = class extends ExtensionAPI {
98100
};
99101

100102
const _getNodeInfo = node => {
101-
const { attributes, nodeName, nodeType, customElementLocation } = node;
102-
return { attributes, nodeName, nodeType, isCustomElement: !!customElementLocation };
103+
const {
104+
id,
105+
className,
106+
attributes,
107+
nodeName,
108+
nodeType,
109+
customElementLocation,
110+
} = node;
111+
112+
return {
113+
id,
114+
className: className.trim(),
115+
attributes,
116+
nodeName,
117+
nodeType,
118+
isCustomElement: !!customElementLocation,
119+
};
103120
};
104121

105122
const _getNode = async (clientId) => {

extension/manifest.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,21 @@
1313
],
1414
"background": {
1515
"scripts": [
16+
"experiments/highlighter/api-server.js",
1617
"experiments/inspectedNode/api-server.js",
1718
"proxy/tabs/api-server.js"
1819
]
1920
},
2021
"devtools_page": "devtools.html",
2122
"experiment_apis": {
23+
"highlighter": {
24+
"schema": "experiments/highlighter/schema.json",
25+
"parent": {
26+
"scopes": ["addon_parent"],
27+
"script": "experiments/highlighter/api.js",
28+
"paths": [["experiments", "highlighter"]]
29+
}
30+
},
2231
"inspectedNode": {
2332
"schema": "experiments/inspectedNode/schema.json",
2433
"parent": {

extension/sidebar-pane.html

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
line-height: 1.8;
1616
}
1717

18+
a {
19+
text-decoration: none;
20+
}
21+
1822
header {
1923
color: #0c0c0d;
2024
background-color: #f9f9fa;
@@ -27,10 +31,6 @@
2731
border-top: 1px solid #e0e0e2;
2832
}
2933

30-
a {
31-
text-decoration: none;
32-
}
33-
3434
li {
3535
margin-block-end: 4px;
3636
list-style: none;
@@ -128,6 +128,33 @@
128128
font-weight: 600;
129129
}
130130

131+
/* occurrences part */
132+
.occurrences details summary {
133+
cursor: pointer;
134+
color: #b6babf;
135+
}
136+
137+
.occurrences ul {
138+
line-height: 1.2;
139+
padding-inline-start: 16px;
140+
padding-block-start: 2px;
141+
}
142+
143+
.occurrences li {
144+
font-family: monospace;
145+
font-size: 12px;
146+
cursor: pointer;
147+
}
148+
149+
.occurrences .node-name {
150+
color: #0074e8;
151+
}
152+
153+
.occurrences .node-id,
154+
.occurrences .node-class {
155+
color: #dd00a9;
156+
}
157+
131158
#subtree aside {
132159
display: none;
133160
padding-block: 12px;
@@ -166,6 +193,7 @@
166193
to { transform: rotate(1turn); }
167194
}
168195
</style>
196+
<script src="experiments/highlighter/api-client.js"></script>
169197
<script src="experiments/inspectedNode/api-client.js"></script>
170198
<script src="proxy/tabs/api-client.js"></script>
171199
<script type="module" src="sidebar-pane.js"></script>

extension/sidebar-pane.js

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,24 +62,80 @@ async function _updateSubtree(selectedNode) {
6262

6363
const { attributes, nodeName } = node;
6464
issues.push(
65-
..._webcompat.getHTMLElementIssues(nodeName, attributes, _targetBrowsers));
65+
...
66+
_webcompat.getHTMLElementIssues(nodeName, attributes, _targetBrowsers)
67+
.map(issue => {
68+
issue.node = node;
69+
return issue;
70+
})
71+
)
6672
}
6773

6874
progressEl.textContent = "Getting all descendants of the selected node";
6975
const declarationBlocks = await browser.experiments.inspectedNode.getStylesInSubtree();
7076

7177
progressEl.textContent = "Getting web compatibility issues for CSS styles";
72-
for (const { declarations } of declarationBlocks) {
78+
for (const { node, declarations } of declarationBlocks) {
7379
issues.push(
74-
..._webcompat.getCSSDeclarationBlockIssues(declarations, _targetBrowsers));
80+
...
81+
_webcompat.getCSSDeclarationBlockIssues(declarations, _targetBrowsers)
82+
.map(issue => {
83+
issue.node = node;
84+
return issue;
85+
})
86+
)
7587
}
7688

89+
progressEl.textContent = "Grouping all issues";
90+
const issueGroups = _groupIssues(issues);
91+
7792
progressEl.textContent = "Rendering all issues";
78-
_render(issues, issueListEl);
93+
_render(issueGroups, issueListEl);
7994

8095
subtreeEl.classList.remove("processing");
8196
}
8297

98+
/**
99+
* Group by the issue cause.
100+
* @param {Array} issues
101+
* The issue list which WebCompat library returns. Also the issue in the list
102+
* assume to contain the node information additionaly.
103+
* @return {Array}
104+
* Array of issues grouped. The issue has `nodes` attribute which contains the
105+
* node informations where caused the issue.
106+
*/
107+
function _groupIssues(issues) {
108+
const issueGroups = [];
109+
110+
for (const issue of issues) {
111+
let issueGroup = issueGroups.find(i => {
112+
return i.type === issue.type &&
113+
i.property === issue.property &&
114+
i.element === issue.element &&
115+
i.attribute === issue.attribute &&
116+
i.value === issue.value;
117+
});
118+
119+
if (!issueGroup) {
120+
issueGroup = Object.assign({}, issue, { nodes: [], node: undefined });
121+
issueGroups.push(issueGroup);
122+
}
123+
124+
const isNodeContainedInGroup = issueGroup.nodes.some(n => {
125+
return n.nodeName === issue.node.nodeName &&
126+
n.nodeType === issue.node.nodeType &&
127+
n.id === issue.node.id &&
128+
n.className === issue.node.className;
129+
});
130+
131+
if (!isNodeContainedInGroup) {
132+
issueGroup.nodes.push(issue.node);
133+
}
134+
}
135+
136+
return issueGroups;
137+
}
138+
83139
function _isValidElement({ nodeType, isCustomElement }) {
84140
return nodeType === Node.ELEMENT_NODE && !isCustomElement;
85141
}
@@ -107,9 +163,47 @@ function _renderIssue(issue) {
107163

108164
issueEl.classList.add((issue.deprecated ? "warning" : "information"));
109165

166+
if (issue.nodes) {
167+
issueEl.append(_renderOccurrences(issue));
168+
}
169+
110170
return issueEl;
111171
}
112172

173+
function _renderOccurrences({ nodes }) {
174+
const occurrencesEl = document.createElement("section");
175+
occurrencesEl.classList.add("occurrences");
176+
177+
const nodelistEl = document.createElement("ul");
178+
for (const { id, className, nodeName } of nodes) {
179+
const nodeEl = document.createElement("li");
180+
nodeEl.append(_renderTerm(nodeName.toLowerCase(), ["node-name"]));
181+
182+
if (id) {
183+
nodeEl.append(_renderTerm(`#${ id }`, ["node-id"]));
184+
} else if (className.length) {
185+
nodeEl.append(_renderTerm(`.${ className.replace(/\s+/g, ".") }`, ["node-class"]));
186+
}
187+
188+
nodeEl.addEventListener("click", _onClickNodeSelector);
189+
nodelistEl.append(nodeEl);
190+
}
191+
192+
if (nodes.length !== 1) {
193+
const summaryEl = document.createElement("summary");
194+
summaryEl.textContent = `${nodes.length} occurrences`;
195+
196+
const detailsEl = document.createElement("details");
197+
detailsEl.append(summaryEl, nodelistEl);
198+
199+
occurrencesEl.append(detailsEl);
200+
} else {
201+
occurrencesEl.append(nodelistEl);
202+
}
203+
204+
return occurrencesEl;
205+
}
206+
113207
function _renderSubject(issue) {
114208
const { type, url } = issue;
115209
const subjectEl = document.createElement("span");
@@ -283,6 +377,13 @@ function _onClickLink(e) {
283377
browser.tabs.create({ url: e.target.href });
284378
}
285379

380+
function _onClickNodeSelector(e) {
381+
e.stopPropagation();
382+
e.preventDefault();
383+
const selector = e.target.closest("li").textContent;
384+
browser.experiments.highlighter.highlight(selector);
385+
}
386+
286387
async function _updateCSSValueEnabled() {
287388
const isCSSValueEnabled = await _userSettings.isCSSValueEnabled();
288389
_webcompat.setCSSValueEnabled(isCSSValueEnabled);

0 commit comments

Comments
 (0)