[Web Inspector] Graphics tab should display pseudo-elements for more than ::before and ::after
https://bugs.webkit.org/show_bug.cgi?id=235234
<rdar://87766777>

Reviewed by Devin Rousso.

Source/JavaScriptCore:

Add a new `DOM.Styleable` type to be used as the parameter type for `requestEffectTarget()` callbacks.

* inspector/protocol/Animation.json:
* inspector/protocol/DOM.json:

Source/WebCore:

Until now, we would pass the result of KeyframeEffect::targetElementOrPseudoElement() to
InspectorAnimationAgent::willApplyKeyframeEffect(). This meant that the inspector would only
be told of an animation target as an Element or a PseudoElement but not as an Element / PseudoId
pair, thus only allowing ::before and ::after to be represented since only those pseudo-elements
create a PseudoElement.

We now pass a Styleable, which encapsulate an Element / PseudoId pair, to
InspectorAnimationAgent::willApplyKeyframeEffect(). Additionally, the Styleable target is read from
the effect for callbacks provided to requestEffectTarget().

Sadly, we still rely on PseudoElement, but this patch at least removes all use of PseudoElement
from KeyframeEffect and pushes it down to InspectorDOMAgent with a new static method
elementToPushForStyleable() which the new pushStyleableElementToFrontend() and pushStyleablePathToFrontend()
methods use to turn the Styleable to an Element or PseudoElement.

In the future, we would need to further remove PseudoElement from Web Inspector and expose something
similar to Styleable throughout the codebase, or find some other way to encapsulate an Element / PseudoId
pair.

* animation/KeyframeEffect.cpp:
(WebCore::KeyframeEffect::apply):
(WebCore::KeyframeEffect::targetElementOrPseudoElement const): Deleted.
* animation/KeyframeEffect.h:
(WebCore::KeyframeEffect::target const):
* inspector/InspectorInstrumentation.cpp:
(WebCore::InspectorInstrumentation::willApplyKeyframeEffectImpl):
* inspector/InspectorInstrumentation.h:
(WebCore::InspectorInstrumentation::willApplyKeyframeEffect):
* inspector/agents/InspectorAnimationAgent.cpp:
(WebCore::InspectorAnimationAgent::requestEffectTarget):
(WebCore::InspectorAnimationAgent::willApplyKeyframeEffect):
* inspector/agents/InspectorAnimationAgent.h:
* inspector/agents/InspectorCSSAgent.cpp:
(WebCore::InspectorCSSAgent::protocolValueForPseudoId):
(WebCore::protocolValueForPseudoId): Deleted.
* inspector/agents/InspectorCSSAgent.h:
* inspector/agents/InspectorDOMAgent.cpp:
(WebCore::elementToPushForStyleable):
(WebCore::InspectorDOMAgent::pushStyleableElementToFrontend):
(WebCore::InspectorDOMAgent::pushStyleablePathToFrontend):
* inspector/agents/InspectorDOMAgent.h:

Source/WebInspectorUI:

Add a new `DOMStyleable` model class to match the new `DOM.Styleable` protocol type.

When calling Animation.requestEffectTarget(), we now use this new DOMStyleable class
to display pseudo-elements other than ::before or ::after in the Graphics tab.

* UserInterface/Base/DOMUtilities.js:
(WI.linkifyStyleable):
* UserInterface/Main.html:
* UserInterface/Models/Animation.js:
(WI.Animation.prototype.requestEffectTarget):
* UserInterface/Models/DOMStyleable.js: Added.
(WI.DOMStyleable.prototype.fromPayload):
(WI.DOMStyleable.prototype.get node):
(WI.DOMStyleable.prototype.get pseudoId):
(WI.DOMStyleable.prototype.get displayName):
(WI.DOMStyleable):
* UserInterface/Test.html:
* UserInterface/Views/AnimationCollectionContentView.js:
(WI.AnimationCollectionContentView.prototype._handleContentViewMouseEnter):
* UserInterface/Views/AnimationContentView.js:
(WI.AnimationContentView.prototype._refreshSubtitle):
* UserInterface/Views/AnimationDetailsSidebarPanel.js:
(WI.AnimationDetailsSidebarPanel.prototype._refreshIdentitySection):

LayoutTests:

Check that requestEffectTarget() returns the correct Styleable by checking whether it's defined
or null, and checking its `node` and `pseudoId` members.

* inspector/animation/targetChanged-expected.txt:
* inspector/animation/targetChanged.html:


git-svn-id: http://svn.webkit.org/repository/webkit/trunk@288623 268f45cc-cd09-0410-ab3c-d52691b4dbfc
diff --git a/LayoutTests/ChangeLog b/LayoutTests/ChangeLog
index 07da877..8bf660c 100644
--- a/LayoutTests/ChangeLog
+++ b/LayoutTests/ChangeLog
@@ -1,3 +1,17 @@
+2022-01-26  Antoine Quint  <graouts@webkit.org>
+
+        [Web Inspector] Graphics tab should display pseudo-elements for more than ::before and ::after
+        https://bugs.webkit.org/show_bug.cgi?id=235234
+        <rdar://87766777>
+
+        Reviewed by Devin Rousso.
+
+        Check that requestEffectTarget() returns the correct Styleable by checking whether it's defined
+        or null, and checking its `node` and `pseudoId` members.
+
+        * inspector/animation/targetChanged-expected.txt:
+        * inspector/animation/targetChanged.html:
+
 2022-01-26  J Pascoe  <j_pascoe@apple.com>
 
         [WebAuthn] Add authenticator attachment used during authentication to credential payload
diff --git a/LayoutTests/inspector/animation/targetChanged-expected.txt b/LayoutTests/inspector/animation/targetChanged-expected.txt
index 988de22..b76dcc8 100644
--- a/LayoutTests/inspector/animation/targetChanged-expected.txt
+++ b/LayoutTests/inspector/animation/targetChanged-expected.txt
@@ -3,16 +3,33 @@
 
 == Running test suite: Animation.targetChanged
 -- Running test case: Animation.targetChanged.NewTarget
-PASS: Animation should have a target.
+PASS: Animation effect should have a target element.
+PASS: Animation effect should not have a target pseudo-element.
 Changing target...
 
-PASS: Animation should have a target.
-PASS: Animation effect should have changed.
+PASS: Animation effect should have a target element.
+PASS: Animation effect should not have a target pseudo-element.
+PASS: Animation effect target should have changed.
+PASS: Animation effect target element should have changed.
 
 -- Running test case: Animation.targetChanged.NullTarget
-PASS: Animation should have a target.
+PASS: Animation effect should have a target element.
+PASS: Animation effect should not have a target pseudo-element.
 Changing target...
 
-PASS: Animation should not have a target.
-PASS: Animation effect should have changed.
+PASS: Animation effect should not have a target element.
+PASS: Animation effect target should have changed.
+
+-- Running test case: Animation.targetChanged.NewTargetWithPseudoId
+PASS: Animation effect should not have a target.
+Changing target...
+
+PASS: Animation effect should have a target element.
+PASS: Animation effect should not have a target pseudo-element.
+Changing target again...
+
+PASS: Animation effect should have a target element.
+PASS: Animation effect should have a target pseudo-element.
+PASS: Animation effect target should have changed.
+PASS: Animation effect target element should not have changed.
 
diff --git a/LayoutTests/inspector/animation/targetChanged.html b/LayoutTests/inspector/animation/targetChanged.html
index be705f6..80b5bc9 100644
--- a/LayoutTests/inspector/animation/targetChanged.html
+++ b/LayoutTests/inspector/animation/targetChanged.html
@@ -29,7 +29,8 @@
             InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
 
             let [oldTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
-            InspectorTest.expectThat(oldTarget instanceof WI.DOMNode, "Animation should have a target.");
+            InspectorTest.expectThat(oldTarget.node instanceof WI.DOMNode, "Animation effect should have a target element.");
+            InspectorTest.expectNull(oldTarget.pseudoId, "Animation effect should not have a target pseudo-element.");
 
             InspectorTest.log("Changing target...\n");
             await Promise.all([
@@ -38,9 +39,11 @@
             ]);
 
             let [newTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
-            InspectorTest.expectThat(newTarget instanceof WI.DOMNode, "Animation should have a target.");
+            InspectorTest.expectThat(newTarget.node instanceof WI.DOMNode, "Animation effect should have a target element.");
+            InspectorTest.expectNull(newTarget.pseudoId, "Animation effect should not have a target pseudo-element.");
 
-            InspectorTest.expectNotEqual(newTarget, oldTarget, "Animation effect should have changed.");
+            InspectorTest.expectNotEqual(newTarget, oldTarget, "Animation effect target should have changed.");
+            InspectorTest.expectNotEqual(newTarget.node, oldTarget.node, "Animation effect target element should have changed.");
         },
     });
 
@@ -60,7 +63,8 @@
             InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
 
             let [oldTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
-            InspectorTest.expectThat(oldTarget instanceof WI.DOMNode, "Animation should have a target.");
+            InspectorTest.expectThat(oldTarget.node instanceof WI.DOMNode, "Animation effect should have a target element.");
+            InspectorTest.expectNull(oldTarget.pseudoId, "Animation effect should not have a target pseudo-element.");
 
             InspectorTest.log("Changing target...\n");
             await Promise.all([
@@ -69,9 +73,52 @@
             ]);
 
             let [newTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
-            InspectorTest.expectNull(newTarget, "Animation should not have a target.");
+            InspectorTest.expectNull(newTarget, "Animation effect should not have a target element.");
 
-            InspectorTest.expectNotEqual(newTarget, oldTarget, "Animation effect should have changed.");
+            InspectorTest.expectNotEqual(newTarget, oldTarget, "Animation effect target should have changed.");
+        },
+    });
+
+    suite.addTestCase({
+        name: "Animation.targetChanged.NewTargetWithPseudoId",
+        description: "Should return a valid object for the given animation identifier.",
+        async test() {
+            let animations = Array.from(WI.animationManager.animationCollection);
+            InspectorTest.assert(animations.length === 1, "There should only be one animation.");
+
+            let animation = animations[0];
+            if (!animation) {
+                throw `Missing animation.`;
+                return;
+            }
+
+            InspectorTest.assert(animation.animationType === WI.Animation.Type.WebAnimation, "Animation should be a web animation.");
+
+            let [oldTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
+            InspectorTest.expectNull(oldTarget, "Animation effect should not have a target.");
+
+            InspectorTest.log("Changing target...\n");
+            await Promise.all([
+                animation.awaitEvent(WI.Animation.Event.TargetChanged),
+                InspectorTest.evaluateInPage(`window.animation.effect.target = document.createElement("div")`),
+            ]);
+
+            let [newTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
+            InspectorTest.expectThat(newTarget.node instanceof WI.DOMNode, "Animation effect should have a target element.");
+            InspectorTest.expectNull(newTarget.pseudoId, "Animation effect should not have a target pseudo-element.");
+
+            InspectorTest.log("Changing target again...\n");
+            await Promise.all([
+                animation.awaitEvent(WI.Animation.Event.TargetChanged),
+                InspectorTest.evaluateInPage(`window.animation.effect.pseudoElement = "::before"`),
+            ]);
+
+            let [newestTarget] = await promisify((callback) => { animation.requestEffectTarget(callback); });
+            InspectorTest.expectThat(newestTarget.node instanceof WI.DOMNode, "Animation effect should have a target element.");
+            InspectorTest.expectEqual(newestTarget.pseudoId, "before", "Animation effect should have a target pseudo-element.");
+
+            InspectorTest.expectNotEqual(newTarget, newestTarget, "Animation effect target should have changed.");
+            InspectorTest.expectEqual(newTarget.node, newestTarget.node, "Animation effect target element should not have changed.");
         },
     });
 
diff --git a/Source/JavaScriptCore/ChangeLog b/Source/JavaScriptCore/ChangeLog
index f8d318b..0d9d94b 100644
--- a/Source/JavaScriptCore/ChangeLog
+++ b/Source/JavaScriptCore/ChangeLog
@@ -1,3 +1,16 @@
+2022-01-26  Antoine Quint  <graouts@webkit.org>
+
+        [Web Inspector] Graphics tab should display pseudo-elements for more than ::before and ::after
+        https://bugs.webkit.org/show_bug.cgi?id=235234
+        <rdar://87766777>
+
+        Reviewed by Devin Rousso.
+
+        Add a new `DOM.Styleable` type to be used as the parameter type for `requestEffectTarget()` callbacks.
+
+        * inspector/protocol/Animation.json:
+        * inspector/protocol/DOM.json:
+
 2022-01-26  Yusuke Suzuki  <ysuzuki@apple.com>
 
         [JSC] Do not run testPingPongStackOverflow while running multithreaded MultithreadedMultiVMExecutionTest
diff --git a/Source/JavaScriptCore/inspector/protocol/Animation.json b/Source/JavaScriptCore/inspector/protocol/Animation.json
index 0296510..78b1c37 100644
--- a/Source/JavaScriptCore/inspector/protocol/Animation.json
+++ b/Source/JavaScriptCore/inspector/protocol/Animation.json
@@ -88,7 +88,7 @@
                 { "name": "animationId", "$ref": "AnimationId" }
             ],
             "returns": [
-                { "name": "nodeId", "$ref": "DOM.NodeId" }
+                { "name": "nodeId", "$ref": "DOM.Styleable" }
             ]
         },
         {
diff --git a/Source/JavaScriptCore/inspector/protocol/DOM.json b/Source/JavaScriptCore/inspector/protocol/DOM.json
index 3a91ac6..31ef6b8 100644
--- a/Source/JavaScriptCore/inspector/protocol/DOM.json
+++ b/Source/JavaScriptCore/inspector/protocol/DOM.json
@@ -168,6 +168,15 @@
                 { "name": "borderColor", "$ref": "RGBAColor", "optional": true, "description": "The border highlight fill color (default: transparent)." },
                 { "name": "marginColor", "$ref": "RGBAColor", "optional": true, "description": "The margin highlight fill color (default: transparent)." }
             ]
+        },
+        {
+            "id": "Styleable",
+            "type": "object",
+            "properties": [
+                { "name": "nodeId", "$ref": "NodeId" },
+                { "name": "pseudoId", "$ref": "CSS.PseudoId", "optional": true }
+            ],
+            "description": "An object referencing a node and a pseudo-element, primarily used to identify an animation effect target."
         }
     ],
     "commands": [
diff --git a/Source/WebCore/ChangeLog b/Source/WebCore/ChangeLog
index 5c054ee..d1c441c 100644
--- a/Source/WebCore/ChangeLog
+++ b/Source/WebCore/ChangeLog
@@ -1,3 +1,53 @@
+2022-01-26  Antoine Quint  <graouts@webkit.org>
+
+        [Web Inspector] Graphics tab should display pseudo-elements for more than ::before and ::after
+        https://bugs.webkit.org/show_bug.cgi?id=235234
+        <rdar://87766777>
+
+        Reviewed by Devin Rousso.
+
+        Until now, we would pass the result of KeyframeEffect::targetElementOrPseudoElement() to 
+        InspectorAnimationAgent::willApplyKeyframeEffect(). This meant that the inspector would only
+        be told of an animation target as an Element or a PseudoElement but not as an Element / PseudoId
+        pair, thus only allowing ::before and ::after to be represented since only those pseudo-elements
+        create a PseudoElement.
+
+        We now pass a Styleable, which encapsulate an Element / PseudoId pair, to
+        InspectorAnimationAgent::willApplyKeyframeEffect(). Additionally, the Styleable target is read from
+        the effect for callbacks provided to requestEffectTarget().
+
+        Sadly, we still rely on PseudoElement, but this patch at least removes all use of PseudoElement
+        from KeyframeEffect and pushes it down to InspectorDOMAgent with a new static method
+        elementToPushForStyleable() which the new pushStyleableElementToFrontend() and pushStyleablePathToFrontend()
+        methods use to turn the Styleable to an Element or PseudoElement.
+
+        In the future, we would need to further remove PseudoElement from Web Inspector and expose something
+        similar to Styleable throughout the codebase, or find some other way to encapsulate an Element / PseudoId
+        pair.
+
+        * animation/KeyframeEffect.cpp:
+        (WebCore::KeyframeEffect::apply):
+        (WebCore::KeyframeEffect::targetElementOrPseudoElement const): Deleted.
+        * animation/KeyframeEffect.h:
+        (WebCore::KeyframeEffect::target const):
+        * inspector/InspectorInstrumentation.cpp:
+        (WebCore::InspectorInstrumentation::willApplyKeyframeEffectImpl):
+        * inspector/InspectorInstrumentation.h:
+        (WebCore::InspectorInstrumentation::willApplyKeyframeEffect):
+        * inspector/agents/InspectorAnimationAgent.cpp:
+        (WebCore::InspectorAnimationAgent::requestEffectTarget):
+        (WebCore::InspectorAnimationAgent::willApplyKeyframeEffect):
+        * inspector/agents/InspectorAnimationAgent.h:
+        * inspector/agents/InspectorCSSAgent.cpp:
+        (WebCore::InspectorCSSAgent::protocolValueForPseudoId):
+        (WebCore::protocolValueForPseudoId): Deleted.
+        * inspector/agents/InspectorCSSAgent.h:
+        * inspector/agents/InspectorDOMAgent.cpp:
+        (WebCore::elementToPushForStyleable):
+        (WebCore::InspectorDOMAgent::pushStyleableElementToFrontend):
+        (WebCore::InspectorDOMAgent::pushStyleablePathToFrontend):
+        * inspector/agents/InspectorDOMAgent.h:
+
 2022-01-26  J Pascoe  <j_pascoe@apple.com>
 
         [WebAuthn] Add authenticator attachment used during authentication to credential payload
diff --git a/Source/WebCore/animation/KeyframeEffect.cpp b/Source/WebCore/animation/KeyframeEffect.cpp
index d8e623a..ee9b7a0 100644
--- a/Source/WebCore/animation/KeyframeEffect.cpp
+++ b/Source/WebCore/animation/KeyframeEffect.cpp
@@ -48,7 +48,6 @@
 #include "KeyframeEffectStack.h"
 #include "Logging.h"
 #include "PropertyAllowlist.h"
-#include "PseudoElement.h"
 #include "RenderBox.h"
 #include "RenderBoxModelObject.h"
 #include "RenderElement.h"
@@ -1182,19 +1181,6 @@
     return m_target.get() && m_pseudoId != PseudoId::None;
 }
 
-Element* KeyframeEffect::targetElementOrPseudoElement() const
-{
-    if (m_target) {
-        if (m_pseudoId == PseudoId::Before)
-            return m_target->beforePseudoElement();
-
-        if (m_pseudoId == PseudoId::After)
-            return m_target->afterPseudoElement();
-    }
-
-    return m_target.get();
-}
-
 void KeyframeEffect::setTarget(RefPtr<Element>&& newTarget)
 {
     if (m_target == newTarget)
@@ -1288,7 +1274,7 @@
     auto computedTiming = getComputedTiming(startTime);
     if (!startTime) {
         m_phaseAtLastApplication = computedTiming.phase;
-        if (auto* target = targetElementOrPseudoElement())
+        if (auto target = targetStyleable())
             InspectorInstrumentation::willApplyKeyframeEffect(*target, *this, computedTiming);
     }
 
diff --git a/Source/WebCore/animation/KeyframeEffect.h b/Source/WebCore/animation/KeyframeEffect.h
index c2833c6..49a0a16 100644
--- a/Source/WebCore/animation/KeyframeEffect.h
+++ b/Source/WebCore/animation/KeyframeEffect.h
@@ -101,7 +101,6 @@
     const Vector<ParsedKeyframe>& parsedKeyframes() const { return m_parsedKeyframes; }
 
     Element* target() const { return m_target.get(); }
-    Element* targetElementOrPseudoElement() const;
     void setTarget(RefPtr<Element>&&);
 
     bool targetsPseudoElement() const;
diff --git a/Source/WebCore/inspector/InspectorInstrumentation.cpp b/Source/WebCore/inspector/InspectorInstrumentation.cpp
index 3c978e3..fa92adc 100644
--- a/Source/WebCore/inspector/InspectorInstrumentation.cpp
+++ b/Source/WebCore/inspector/InspectorInstrumentation.cpp
@@ -1120,7 +1120,7 @@
 }
 #endif
 
-void InspectorInstrumentation::willApplyKeyframeEffectImpl(InstrumentingAgents& instrumentingAgents, Element& target, KeyframeEffect& effect, ComputedEffectTiming computedTiming)
+void InspectorInstrumentation::willApplyKeyframeEffectImpl(InstrumentingAgents& instrumentingAgents, const Styleable& target, KeyframeEffect& effect, ComputedEffectTiming computedTiming)
 {
     if (auto* animationAgent = instrumentingAgents.trackingAnimationAgent())
         animationAgent->willApplyKeyframeEffect(target, effect, computedTiming);
diff --git a/Source/WebCore/inspector/InspectorInstrumentation.h b/Source/WebCore/inspector/InspectorInstrumentation.h
index 82acced..777a7ed 100644
--- a/Source/WebCore/inspector/InspectorInstrumentation.h
+++ b/Source/WebCore/inspector/InspectorInstrumentation.h
@@ -99,6 +99,8 @@
 class WebSocketChannel;
 class WorkerOrWorkletGlobalScope;
 
+struct Styleable;
+
 #if ENABLE(WEBGL)
 class WebGLProgram;
 #endif
@@ -299,7 +301,7 @@
     static bool isWebGLProgramHighlighted(WebGLRenderingContextBase&, WebGLProgram&);
 #endif
 
-    static void willApplyKeyframeEffect(Element&, KeyframeEffect&, ComputedEffectTiming);
+    static void willApplyKeyframeEffect(const Styleable&, KeyframeEffect&, ComputedEffectTiming);
     static void didChangeWebAnimationName(WebAnimation&);
     static void didSetWebAnimationEffect(WebAnimation&);
     static void didChangeWebAnimationEffectTiming(WebAnimation&);
@@ -502,7 +504,7 @@
     static bool isWebGLProgramHighlightedImpl(InstrumentingAgents&, WebGLProgram&);
 #endif
 
-    static void willApplyKeyframeEffectImpl(InstrumentingAgents&, Element&, KeyframeEffect&, ComputedEffectTiming);
+    static void willApplyKeyframeEffectImpl(InstrumentingAgents&, const Styleable&, KeyframeEffect&, ComputedEffectTiming);
     static void didChangeWebAnimationNameImpl(InstrumentingAgents&, WebAnimation&);
     static void didSetWebAnimationEffectImpl(InstrumentingAgents&, WebAnimation&);
     static void didChangeWebAnimationEffectTimingImpl(InstrumentingAgents&, WebAnimation&);
@@ -1455,10 +1457,10 @@
 }
 #endif
 
-inline void InspectorInstrumentation::willApplyKeyframeEffect(Element& target, KeyframeEffect& effect, ComputedEffectTiming computedTiming)
+inline void InspectorInstrumentation::willApplyKeyframeEffect(const Styleable& target, KeyframeEffect& effect, ComputedEffectTiming computedTiming)
 {
     FAST_RETURN_IF_NO_FRONTENDS(void());
-    if (auto* agents = instrumentingAgents(target.document()))
+    if (auto* agents = instrumentingAgents(target.element.document()))
         willApplyKeyframeEffectImpl(*agents, target, effect, computedTiming);
 }
 
diff --git a/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp b/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
index 5e5cdf6..0bbcc51 100644
--- a/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
+++ b/Source/WebCore/inspector/agents/InspectorAnimationAgent.cpp
@@ -38,6 +38,7 @@
 #include "Event.h"
 #include "FillMode.h"
 #include "Frame.h"
+#include "InspectorCSSAgent.h"
 #include "InspectorDOMAgent.h"
 #include "InstrumentingAgents.h"
 #include "JSExecState.h"
@@ -47,6 +48,7 @@
 #include "Page.h"
 #include "PlaybackDirection.h"
 #include "RenderElement.h"
+#include "Styleable.h"
 #include "TimingFunction.h"
 #include "WebAnimation.h"
 #include <JavaScriptCore/IdentifiersFactory.h>
@@ -279,7 +281,7 @@
     return { };
 }
 
-Protocol::ErrorStringOr<Protocol::DOM::NodeId> InspectorAnimationAgent::requestEffectTarget(const Protocol::Animation::AnimationId& animationId)
+Protocol::ErrorStringOr<Ref<Protocol::DOM::Styleable>> InspectorAnimationAgent::requestEffectTarget(const Protocol::Animation::AnimationId& animationId)
 {
     Protocol::ErrorString errorString;
 
@@ -297,11 +299,11 @@
 
     auto& keyframeEffect = downcast<KeyframeEffect>(*effect);
 
-    auto* target = keyframeEffect.targetElementOrPseudoElement();
+    auto target = keyframeEffect.targetStyleable();
     if (!target)
         return makeUnexpected("Animation for given animationId does not have a target"_s);
 
-    return domAgent->pushNodePathToFrontend(errorString, target);
+    return domAgent->pushStyleablePathToFrontend(errorString, *target);
 }
 
 Protocol::ErrorStringOr<Ref<Protocol::Runtime::RemoteObject>> InspectorAnimationAgent::resolveAnimation(const Protocol::Animation::AnimationId& animationId, const String& objectGroup)
@@ -371,7 +373,7 @@
     return computedTiming.localTime.value() < (computedTiming.endTime - computedTiming.activeDuration);
 }
 
-void InspectorAnimationAgent::willApplyKeyframeEffect(Element& target, KeyframeEffect& keyframeEffect, ComputedEffectTiming computedTiming)
+void InspectorAnimationAgent::willApplyKeyframeEffect(const Styleable& target, KeyframeEffect& keyframeEffect, ComputedEffectTiming computedTiming)
 {
     auto* animation = keyframeEffect.animation();
     if (!is<DeclarativeAnimation>(animation))
@@ -421,7 +423,7 @@
 
     if (ensureResult.isNewEntry) {
         if (auto* domAgent = m_instrumentingAgents.persistentDOMAgent()) {
-            if (auto nodeId = domAgent->pushNodeToFrontend(&target))
+            if (auto nodeId = domAgent->pushStyleableElementToFrontend(target))
                 event->setNodeId(nodeId);
         }
 
diff --git a/Source/WebCore/inspector/agents/InspectorAnimationAgent.h b/Source/WebCore/inspector/agents/InspectorAnimationAgent.h
index b3908c3..5c6313e 100644
--- a/Source/WebCore/inspector/agents/InspectorAnimationAgent.h
+++ b/Source/WebCore/inspector/agents/InspectorAnimationAgent.h
@@ -44,6 +44,8 @@
 class Page;
 class WebAnimation;
 
+struct Styleable;
+
 class InspectorAnimationAgent final : public InspectorAgentBase, public Inspector::AnimationBackendDispatcherHandler {
     WTF_MAKE_NONCOPYABLE(InspectorAnimationAgent);
     WTF_MAKE_FAST_ALLOCATED;
@@ -58,13 +60,13 @@
     // AnimationBackendDispatcherHandler
     Inspector::Protocol::ErrorStringOr<void> enable();
     Inspector::Protocol::ErrorStringOr<void> disable();
-    Inspector::Protocol::ErrorStringOr<Inspector::Protocol::DOM::NodeId> requestEffectTarget(const Inspector::Protocol::Animation::AnimationId&);
+    Inspector::Protocol::ErrorStringOr<Ref<Inspector::Protocol::DOM::Styleable>> requestEffectTarget(const Inspector::Protocol::Animation::AnimationId&);
     Inspector::Protocol::ErrorStringOr<Ref<Inspector::Protocol::Runtime::RemoteObject>> resolveAnimation(const Inspector::Protocol::Animation::AnimationId&, const String& objectGroup);
     Inspector::Protocol::ErrorStringOr<void> startTracking();
     Inspector::Protocol::ErrorStringOr<void> stopTracking();
 
     // InspectorInstrumentation
-    void willApplyKeyframeEffect(Element&, KeyframeEffect&, ComputedEffectTiming);
+    void willApplyKeyframeEffect(const Styleable&, KeyframeEffect&, ComputedEffectTiming);
     void didChangeWebAnimationName(WebAnimation&);
     void didSetWebAnimationEffect(WebAnimation&);
     void didChangeWebAnimationEffectTiming(WebAnimation&);
diff --git a/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp b/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp
index d19ad3d..63bc937 100644
--- a/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp
+++ b/Source/WebCore/inspector/agents/InspectorCSSAgent.cpp
@@ -441,7 +441,7 @@
     }
 }
 
-static std::optional<Protocol::CSS::PseudoId> protocolValueForPseudoId(PseudoId pseudoId)
+std::optional<Protocol::CSS::PseudoId> InspectorCSSAgent::protocolValueForPseudoId(PseudoId pseudoId)
 {
     switch (pseudoId) {
     case PseudoId::FirstLine:
diff --git a/Source/WebCore/inspector/agents/InspectorCSSAgent.h b/Source/WebCore/inspector/agents/InspectorCSSAgent.h
index a4c6127..4e35e9a 100644
--- a/Source/WebCore/inspector/agents/InspectorCSSAgent.h
+++ b/Source/WebCore/inspector/agents/InspectorCSSAgent.h
@@ -85,6 +85,8 @@
     static CSSStyleRule* asCSSStyleRule(CSSRule&);
     static std::optional<Inspector::Protocol::CSS::LayoutContextType> layoutContextTypeForRenderer(RenderObject*);
 
+    static std::optional<Inspector::Protocol::CSS::PseudoId> protocolValueForPseudoId(PseudoId);
+
     // InspectorAgentBase
     void didCreateFrontendAndBackend(Inspector::FrontendRouter*, Inspector::BackendDispatcher*);
     void willDestroyFrontendAndBackend(Inspector::DisconnectReason);
diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp
index 7a790bb..72d152c 100644
--- a/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp
+++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.cpp
@@ -104,6 +104,7 @@
 #include "StyleProperties.h"
 #include "StyleResolver.h"
 #include "StyleSheetList.h"
+#include "Styleable.h"
 #include "Text.h"
 #include "TextNodeTraversal.h"
 #include "Timer.h"
@@ -538,6 +539,25 @@
     m_childrenRequested.clear();
 }
 
+static Element* elementToPushForStyleable(const Styleable& styleable)
+{
+    // FIXME: We want to get rid of PseudoElement.
+    auto* element = &styleable.element;
+
+    if (styleable.pseudoId == PseudoId::Before)
+        element = element->beforePseudoElement();
+    else if (styleable.pseudoId == PseudoId::After)
+        element = element->afterPseudoElement();
+
+    return element;
+}
+
+Protocol::DOM::NodeId InspectorDOMAgent::pushStyleableElementToFrontend(const Styleable& styleable)
+{
+    auto* element = elementToPushForStyleable(styleable);
+    return pushNodeToFrontend(element ?: &styleable.element);
+}
+
 Protocol::DOM::NodeId InspectorDOMAgent::pushNodeToFrontend(Node* nodeToPush)
 {
     if (!nodeToPush)
@@ -637,6 +657,23 @@
     return pushNodePathToFrontend(ignored, nodeToPush);
 }
 
+Ref<Protocol::DOM::Styleable> InspectorDOMAgent::pushStyleablePathToFrontend(Protocol::ErrorString errorString, const Styleable& styleable)
+{
+    auto* element = elementToPushForStyleable(styleable);
+    auto nodeId = pushNodePathToFrontend(errorString, element ?: &styleable.element);
+
+    auto protocolStyleable = Protocol::DOM::Styleable::create()
+        .setNodeId(nodeId)
+        .release();
+
+    if (styleable.pseudoId != PseudoId::None) {
+        if (auto pseudoId = InspectorCSSAgent::protocolValueForPseudoId(styleable.pseudoId))
+            protocolStyleable->setPseudoId(*pseudoId);
+    }
+
+    return protocolStyleable;
+}
+
 Protocol::DOM::NodeId InspectorDOMAgent::pushNodePathToFrontend(Protocol::ErrorString errorString, Node* nodeToPush)
 {
     ASSERT(nodeToPush);  // Invalid input
diff --git a/Source/WebCore/inspector/agents/InspectorDOMAgent.h b/Source/WebCore/inspector/agents/InspectorDOMAgent.h
index 4eb63b7..eec24d3 100644
--- a/Source/WebCore/inspector/agents/InspectorDOMAgent.h
+++ b/Source/WebCore/inspector/agents/InspectorDOMAgent.h
@@ -75,6 +75,8 @@
 class RevalidateStyleAttributeTask;
 class ShadowRoot;
 
+struct Styleable;
+
 class InspectorDOMAgent final : public InspectorAgentBase, public Inspector::DOMBackendDispatcherHandler {
     WTF_MAKE_NONCOPYABLE(InspectorDOMAgent);
     WTF_MAKE_FAST_ALLOCATED;
@@ -184,6 +186,8 @@
 
     void styleAttributeInvalidated(const Vector<Element*>& elements);
 
+    Inspector::Protocol::DOM::NodeId pushStyleableElementToFrontend(const Styleable&);
+    Ref<Inspector::Protocol::DOM::Styleable> pushStyleablePathToFrontend(Inspector::Protocol::ErrorString, const Styleable&);
     Inspector::Protocol::DOM::NodeId pushNodeToFrontend(Node*);
     Inspector::Protocol::DOM::NodeId pushNodeToFrontend(Inspector::Protocol::ErrorString&, Inspector::Protocol::DOM::NodeId documentNodeId, Node*);
     Inspector::Protocol::DOM::NodeId pushNodePathToFrontend(Node*);
diff --git a/Source/WebInspectorUI/ChangeLog b/Source/WebInspectorUI/ChangeLog
index e0eba26..898e114 100644
--- a/Source/WebInspectorUI/ChangeLog
+++ b/Source/WebInspectorUI/ChangeLog
@@ -1,3 +1,35 @@
+2022-01-26  Antoine Quint  <graouts@webkit.org>
+
+        [Web Inspector] Graphics tab should display pseudo-elements for more than ::before and ::after
+        https://bugs.webkit.org/show_bug.cgi?id=235234
+        <rdar://87766777>
+
+        Reviewed by Devin Rousso.
+
+        Add a new `DOMStyleable` model class to match the new `DOM.Styleable` protocol type. 
+
+        When calling Animation.requestEffectTarget(), we now use this new DOMStyleable class
+        to display pseudo-elements other than ::before or ::after in the Graphics tab.
+
+        * UserInterface/Base/DOMUtilities.js:
+        (WI.linkifyStyleable):
+        * UserInterface/Main.html:
+        * UserInterface/Models/Animation.js:
+        (WI.Animation.prototype.requestEffectTarget):
+        * UserInterface/Models/DOMStyleable.js: Added.
+        (WI.DOMStyleable.prototype.fromPayload):
+        (WI.DOMStyleable.prototype.get node):
+        (WI.DOMStyleable.prototype.get pseudoId):
+        (WI.DOMStyleable.prototype.get displayName):
+        (WI.DOMStyleable):
+        * UserInterface/Test.html:
+        * UserInterface/Views/AnimationCollectionContentView.js:
+        (WI.AnimationCollectionContentView.prototype._handleContentViewMouseEnter):
+        * UserInterface/Views/AnimationContentView.js:
+        (WI.AnimationContentView.prototype._refreshSubtitle):
+        * UserInterface/Views/AnimationDetailsSidebarPanel.js:
+        (WI.AnimationDetailsSidebarPanel.prototype._refreshIdentitySection):
+
 2022-01-25  Nikita Vasilyev  <nvasilyev@apple.com>
 
         Web Inspector: Collapse blackboxed call frames by default
diff --git a/Source/WebInspectorUI/UserInterface/Base/DOMUtilities.js b/Source/WebInspectorUI/UserInterface/Base/DOMUtilities.js
index 42b39a1..ff73d05 100644
--- a/Source/WebInspectorUI/UserInterface/Base/DOMUtilities.js
+++ b/Source/WebInspectorUI/UserInterface/Base/DOMUtilities.js
@@ -57,6 +57,15 @@
     return link;
 };
 
+WI.linkifyStyleable = function(styleable)
+{
+    console.assert(styleable instanceof WI.DOMStyleable, styleable);
+    let displayName = styleable.displayName;
+    let link = document.createElement("span");
+    link.append(displayName);
+    return WI.linkifyNodeReferenceElement(styleable.node, link, {displayName});
+};
+
 WI.linkifyNodeReference = function(node, options = {})
 {
     let displayName = node.displayName;
diff --git a/Source/WebInspectorUI/UserInterface/Main.html b/Source/WebInspectorUI/UserInterface/Main.html
index cabcfd3..82ac693 100644
--- a/Source/WebInspectorUI/UserInterface/Main.html
+++ b/Source/WebInspectorUI/UserInterface/Main.html
@@ -419,6 +419,7 @@
     <script src="Models/DOMNodeStyles.js"></script>
     <script src="Models/DOMSearchMatchObject.js"></script>
     <script src="Models/DOMStorageObject.js"></script>
+    <script src="Models/DOMStyleable.js"></script>
     <script src="Models/DOMTree.js"></script>
     <script src="Models/DatabaseObject.js"></script>
     <script src="Models/DatabaseTableObject.js"></script>
diff --git a/Source/WebInspectorUI/UserInterface/Models/Animation.js b/Source/WebInspectorUI/UserInterface/Models/Animation.js
index e66e9a2..7c1b4c9 100644
--- a/Source/WebInspectorUI/UserInterface/Models/Animation.js
+++ b/Source/WebInspectorUI/UserInterface/Models/Animation.js
@@ -191,6 +191,7 @@
         return WI.UIString("Animation %d").format(this._uniqueDisplayNameNumber);
     }
 
+    // COMPATIBILITY (iOS 15): effectTarget changed from DOM.NodeId to CSS.Styleable.
     requestEffectTarget(callback)
     {
         if (this._effectTarget !== undefined) {
@@ -208,8 +209,12 @@
         WI.domManager.ensureDocument();
 
         let target = WI.assumingMainTarget();
-        target.AnimationAgent.requestEffectTarget(this._animationId, (error, nodeId) => {
-            this._effectTarget = !error ? WI.domManager.nodeForId(nodeId) : null;
+        target.AnimationAgent.requestEffectTarget(this._animationId, (error, styleable) => {
+            // COMPATIBILITY (iOS 15): effectTarget changed from DOM.NodeId to DOM.Styleable.
+            if (!isNaN(styleable))
+                styleable = {nodeId: styleable};
+
+            this._effectTarget = !error ? WI.DOMStyleable.fromPayload(styleable) : null;
 
             for (let requestEffectTargetCallback of this._requestEffectTargetCallbacks)
                 requestEffectTargetCallback(this._effectTarget);
diff --git a/Source/WebInspectorUI/UserInterface/Models/DOMStyleable.js b/Source/WebInspectorUI/UserInterface/Models/DOMStyleable.js
new file mode 100644
index 0000000..92bf0e7
--- /dev/null
+++ b/Source/WebInspectorUI/UserInterface/Models/DOMStyleable.js
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 Apple Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+ * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+ * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+ * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+WI.DOMStyleable = class DOMStyleable
+{
+    constructor(node, {pseudoId} = {})
+    {
+        console.assert(node instanceof WI.DOMNode, node);
+        console.assert(!pseudoId || Object.values(WI.CSSManager.PseudoSelectorNames).includes(pseudoId), pseudoId);
+
+        this._node = node;
+        this._pseudoId = pseudoId || null;
+    }
+
+    // Static
+
+    static fromPayload({nodeId, pseudoId})
+    {
+        return new WI.DOMStyleable(WI.domManager.nodeForId(nodeId), {pseudoId});
+    }
+
+    // Public
+
+    get node() { return this._node; }
+    get pseudoId() { return this._pseudoId; }
+
+    get displayName()
+    {
+        if (this._pseudoId)
+            return WI.CSSManager.displayNameForPseudoId(this._pseudoId);
+        return this._node.displayName;
+    }
+};
diff --git a/Source/WebInspectorUI/UserInterface/Test.html b/Source/WebInspectorUI/UserInterface/Test.html
index 556f085..cd2134a 100644
--- a/Source/WebInspectorUI/UserInterface/Test.html
+++ b/Source/WebInspectorUI/UserInterface/Test.html
@@ -152,6 +152,7 @@
     <script src="Models/DOMNode.js"></script>
     <script src="Models/DOMNodeStyles.js"></script>
     <script src="Models/DOMStorageObject.js"></script>
+    <script src="Models/DOMStyleable.js"></script>
     <script src="Models/DOMTree.js"></script>
     <script src="Models/DebuggerData.js"></script>
     <script src="Models/EventBreakpoint.js"></script>
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.js b/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.js
index 9ec1d2c..c20c3a0 100644
--- a/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/AnimationCollectionContentView.js
@@ -96,10 +96,10 @@
             return;
 
         let animation = contentView.representedObject;
-        animation.requestEffectTarget((node) => {
-            if (!node || !node.ownerDocument)
+        animation.requestEffectTarget((styleable) => {
+            if (!styleable.node || !styleable.node.ownerDocument)
                 return;
-            node.highlight();
+            styleable.node.highlight();
         });
     }
 
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js
index c6260d0..1d135e48 100644
--- a/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js
+++ b/Source/WebInspectorUI/UserInterface/Views/AnimationContentView.js
@@ -162,12 +162,12 @@
 
     _refreshSubtitle()
     {
-        this.representedObject.requestEffectTarget((domNode) => {
-            this._animationTargetDOMNode = domNode;
+        this.representedObject.requestEffectTarget((styleable) => {
+            this._animationTargetDOMNode = styleable.node;
 
             this._subtitleElement.removeChildren();
-            if (domNode)
-                this._subtitleElement.appendChild(WI.linkifyNodeReference(domNode));
+            if (styleable)
+                this._subtitleElement.appendChild(WI.linkifyStyleable(styleable));
         });
     }
 
diff --git a/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.js b/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.js
index 8c3fd94..42d2c11 100644
--- a/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.js
+++ b/Source/WebInspectorUI/UserInterface/Views/AnimationDetailsSidebarPanel.js
@@ -179,8 +179,8 @@
         this._cssTransitionPropertyRow.value = cssTransitionProperty;
 
         this._targetRow.value = null;
-        this._animation.requestEffectTarget((domNode) => {
-            this._targetRow.value = domNode ? WI.linkifyNodeReference(domNode) : null;
+        this._animation.requestEffectTarget((styleable) => {
+            this._targetRow.value = WI.linkifyStyleable(styleable);
         });
     }