Enable full Shadow DOM support for child widgets

This CL fixes an architectural issue in the Widget framework where child
widgets were being incorrectly relocated from a parent's Shadow DOM to
its Light DOM.

Why the original code was insufficient: The Widget framework was built
on the assumption of a linear parentElement hierarchy. This assumption
failed for "pure" Shadow DOM widgets (where the contentElement is the
ShadowRoot) for several reasons:

1. Hierarchy Blind Spot: In the DOM, a child of a ShadowRoot has
parentElement === null. The framework's visibility and reparenting
checks (this.element.parentElement === parentElement) always failed when
the target parent was a ShadowRoot.

2. Protection Bypass: Monkey-patching of native DOM methods (like
appendChild) was only applied to Element.prototype. Since ShadowRoot
inherits from DocumentFragment (and not Element), widgets could be
attached to a ShadowRoot without the framework's safety checks or
hierarchy tracking triggering correctly.

3. Forced Relocation: During onConnect, the framework used
parentElementOrShadowHost() to find the parent. For a widget in a Shadow
Root, this skipped the ShadowRoot and returned the host element, leading
to a widget.show(host) call that moved the widget into the Light DOM. In
"pure" widgets (which lack slots), this made child widgets invisible and
broke visual logging impressions.

Bug: 407751340
Change-Id: Ie9f8b35c785a5fa8c341f78232af4b5bf11a0a73
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7785231
Auto-Submit: Danil Somsikov <dsv@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Commit-Queue: Danil Somsikov <dsv@chromium.org>
diff --git a/front_end/core/dom_extension/DOMExtension.ts b/front_end/core/dom_extension/DOMExtension.ts
index 8090c5f..23fb931 100644
--- a/front_end/core/dom_extension/DOMExtension.ts
+++ b/front_end/core/dom_extension/DOMExtension.ts
@@ -159,7 +159,7 @@
   return this.ownerDocument.defaultView as Window;
 };
 
-Element.prototype.removeChildren = function(): void {
+Node.prototype.removeChildren = function(): void {
   if (this.firstChild) {
     this.textContent = '';
   }
diff --git a/front_end/legacy/legacy-defs.d.ts b/front_end/legacy/legacy-defs.d.ts
index 4743724..252cca6 100644
--- a/front_end/legacy/legacy-defs.d.ts
+++ b/front_end/legacy/legacy-defs.d.ts
@@ -61,7 +61,6 @@
   createChild<K extends keyof HTMLElementTagNameMap>(tagName: K, className?: string): HTMLElementTagNameMap[K];
   hasFocus(): boolean;
   positionAt(x: (number|undefined), y: (number|undefined), relativeTo?: Element): void;
-  removeChildren(): void;
   scrollIntoViewIfNeeded(center?: boolean): void;
 }
 
@@ -86,6 +85,7 @@
   isSelfOrDescendant(node: Node|null): boolean;
   parentElementOrShadowHost(): Element|null;
   parentNodeOrShadowHost(): Node|null;
+  removeChildren(): void;
   setTextContentTruncatedIfNeeded(text: unknown, placeholder?: string): boolean;
   traverseNextNode(stayWithin?: Node): Node|null;
   traversePreviousNode(stayWithin?: Node): Node|null;
diff --git a/front_end/ui/legacy/Widget.test.ts b/front_end/ui/legacy/Widget.test.ts
index b2c71aa..60164e9 100644
--- a/front_end/ui/legacy/Widget.test.ts
+++ b/front_end/ui/legacy/Widget.test.ts
@@ -977,6 +977,14 @@
   });
 
   describe('Shadow DOM', () => {
+    it('throws error when using DocumentFragment.appendChild with a widget', () => {
+      const fragment = document.createDocumentFragment();
+      const widget = new Widget();
+      widget.markAsRoot();
+      assert.throws(
+          () => fragment.appendChild(widget.element), /Attempt to modify widget with native DOM method `appendChild`/);
+    });
+
     it('keeps child widget in the Shadow Root when using pure shadow DOM', () => {
       const container = document.createElement('div');
       renderElementIntoDOM(container);
diff --git a/front_end/ui/legacy/Widget.ts b/front_end/ui/legacy/Widget.ts
index 6384132..5dca779 100644
--- a/front_end/ui/legacy/Widget.ts
+++ b/front_end/ui/legacy/Widget.ts
@@ -42,10 +42,10 @@
 
 // Remember the original DOM mutation methods here, since we
 // will override them below to sanity check the Widget system.
-const originalAppendChild = Element.prototype.appendChild;
-const originalInsertBefore = Element.prototype.insertBefore;
-const originalRemoveChild = Element.prototype.removeChild;
-const originalRemoveChildren = Element.prototype.removeChildren;
+const originalAppendChild = Node.prototype.appendChild;
+const originalInsertBefore = Node.prototype.insertBefore;
+const originalRemoveChild = Node.prototype.removeChild;
+const originalRemoveChildren = Node.prototype.removeChildren;
 
 function assert(condition: unknown, message: string): void {
   if (!condition) {
@@ -218,8 +218,9 @@
         (element.parentNode instanceof DocumentFragment) ? element.parentNode : element.parentElementOrShadowHost();
     if (!parent) {
       widget.markAsRoot();
+    } else {
+      widget.show(parent as HTMLElement, undefined, /* suppressOrphanWidgetError= */ true);
     }
-    widget.show(parent as HTMLElement, undefined, /* suppressOrphanWidgetError= */ true);
   };
 }
 
@@ -1311,28 +1312,28 @@
   return new Error(`Attempt to modify widget with native DOM method \`${funcName}\``);
 }
 
-Element.prototype.appendChild = function<T extends Node>(node: T): T {
+Node.prototype.appendChild = function<T extends Node>(node: T): T {
   if (widgetMap.get(node) && node.parentNode !== this) {
     throw domOperationError('appendChild');
   }
   return originalAppendChild.call(this, node) as T;
 };
 
-Element.prototype.insertBefore = function<T extends Node>(node: T, child: Node|null): T {
+Node.prototype.insertBefore = function<T extends Node>(node: T, child: Node|null): T {
   if (widgetMap.get(node) && node.parentNode !== this) {
     throw domOperationError('insertBefore');
   }
   return originalInsertBefore.call(this, node, child) as T;
 };
 
-Element.prototype.removeChild = function<T extends Node>(child: T): T {
+Node.prototype.removeChild = function<T extends Node>(child: T): T {
   if (widgetCounterMap.get(child) || widgetMap.get(child)) {
     throw domOperationError('removeChild');
   }
   return originalRemoveChild.call(this, child) as T;
 };
 
-Element.prototype.removeChildren = function(): void {
+Node.prototype.removeChildren = function(): void {
   if (widgetCounterMap.get(this)) {
     throw domOperationError('removeChildren');
   }