Skip to content
Permalink
Browse files
Web Inspector: Elements Tab: create an Event badge for nodes that hav…
…e event listeners

https://bugs.webkit.org/show_bug.cgi?id=244124

Reviewed by Patrick Angle.

Only show "Event" badges for nodes that directly have event listeners (i.e. no ancestors). This will
give developers a visual way of seeing where an event could be handled in a particular ancestor path
as they'd be able to scan for "Event" badges to find every ancestor that has an event listener.

Clicking the "Event" badge will show a popover containing details about the attached event listeners,
such as capturing/bubbling, once, enabled/disabled, and breakpoint, all grouped by event type.

* Source/JavaScriptCore/inspector/protocol/CSS.json:
Add an `"event"` enum value to `CSS.LayoutFlag`.

* Source/WebCore/inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::didAddEventListenerImpl):
(WebCore::InspectorInstrumentation::willRemoveEventListenerImpl):
Also inform the `InspectorCSSAgent` that an event listener was added/removed.

* Source/WebCore/inspector/agents/InspectorCSSAgent.h:
* Source/WebCore/inspector/agents/InspectorCSSAgent.cpp:
(WebCore::InspectorCSSAgent::reset):
(WebCore::layoutFlagContextType):
(WebCore::layoutFlagsContainLayoutContextType): Added.
(WebCore::hasJSEventListener):
(WebCore::InspectorCSSAgent::layoutFlagsForNode):
(WebCore::toProtocol): Added.
(WebCore::InspectorCSSAgent::protocolLayoutFlagsForNode): Added.
(WebCore::pushChildrenNodesToFrontendIfLayoutFlagIsRelevant):
(WebCore::InspectorCSSAgent::didChangeRendererForDOMNode):
(WebCore::InspectorCSSAgent::didAddEventListener): Added.
(WebCore::InspectorCSSAgent::willRemoveEventListener): Added.
(WebCore::InspectorCSSAgent::nodeHasLayoutFlagsChange): Added.
(WebCore::InspectorCSSAgent::nodesWithPendingLayoutFlagsChangeDispatchTimerFired):
(WebCore::layoutFlagContextTypeForRenderer): Deleted.
Keep track of the layout flags for each `Node` to avoid re-dispatching the same flags to the
frontend. This is especially important for event listeners as they can be very noisy (211169@main).

* Source/JavaScriptCore/inspector/protocol/DOM.json:
* Source/WebCore/inspector/agents/InspectorDOMAgent.h:
* Source/WebCore/inspector/agents/InspectorDOMAgent.cpp:
(WebCore::InspectorDOMAgent::getEventListenersForNode):
(WebCore::InspectorDOMAgent::buildObjectForNode):
Allow `DOM.getEventListenersForNode` to control whether ancestor event listeners are included.

* Source/WebInspectorUI/UserInterface/Models/DOMNode.js:
(WI.DOMNode.prototype.set layoutFlags):
(WI.DOMNode.prototype.getEventListeners):
* Source/WebInspectorUI/UserInterface/Views/DOMTreeContentView.js:
(WI.DOMTreeContentView.prototype._populateConfigureDOMTreeBadgesNavigationItemContextMenu):
* Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.js:
(WI.DOMTreeElement.prototype._createBadge):
(WI.DOMTreeElement.prototype._createBadges):
(WI.DOMTreeElement.prototype.async _handleEventBadgeClicked): Added.
(WI.DOMTreeElement.prototype._handleBadgeDoubleClicked): Renamed from `_layoutBadgeDoubleClicked`.
* Source/WebInspectorUI/UserInterface/Views/DOMTreeElement.css:
(.event-badge-popover-content): Added.
(.event-badge-popover-content > .details-section.event-listeners): Added.
(.event-badge-popover-content > .details-section.event-listeners .details-section): Added.
Add logic for creating an "Event" badge that shows the list of event listeners directly attached to
that node (i.e. no ancestors).

* Source/WebInspectorUI/UserInterface/Views/EventListenerSectionGroup.js:
(WI.EventListenerSectionGroup.groupIntoSectionsByEvent): Added.
(WI.EventListenerSectionGroup.groupIntoSectionsByTarget): Added.
(WI.EventListenerSectionGroup._createEventListenerSection): Added.
* Source/WebInspectorUI/UserInterface/Views/EventListenerSectionGroup.css:
(.event-listener-section > .header > .event-listener-options): Added.
(.event-listener-section:hover > .header > .event-listener-options): Added.
* Source/WebInspectorUI/UserInterface/Views/DOMNodeDetailsSidebarPanel.js:
(WI.DOMNodeDetailsSidebarPanel.prototype.async _refreshEventListeners):
(WI.DOMNodeDetailsSidebarPanel.prototype._refreshEventListeners.createEventListenerSection): Deleted.
(WI.DOMNodeDetailsSidebarPanel.prototype._refreshEventListeners.generateGroupsByEvent): Deleted.
(WI.DOMNodeDetailsSidebarPanel.prototype._refreshEventListeners.generateGroupsByTarget): Deleted.
(WI.DOMNodeDetailsSidebarPanel.prototype._refreshEventListeners.eventListenersCallback): Deleted.
* Source/WebInspectorUI/UserInterface/Views/DOMNodeDetailsSidebarPanel.css:
(.sidebar > .panel.dom-node-details .details-section.dom-node-event-listeners .details-section.event-listener-section > .header > .event-listener-options): Deleted.
(.sidebar > .panel.dom-node-details .details-section.dom-node-event-listeners .details-section.event-listener-section:hover > .header > .event-listener-options): Deleted.
Move the code that groups `WI.EventListenerSectionGroup` to be `static` methods on `WI.EventListenerSectionGroup`
so that it can be reused in other places (e.g. inside the `WI.Popover` for the "Event" badge).

* Source/WebInspectorUI/UserInterface/Views/Popover.js:
(WI.Popover.prototype.present):
Add an option to also update the sizing of the `WI.Popover` when re-presenting the same content.
This is necessary because the `WI.Popover` for the "Event" badge is limited in height to the size of
Web Inspector, so resizing Web Inspector should also resize the popover (not just reposition).

* Source/WebInspectorUI/UserInterface/Views/DetailsSection.js:
(WI.DetailsSection):
* Source/WebInspectorUI/UserInterface/Views/DetailsSection.css:
(.details-section .details-section > .header):
(.details-section:has(> .header > .title:not(:empty)) .details-section > .header): Added.
Don't lower the header of a subsection unless the parent section has a `title`. This is necessary
because the `WI.Popover` for the "Event" badge has a top-level `WI.DetailsSection` with no `title`
in order to get the same nested styling applied to the Event Listeners section in the Node panel of
the details sidebar in the Elements Tab.

* LayoutTests/inspector/css/nodeLayoutFlagsChanged-Event-attribute.html: Added.
* LayoutTests/inspector/css/nodeLayoutFlagsChanged-Event-attribute-expected.txt: Added.
* LayoutTests/inspector/css/nodeLayoutFlagsChanged-Event-script.html: Added.
* LayoutTests/inspector/css/nodeLayoutFlagsChanged-Event-script-expected.txt: Added.
* LayoutTests/inspector/dom/getEventListenersForNode.html:
* LayoutTests/inspector/dom/getEventListenersForNode-expected.txt:

Canonical link: https://commits.webkit.org/253727@main
  • Loading branch information
dcrousso committed Aug 24, 2022
1 parent 1c1d283 commit 2a8187b1de2df352158fefedcf033f80d9cf74b3
Show file tree
Hide file tree
Showing 24 changed files with 895 additions and 206 deletions.
@@ -0,0 +1,35 @@
Tests for the CSS.nodeLayoutFlagsChanged event with the Event enum.


== Running test suite: CSS.nodeLayoutFlagsChanged.Event
-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.Initial
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.Removed.Parent
Removing attribute event listener from parent...
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.Removed.Child
Removing attribute event listener from child...
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.Removed
Removing attribute event listener...
PASS: Should not have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.Added.Parent
Adding attribute event listener to parent...
PASS: Should not have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.Added.Child
Adding attribute event listener to child...
PASS: Should not have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.Added
Adding attribute event listener...
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Attribute.RapidChange
Rapidly changing attribute event listener...
PASS: Should have Event layout flag.

@@ -0,0 +1,172 @@
<!DOCTYPE html>
<html>
<head>
<script src="../../http/tests/inspector/resources/inspector-test.js"></script>
<script>
function test()
{
let suite = InspectorTest.createAsyncSuite("CSS.nodeLayoutFlagsChanged.Event");

function addTestCase({name, description, selector, setup, domNodeHandler})
{
suite.addTestCase({
name,
description,
setup,
async test() {
let documentNode = await WI.domManager.requestDocument();
let nodeId = await documentNode.querySelector(selector);
let domNode = WI.domManager.nodeForId(nodeId);
InspectorTest.assert(domNode, `Should find DOM Node for selector '${selector}'.`);
await domNodeHandler(domNode);
},
});
}

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.Initial",
description: "Test node with an attribute event listener.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.expectTrue(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");
},
});

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.Removed.Parent",
description: "Test that removing an attribute event listener from a parent has no effect on the child.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.assert(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");

InspectorTest.log("Removing attribute event listener from parent...");
let listener = domNode.singleFireEventListener(WI.DOMNode.Event.LayoutFlagsChanged, (event) => {
InspectorTest.fail("Should not change layout flags of child.");
});
await InspectorTest.evaluateInPage(`document.querySelector("#parent").onclick = null`);
domNode.removeEventListener(WI.DOMNode.Event.LayoutFlagsChanged, listener);

InspectorTest.expectTrue(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");
},
});

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.Removed.Child",
description: "Test that removing an attribute event listener from a child has no effect on the parent.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.assert(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");

InspectorTest.log("Removing attribute event listener from child...");
let listener = domNode.singleFireEventListener(WI.DOMNode.Event.LayoutFlagsChanged, (event) => {
InspectorTest.fail("Should not change layout flags of parent.");
});
await InspectorTest.evaluateInPage(`document.querySelector("#child").onclick = null`);
domNode.removeEventListener(WI.DOMNode.Event.LayoutFlagsChanged, listener);

InspectorTest.expectTrue(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");
},
});

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.Removed",
description: "Test removing an attribute event listener.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.assert(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");

InspectorTest.log("Removing attribute event listener...");
await Promise.all([
domNode.awaitEvent(WI.DOMNode.Event.LayoutFlagsChanged),
InspectorTest.evaluateInPage(`document.querySelector("#target").onclick = null`),
]);

InspectorTest.expectFalse(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should not have Event layout flag.");
},
});

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.Added.Parent",
description: "Test that adding an attribute event listener to a parent has no effect on the child.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.assert(!domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should not have Event layout flag.");

InspectorTest.log("Adding attribute event listener to parent...");
let listener = domNode.singleFireEventListener(WI.DOMNode.Event.LayoutFlagsChanged, (event) => {
InspectorTest.fail("Should not change layout flags of child.");
});
await InspectorTest.evaluateInPage(`document.querySelector("#parent").onclick = (function parentClick2() { })`);
domNode.removeEventListener(WI.DOMNode.Event.LayoutFlagsChanged, listener);

InspectorTest.expectFalse(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should not have Event layout flag.");
},
});

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.Added.Child",
description: "Test that adding an attribute event listener to a child has no effect on the parent.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.assert(!domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should not have Event layout flag.");

InspectorTest.log("Adding attribute event listener to child...");
let listener = domNode.singleFireEventListener(WI.DOMNode.Event.LayoutFlagsChanged, (event) => {
InspectorTest.fail("Should not change layout flags of parent.");
});
await InspectorTest.evaluateInPage(`document.querySelector("#child").onclick = (function childClick2() { })`);
domNode.removeEventListener(WI.DOMNode.Event.LayoutFlagsChanged, listener);

InspectorTest.expectFalse(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should not have Event layout flag.");
},
});

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.Added",
description: "Test adding an attribute event listener.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.assert(!domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should not have Event layout flag.");

InspectorTest.log("Adding attribute event listener...");
await Promise.all([
domNode.awaitEvent(WI.DOMNode.Event.LayoutFlagsChanged),
InspectorTest.evaluateInPage(`document.querySelector("#target").onclick = (function targetClick2() { })`),
]);

InspectorTest.expectTrue(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");
},
});

addTestCase({
name: "CSS.nodeLayoutFlagsChanged.Event.Attribute.RapidChange",
description: "Test rapidly removing and adding an attribute event listener.",
selector: "#target",
async domNodeHandler(domNode) {
InspectorTest.assert(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");

InspectorTest.log("Rapidly changing attribute event listener...");
let listener = domNode.singleFireEventListener(WI.DOMNode.Event.LayoutFlagsChanged, (event) => {
InspectorTest.fail("Should not change layout flags.");
});
await InspectorTest.evaluateInPage(`for (let i = 0; i < 10; ++i) { let old = document.querySelector("#target").onclick; document.querySelector("#target").onclick = null; document.querySelector("#target").onclick = old; }`);
domNode.removeEventListener(WI.DOMNode.Event.LayoutFlagsChanged, listener);

InspectorTest.expectTrue(domNode.layoutFlags.includes(WI.DOMNode.LayoutFlag.Event), "Should have Event layout flag.");
},
});

suite.runTestCasesAndFinish();
}
</script>
</head>
<body onload="runTest()">
<p>Tests for the CSS.nodeLayoutFlagsChanged event with the Event enum.</p>

<div id="parent" onclick="(function parentClick(event) { })()">
<div id="target" onclick="(function targetClick(event) { })()">
<div id="child" onclick="(function childClick(event) { })()">
</div>
</div>
</body>
</html>
@@ -0,0 +1,35 @@
Tests for the CSS.nodeLayoutFlagsChanged event with the Event enum.


== Running test suite: CSS.nodeLayoutFlagsChanged.Event
-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.Initial
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.Removed.Parent
Removing JS event listener from parent...
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.Removed.Child
Removing JS event listener from child...
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.Removed
Removing JS event listener...
PASS: Should not have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.Added.Parent
Adding JS event listener to parent...
PASS: Should not have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.Added.Child
Adding JS event listener to child...
PASS: Should not have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.Added
Adding JS event listener...
PASS: Should have Event layout flag.

-- Running test case: CSS.nodeLayoutFlagsChanged.Event.Script.RapidChange
Rapidly changing JS event listener...
PASS: Should have Event layout flag.

0 comments on commit 2a8187b

Please sign in to comment.