@treelocator/runtime 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,30 +1,30 @@
1
1
 
2
- > @treelocator/runtime@0.1.5 build /Users/wende/projects/locatorjs/packages/runtime
2
+ > @treelocator/runtime@0.1.7 build /Users/wende/projects/locatorjs/packages/runtime
3
3
  > concurrently pnpm:build:*
4
4
 
5
+ [babel]
6
+ [babel] > @treelocator/runtime@0.1.7 build:babel /Users/wende/projects/locatorjs/packages/runtime
7
+ [babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
8
+ [babel]
5
9
  [ts]
6
- [ts] > @treelocator/runtime@0.1.5 build:ts /Users/wende/projects/locatorjs/packages/runtime
10
+ [ts] > @treelocator/runtime@0.1.7 build:ts /Users/wende/projects/locatorjs/packages/runtime
7
11
  [ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
8
12
  [ts]
9
- [tailwind]
10
- [tailwind] > @treelocator/runtime@0.1.5 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
11
- [tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
12
- [tailwind]
13
13
  [wrapImage]
14
- [wrapImage] > @treelocator/runtime@0.1.5 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
14
+ [wrapImage] > @treelocator/runtime@0.1.7 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
15
15
  [wrapImage] > node ./scripts/wrapImage.js
16
16
  [wrapImage]
17
- [babel]
18
- [babel] > @treelocator/runtime@0.1.5 build:babel /Users/wende/projects/locatorjs/packages/runtime
19
- [babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
20
- [babel]
17
+ [tailwind]
18
+ [tailwind] > @treelocator/runtime@0.1.7 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
19
+ [tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
20
+ [tailwind]
21
21
  [wrapImage] Tree icon file generated
22
22
  [wrapImage] pnpm run build:wrapImage exited with code 0
23
23
  [tailwind]
24
24
  [tailwind] Rebuilding...
25
25
  [tailwind]
26
- [tailwind] Done in 294ms.
26
+ [tailwind] Done in 180ms.
27
27
  [tailwind] pnpm run build:tailwind exited with code 0
28
- [babel] Successfully compiled 72 files with Babel (2364ms).
28
+ [babel] Successfully compiled 72 files with Babel (691ms).
29
29
  [babel] pnpm run build:babel exited with code 0
30
30
  [ts] pnpm run build:ts exited with code 0
@@ -1,18 +1,19 @@
1
1
 
2
- > @treelocator/runtime@0.1.2 test /Users/wende/projects/locatorjs/packages/runtime
2
+ > @treelocator/runtime@0.1.5 test /Users/wende/projects/locatorjs/packages/runtime
3
3
  > vitest run
4
4
 
5
5
 
6
6
  RUN v2.1.9 /Users/wende/projects/locatorjs/packages/runtime
7
7
 
8
- ✓ src/functions/cropPath.test.ts (2 tests) 1ms
9
- ✓ src/functions/evalTemplate.test.ts (1 test) 1ms
10
- ✓ src/functions/transformPath.test.ts (3 tests) 2ms
11
- ✓ src/functions/mergeRects.test.ts (1 test) 1ms
12
- ✓ src/functions/getUsableFileName.test.tsx (4 tests) 1ms
8
+ ✓ src/functions/mergeRects.test.ts (1 test) 2ms
9
+ ✓ src/functions/evalTemplate.test.ts (1 test) 2ms
10
+ ✓ src/functions/transformPath.test.ts (3 tests) 3ms
11
+ ✓ src/functions/getUsableFileName.test.tsx (4 tests) 3ms
12
+ ✓ src/functions/formatAncestryChain.test.ts (7 tests) 2ms
13
+ ✓ src/functions/cropPath.test.ts (2 tests) 2ms
13
14
 
14
- Test Files 5 passed (5)
15
- Tests 11 passed (11)
16
- Start at 18:29:05
17
- Duration 687ms (transform 240ms, setup 0ms, collect 278ms, tests 7ms, environment 1ms, prepare 366ms)
15
+ Test Files 6 passed (6)
16
+ Tests 18 passed (18)
17
+ Start at 17:17:22
18
+ Duration 718ms (transform 282ms, setup 0ms, collect 336ms, tests 14ms, environment 1ms, prepare 463ms)
18
19
 
@@ -13,4 +13,5 @@ export declare class HtmlElementTreeNode implements TreeNode {
13
13
  getParent(): TreeNode | null;
14
14
  getSource(): Source | null;
15
15
  getComponent(): TreeNodeComponent | null;
16
+ getOwnerComponents(): TreeNodeComponent[];
16
17
  }
@@ -18,7 +18,7 @@ export class HtmlElementTreeNode {
18
18
  getChildren() {
19
19
  const children = Array.from(this.element.children);
20
20
  return children.map(child => {
21
- if (child instanceof HTMLElement) {
21
+ if (child instanceof HTMLElement || child instanceof SVGElement) {
22
22
  // @ts-ignore
23
23
  return new this.constructor(child);
24
24
  } else {
@@ -40,4 +40,7 @@ export class HtmlElementTreeNode {
40
40
  getComponent() {
41
41
  throw new Error("Method not implemented.");
42
42
  }
43
+ getOwnerComponents() {
44
+ return [];
45
+ }
43
46
  }
@@ -5,7 +5,16 @@ import { HtmlElementTreeNode } from "../HtmlElementTreeNode";
5
5
  export declare function getElementInfo(found: HTMLElement): FullElementInfo | null;
6
6
  export declare class ReactTreeNodeElement extends HtmlElementTreeNode {
7
7
  getSource(): Source | null;
8
+ private fiberToTreeNodeComponent;
8
9
  getComponent(): TreeNodeComponent | null;
10
+ /**
11
+ * Traverse the _debugOwner chain to collect all owner components.
12
+ * This finds wrapper components (like Sidebar) that don't render their own DOM elements
13
+ * but wrap other components (like GlassPanel) that do.
14
+ *
15
+ * Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
16
+ */
17
+ getOwnerComponents(): TreeNodeComponent[];
9
18
  }
10
19
  declare const reactAdapter: AdapterObject;
11
20
  export default reactAdapter;
@@ -63,23 +63,61 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
63
63
  }
64
64
  return null;
65
65
  }
66
+ fiberToTreeNodeComponent(fiber) {
67
+ const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
68
+ return {
69
+ label: fiberLabel.label,
70
+ callLink: fiberLabel.link && {
71
+ fileName: fiberLabel.link.filePath,
72
+ lineNumber: fiberLabel.link.line,
73
+ columnNumber: fiberLabel.link.column,
74
+ projectPath: fiberLabel.link.projectPath
75
+ } || undefined
76
+ };
77
+ }
66
78
  getComponent() {
67
79
  const fiber = findFiberByHtmlElement(this.element, false);
68
80
  const componentFiber = fiber?._debugOwner;
69
81
  if (componentFiber) {
70
- const fiberLabel = getFiberLabel(componentFiber, findDebugSource(componentFiber)?.source);
71
- return {
72
- label: fiberLabel.label,
73
- callLink: fiberLabel.link && {
74
- fileName: fiberLabel.link.filePath,
75
- lineNumber: fiberLabel.link.line,
76
- columnNumber: fiberLabel.link.column,
77
- projectPath: fiberLabel.link.projectPath
78
- } || undefined
79
- };
82
+ return this.fiberToTreeNodeComponent(componentFiber);
80
83
  }
81
84
  return null;
82
85
  }
86
+
87
+ /**
88
+ * Traverse the _debugOwner chain to collect all owner components.
89
+ * This finds wrapper components (like Sidebar) that don't render their own DOM elements
90
+ * but wrap other components (like GlassPanel) that do.
91
+ *
92
+ * Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
93
+ */
94
+ getOwnerComponents() {
95
+ const fiber = findFiberByHtmlElement(this.element, false);
96
+ if (!fiber) return [];
97
+
98
+ // Get the parent DOM element's owner to know when to stop
99
+ const parentElement = this.element.parentElement;
100
+ let parentOwnerFiber = null;
101
+ if (parentElement) {
102
+ const parentFiber = findFiberByHtmlElement(parentElement, false);
103
+ parentOwnerFiber = parentFiber?._debugOwner || null;
104
+ }
105
+ const components = [];
106
+ let currentFiber = fiber._debugOwner;
107
+
108
+ // Traverse up the _debugOwner chain until we hit the parent's owner or run out
109
+ while (currentFiber) {
110
+ // Stop if we've reached the parent DOM element's owner component
111
+ if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
112
+ break;
113
+ }
114
+ components.push(this.fiberToTreeNodeComponent(currentFiber));
115
+ currentFiber = currentFiber._debugOwner || null;
116
+ }
117
+
118
+ // Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
119
+ return components.reverse();
120
+ }
83
121
  }
84
122
  function getTree(element) {
85
123
  const originalRoot = new ReactTreeNodeElement(element);
@@ -39,14 +39,36 @@ function Runtime(props) {
39
39
  setHoldingModKey(e.altKey);
40
40
  }
41
41
  function mouseOverListener(e) {
42
- const target = e.target;
43
- if (target && target instanceof HTMLElement) {
44
- if (isLocatorsOwnElement(target)) {
45
- return;
42
+ // Also update modifier state
43
+ setHoldingModKey(e.altKey);
44
+
45
+ // Use elementsFromPoint to find elements including ones with pointer-events-none
46
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
47
+
48
+ // Find the topmost element with locator data for highlighting
49
+ let element = null;
50
+ for (const el of elementsAtPoint) {
51
+ if (isLocatorsOwnElement(el)) {
52
+ continue;
53
+ }
54
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
55
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
56
+ if (withLocator) {
57
+ element = withLocator;
58
+ break;
59
+ }
60
+ }
61
+ }
62
+
63
+ // Fallback to e.target
64
+ if (!element) {
65
+ const target = e.target;
66
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
67
+ element = target instanceof SVGElement ? target.closest('svg') ?? target : target;
46
68
  }
47
- setCurrentElement(target);
48
- // Also update modifier state
49
- setHoldingModKey(e.altKey);
69
+ }
70
+ if (element && !isLocatorsOwnElement(element)) {
71
+ setCurrentElement(element);
50
72
  }
51
73
  }
52
74
  function mouseDownUpListener(e) {
@@ -62,31 +84,59 @@ function Runtime(props) {
62
84
  if (!e.altKey && !isCombinationModifiersPressed(e) && !locatorActive()) {
63
85
  return;
64
86
  }
65
- const target = e.target;
66
- if (target && target instanceof HTMLElement) {
67
- if (target.shadowRoot) {
68
- return;
87
+
88
+ // Use elementsFromPoint to find all elements at click position,
89
+ // including ones with pointer-events-none (like canvas overlays)
90
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
91
+
92
+ // Find the topmost element with locator data
93
+ let element = null;
94
+ for (const el of elementsAtPoint) {
95
+ if (isLocatorsOwnElement(el)) {
96
+ continue;
69
97
  }
70
- if (isLocatorsOwnElement(target)) {
71
- return;
98
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
99
+ // Check if this element or its closest ancestor has locator data
100
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
101
+ if (withLocator) {
102
+ element = withLocator;
103
+ break;
104
+ }
72
105
  }
73
- e.preventDefault();
74
- e.stopPropagation();
106
+ }
75
107
 
76
- // Copy ancestry to clipboard on alt+click
77
- const treeNode = createTreeNode(target, props.adapterId);
78
- if (treeNode) {
79
- const ancestry = collectAncestry(treeNode);
80
- const formatted = formatAncestryChain(ancestry);
81
- navigator.clipboard.writeText(formatted).then(() => {
82
- setToastMessage("Copied to clipboard");
83
- });
108
+ // Fallback to e.target if elementsFromPoint didn't find anything
109
+ if (!element) {
110
+ const target = e.target;
111
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
112
+ element = target instanceof SVGElement ? target.closest('[data-locatorjs-id], [data-locatorjs]') ?? target.closest('svg') ?? target : target;
84
113
  }
114
+ }
115
+ if (!element) {
116
+ return;
117
+ }
118
+ if (element instanceof HTMLElement && element.shadowRoot) {
119
+ return;
120
+ }
121
+ if (isLocatorsOwnElement(element)) {
122
+ return;
123
+ }
124
+ e.preventDefault();
125
+ e.stopPropagation();
85
126
 
86
- // Deactivate toggle after click
87
- if (locatorActive()) {
88
- setLocatorActive(false);
89
- }
127
+ // Copy ancestry to clipboard on alt+click
128
+ const treeNode = createTreeNode(element, props.adapterId);
129
+ if (treeNode) {
130
+ const ancestry = collectAncestry(treeNode);
131
+ const formatted = formatAncestryChain(ancestry);
132
+ navigator.clipboard.writeText(formatted).then(() => {
133
+ setToastMessage("Copied to clipboard");
134
+ });
135
+ }
136
+
137
+ // Deactivate toggle after click
138
+ if (locatorActive()) {
139
+ setLocatorActive(false);
90
140
  }
91
141
  }
92
142
  function scrollListener() {
@@ -1,4 +1,9 @@
1
1
  import { TreeNode } from "../types/TreeNode";
2
+ export interface OwnerComponentInfo {
3
+ name: string;
4
+ filePath?: string;
5
+ line?: number;
6
+ }
2
7
  export interface AncestryItem {
3
8
  elementName: string;
4
9
  componentName?: string;
@@ -6,6 +11,8 @@ export interface AncestryItem {
6
11
  line?: number;
7
12
  id?: string;
8
13
  nthChild?: number;
14
+ /** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
15
+ ownerComponents?: OwnerComponentInfo[];
9
16
  }
10
17
  export declare function collectAncestry(node: TreeNode): AncestryItem[];
11
18
  export declare function formatAncestryChain(items: AncestryItem[]): string;
@@ -19,6 +19,13 @@ function getNthChildIfAmbiguous(element) {
19
19
  const index = siblings.indexOf(element);
20
20
  return index + 1; // 1-indexed for CSS nth-child compatibility
21
21
  }
22
+ function treeNodeComponentToOwnerInfo(comp) {
23
+ return {
24
+ name: comp.label,
25
+ filePath: comp.callLink?.fileName,
26
+ line: comp.callLink?.lineNumber
27
+ };
28
+ }
22
29
  export function collectAncestry(node) {
23
30
  const items = [];
24
31
  let current = node;
@@ -28,7 +35,6 @@ export function collectAncestry(node) {
28
35
  current = current.getParent();
29
36
  continue;
30
37
  }
31
- const component = current.getComponent();
32
38
  const source = current.getSource();
33
39
  const item = {
34
40
  elementName: current.name
@@ -47,11 +53,27 @@ export function collectAncestry(node) {
47
53
  }
48
54
  }
49
55
  }
50
- if (component) {
51
- item.componentName = component.label;
52
- if (component.callLink) {
53
- item.filePath = component.callLink.fileName;
54
- item.line = component.callLink.lineNumber;
56
+
57
+ // Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
58
+ const ownerComponents = current.getOwnerComponents();
59
+ const outermost = ownerComponents[0];
60
+ if (outermost) {
61
+ item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
62
+ // Use outermost component as the primary component name
63
+ item.componentName = outermost.label;
64
+ if (outermost.callLink) {
65
+ item.filePath = outermost.callLink.fileName;
66
+ item.line = outermost.callLink.lineNumber;
67
+ }
68
+ } else {
69
+ // Fallback to single component if getOwnerComponents not available
70
+ const component = current.getComponent();
71
+ if (component) {
72
+ item.componentName = component.label;
73
+ if (component.callLink) {
74
+ item.filePath = component.callLink.fileName;
75
+ item.line = component.callLink.lineNumber;
76
+ }
55
77
  }
56
78
  }
57
79
  if (!item.filePath && source) {
@@ -88,7 +110,12 @@ export function formatAncestryChain(items) {
88
110
  selector += `#${item.id}`;
89
111
  }
90
112
  let description = selector;
91
- if (item.componentName) {
113
+
114
+ // Show all owner components if available (Sidebar > GlassPanel)
115
+ if (item.ownerComponents && item.ownerComponents.length > 0) {
116
+ const componentChain = item.ownerComponents.map(c => c.name).join(" > ");
117
+ description = `${selector} in ${componentChain}`;
118
+ } else if (item.componentName) {
92
119
  description = `${selector} in ${item.componentName}`;
93
120
  }
94
121
  const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
@@ -78,4 +78,45 @@ describe("formatAncestryChain", () => {
78
78
  const result = formatAncestryChain([]);
79
79
  expect(result).toBe("");
80
80
  });
81
+ it("shows all owner components when ownerComponents is provided", () => {
82
+ const items = [{
83
+ elementName: "div",
84
+ id: "sidebar-panel",
85
+ componentName: "Sidebar",
86
+ filePath: "src/components/game/Sidebar.jsx",
87
+ line: 78,
88
+ ownerComponents: [{
89
+ name: "Sidebar",
90
+ filePath: "src/components/game/Sidebar.jsx",
91
+ line: 78
92
+ }, {
93
+ name: "GlassPanel",
94
+ filePath: "src/components/common/GlassPanel.jsx",
95
+ line: 39
96
+ }]
97
+ }, {
98
+ elementName: "div",
99
+ componentName: "App",
100
+ filePath: "src/App.jsx",
101
+ line: 104
102
+ }];
103
+ const result = formatAncestryChain(items);
104
+ expect(result).toBe(`div in App at src/App.jsx:104
105
+ └─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`);
106
+ });
107
+ it("shows single owner component without arrow when only one in chain", () => {
108
+ const items = [{
109
+ elementName: "button",
110
+ componentName: "Button",
111
+ ownerComponents: [{
112
+ name: "Button",
113
+ filePath: "src/Button.tsx",
114
+ line: 10
115
+ }],
116
+ filePath: "src/Button.tsx",
117
+ line: 10
118
+ }];
119
+ const result = formatAncestryChain(items);
120
+ expect(result).toBe("button in Button at src/Button.tsx:10");
121
+ });
81
122
  });
@@ -8,6 +8,12 @@ export interface TreeNode {
8
8
  getChildren(): TreeNode[];
9
9
  getSource(): Source | null;
10
10
  getComponent(): TreeNodeComponent | null;
11
+ /**
12
+ * Get all owner components in the ownership chain.
13
+ * For React: traverses _debugOwner chain until hitting parent DOM element's owner.
14
+ * Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
15
+ */
16
+ getOwnerComponents(): TreeNodeComponent[];
11
17
  }
12
18
  export type TreeNodeComponent = {
13
19
  label: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treelocator/runtime",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "TreeLocatorJS runtime for component ancestry tracking. Alt+click any element to copy its component tree to clipboard. Exposes window.__treelocator__ API for browser automation (Playwright, Puppeteer, Selenium, Cypress).",
5
5
  "keywords": [
6
6
  "locator",
@@ -72,5 +72,5 @@
72
72
  "directory": "packages/runtime"
73
73
  },
74
74
  "license": "MIT",
75
- "gitHead": "49e1500d0dee128e4e7b549a012f406cb0b55568"
75
+ "gitHead": "233b05abf1f13da9e1d49b73db62bb8561636618"
76
76
  }
@@ -25,9 +25,9 @@ export class HtmlElementTreeNode implements TreeNode {
25
25
  const children = Array.from(this.element.children);
26
26
  return children
27
27
  .map((child) => {
28
- if (child instanceof HTMLElement) {
28
+ if (child instanceof HTMLElement || child instanceof SVGElement) {
29
29
  // @ts-ignore
30
- return new this.constructor(child);
30
+ return new this.constructor(child as HTMLElement);
31
31
  } else {
32
32
  return null;
33
33
  }
@@ -48,4 +48,7 @@ export class HtmlElementTreeNode implements TreeNode {
48
48
  getComponent(): TreeNodeComponent | null {
49
49
  throw new Error("Method not implemented.");
50
50
  }
51
+ getOwnerComponents(): TreeNodeComponent[] {
52
+ return [];
53
+ }
51
54
  }
@@ -81,30 +81,68 @@ export class ReactTreeNodeElement extends HtmlElementTreeNode {
81
81
  }
82
82
  return null;
83
83
  }
84
+
85
+ private fiberToTreeNodeComponent(fiber: Fiber): TreeNodeComponent {
86
+ const fiberLabel = getFiberLabel(fiber, findDebugSource(fiber)?.source);
87
+ return {
88
+ label: fiberLabel.label,
89
+ callLink:
90
+ (fiberLabel.link && {
91
+ fileName: fiberLabel.link.filePath,
92
+ lineNumber: fiberLabel.link.line,
93
+ columnNumber: fiberLabel.link.column,
94
+ projectPath: fiberLabel.link.projectPath,
95
+ }) ||
96
+ undefined,
97
+ };
98
+ }
99
+
84
100
  getComponent(): TreeNodeComponent | null {
85
101
  const fiber = findFiberByHtmlElement(this.element, false);
86
102
  const componentFiber = fiber?._debugOwner;
87
103
 
88
104
  if (componentFiber) {
89
- const fiberLabel = getFiberLabel(
90
- componentFiber,
91
- findDebugSource(componentFiber)?.source
92
- );
93
-
94
- return {
95
- label: fiberLabel.label,
96
- callLink:
97
- (fiberLabel.link && {
98
- fileName: fiberLabel.link.filePath,
99
- lineNumber: fiberLabel.link.line,
100
- columnNumber: fiberLabel.link.column,
101
- projectPath: fiberLabel.link.projectPath,
102
- }) ||
103
- undefined,
104
- };
105
+ return this.fiberToTreeNodeComponent(componentFiber);
105
106
  }
106
107
  return null;
107
108
  }
109
+
110
+ /**
111
+ * Traverse the _debugOwner chain to collect all owner components.
112
+ * This finds wrapper components (like Sidebar) that don't render their own DOM elements
113
+ * but wrap other components (like GlassPanel) that do.
114
+ *
115
+ * Returns array from outermost owner (Sidebar) to innermost (GlassPanel).
116
+ */
117
+ getOwnerComponents(): TreeNodeComponent[] {
118
+ const fiber = findFiberByHtmlElement(this.element, false);
119
+ if (!fiber) return [];
120
+
121
+ // Get the parent DOM element's owner to know when to stop
122
+ const parentElement = this.element.parentElement;
123
+ let parentOwnerFiber: Fiber | null = null;
124
+ if (parentElement) {
125
+ const parentFiber = findFiberByHtmlElement(parentElement, false);
126
+ parentOwnerFiber = parentFiber?._debugOwner || null;
127
+ }
128
+
129
+ const components: TreeNodeComponent[] = [];
130
+ let currentFiber = fiber._debugOwner;
131
+
132
+ // Traverse up the _debugOwner chain until we hit the parent's owner or run out
133
+ while (currentFiber) {
134
+ // Stop if we've reached the parent DOM element's owner component
135
+ if (parentOwnerFiber && currentFiber === parentOwnerFiber) {
136
+ break;
137
+ }
138
+
139
+ components.push(this.fiberToTreeNodeComponent(currentFiber));
140
+ currentFiber = currentFiber._debugOwner || null;
141
+ }
142
+
143
+ // Reverse so outermost (Sidebar) comes first, innermost (GlassPanel) last
144
+ return components.reverse();
145
+ }
108
146
  }
109
147
 
110
148
  function getTree(element: HTMLElement): TreeState | null {
@@ -48,15 +48,39 @@ function Runtime(props: RuntimeProps) {
48
48
  }
49
49
 
50
50
  function mouseOverListener(e: MouseEvent) {
51
- const target = e.target;
52
- if (target && target instanceof HTMLElement) {
53
- if (isLocatorsOwnElement(target)) {
54
- return;
51
+ // Also update modifier state
52
+ setHoldingModKey(e.altKey);
53
+
54
+ // Use elementsFromPoint to find elements including ones with pointer-events-none
55
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
56
+
57
+ // Find the topmost element with locator data for highlighting
58
+ let element: HTMLElement | null = null;
59
+ for (const el of elementsAtPoint) {
60
+ if (isLocatorsOwnElement(el as HTMLElement)) {
61
+ continue;
62
+ }
63
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
64
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
65
+ if (withLocator) {
66
+ element = withLocator as HTMLElement;
67
+ break;
68
+ }
69
+ }
70
+ }
71
+
72
+ // Fallback to e.target
73
+ if (!element) {
74
+ const target = e.target;
75
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
76
+ element = target instanceof SVGElement
77
+ ? (target.closest('svg') as HTMLElement | null) ?? (target as unknown as HTMLElement)
78
+ : target;
55
79
  }
80
+ }
56
81
 
57
- setCurrentElement(target);
58
- // Also update modifier state
59
- setHoldingModKey(e.altKey);
82
+ if (element && !isLocatorsOwnElement(element)) {
83
+ setCurrentElement(element);
60
84
  }
61
85
  }
62
86
 
@@ -76,33 +100,66 @@ function Runtime(props: RuntimeProps) {
76
100
  return;
77
101
  }
78
102
 
79
- const target = e.target;
80
- if (target && target instanceof HTMLElement) {
81
- if (target.shadowRoot) {
82
- return;
103
+ // Use elementsFromPoint to find all elements at click position,
104
+ // including ones with pointer-events-none (like canvas overlays)
105
+ const elementsAtPoint = document.elementsFromPoint(e.clientX, e.clientY);
106
+
107
+ // Find the topmost element with locator data
108
+ let element: Element | null = null;
109
+ for (const el of elementsAtPoint) {
110
+ if (isLocatorsOwnElement(el as HTMLElement)) {
111
+ continue;
83
112
  }
113
+ if (el instanceof HTMLElement || el instanceof SVGElement) {
114
+ // Check if this element or its closest ancestor has locator data
115
+ const withLocator = el.closest('[data-locatorjs-id], [data-locatorjs]');
116
+ if (withLocator) {
117
+ element = withLocator;
118
+ break;
119
+ }
120
+ }
121
+ }
84
122
 
85
- if (isLocatorsOwnElement(target)) {
86
- return;
123
+ // Fallback to e.target if elementsFromPoint didn't find anything
124
+ if (!element) {
125
+ const target = e.target;
126
+ if (target && (target instanceof HTMLElement || target instanceof SVGElement)) {
127
+ element = target instanceof SVGElement
128
+ ? (target.closest('[data-locatorjs-id], [data-locatorjs]') as Element | null) ??
129
+ (target.closest('svg') as Element | null) ??
130
+ target
131
+ : target;
87
132
  }
133
+ }
88
134
 
89
- e.preventDefault();
90
- e.stopPropagation();
135
+ if (!element) {
136
+ return;
137
+ }
91
138
 
92
- // Copy ancestry to clipboard on alt+click
93
- const treeNode = createTreeNode(target, props.adapterId);
94
- if (treeNode) {
95
- const ancestry = collectAncestry(treeNode);
96
- const formatted = formatAncestryChain(ancestry);
97
- navigator.clipboard.writeText(formatted).then(() => {
98
- setToastMessage("Copied to clipboard");
99
- });
100
- }
139
+ if (element instanceof HTMLElement && element.shadowRoot) {
140
+ return;
141
+ }
101
142
 
102
- // Deactivate toggle after click
103
- if (locatorActive()) {
104
- setLocatorActive(false);
105
- }
143
+ if (isLocatorsOwnElement(element as HTMLElement)) {
144
+ return;
145
+ }
146
+
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+
150
+ // Copy ancestry to clipboard on alt+click
151
+ const treeNode = createTreeNode(element as HTMLElement, props.adapterId);
152
+ if (treeNode) {
153
+ const ancestry = collectAncestry(treeNode);
154
+ const formatted = formatAncestryChain(ancestry);
155
+ navigator.clipboard.writeText(formatted).then(() => {
156
+ setToastMessage("Copied to clipboard");
157
+ });
158
+ }
159
+
160
+ // Deactivate toggle after click
161
+ if (locatorActive()) {
162
+ setLocatorActive(false);
106
163
  }
107
164
  }
108
165
 
@@ -91,4 +91,50 @@ describe("formatAncestryChain", () => {
91
91
  const result = formatAncestryChain([]);
92
92
  expect(result).toBe("");
93
93
  });
94
+
95
+ it("shows all owner components when ownerComponents is provided", () => {
96
+ const items: AncestryItem[] = [
97
+ {
98
+ elementName: "div",
99
+ id: "sidebar-panel",
100
+ componentName: "Sidebar",
101
+ filePath: "src/components/game/Sidebar.jsx",
102
+ line: 78,
103
+ ownerComponents: [
104
+ {
105
+ name: "Sidebar",
106
+ filePath: "src/components/game/Sidebar.jsx",
107
+ line: 78,
108
+ },
109
+ {
110
+ name: "GlassPanel",
111
+ filePath: "src/components/common/GlassPanel.jsx",
112
+ line: 39,
113
+ },
114
+ ],
115
+ },
116
+ { elementName: "div", componentName: "App", filePath: "src/App.jsx", line: 104 },
117
+ ];
118
+
119
+ const result = formatAncestryChain(items);
120
+ expect(result).toBe(
121
+ `div in App at src/App.jsx:104
122
+ └─ div#sidebar-panel in Sidebar > GlassPanel at src/components/game/Sidebar.jsx:78`
123
+ );
124
+ });
125
+
126
+ it("shows single owner component without arrow when only one in chain", () => {
127
+ const items: AncestryItem[] = [
128
+ {
129
+ elementName: "button",
130
+ componentName: "Button",
131
+ ownerComponents: [{ name: "Button", filePath: "src/Button.tsx", line: 10 }],
132
+ filePath: "src/Button.tsx",
133
+ line: 10,
134
+ },
135
+ ];
136
+
137
+ const result = formatAncestryChain(items);
138
+ expect(result).toBe("button in Button at src/Button.tsx:10");
139
+ });
94
140
  });
@@ -1,4 +1,10 @@
1
- import { TreeNode, TreeNodeElement } from "../types/TreeNode";
1
+ import { TreeNode, TreeNodeComponent, TreeNodeElement } from "../types/TreeNode";
2
+
3
+ export interface OwnerComponentInfo {
4
+ name: string;
5
+ filePath?: string;
6
+ line?: number;
7
+ }
2
8
 
3
9
  export interface AncestryItem {
4
10
  elementName: string;
@@ -7,6 +13,8 @@ export interface AncestryItem {
7
13
  line?: number;
8
14
  id?: string;
9
15
  nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
16
+ /** All owner components from outermost (Sidebar) to innermost (GlassPanel) */
17
+ ownerComponents?: OwnerComponentInfo[];
10
18
  }
11
19
 
12
20
  // Elements to exclude from ancestry (not useful for debugging)
@@ -36,6 +44,16 @@ function getNthChildIfAmbiguous(element: Element): number | undefined {
36
44
  return index + 1; // 1-indexed for CSS nth-child compatibility
37
45
  }
38
46
 
47
+ function treeNodeComponentToOwnerInfo(
48
+ comp: TreeNodeComponent
49
+ ): OwnerComponentInfo {
50
+ return {
51
+ name: comp.label,
52
+ filePath: comp.callLink?.fileName,
53
+ line: comp.callLink?.lineNumber,
54
+ };
55
+ }
56
+
39
57
  export function collectAncestry(node: TreeNode): AncestryItem[] {
40
58
  const items: AncestryItem[] = [];
41
59
  let current: TreeNode | null = node;
@@ -47,7 +65,6 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
47
65
  continue;
48
66
  }
49
67
 
50
- const component = current.getComponent();
51
68
  const source = current.getSource();
52
69
 
53
70
  const item: AncestryItem = {
@@ -68,11 +85,26 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
68
85
  }
69
86
  }
70
87
 
71
- if (component) {
72
- item.componentName = component.label;
73
- if (component.callLink) {
74
- item.filePath = component.callLink.fileName;
75
- item.line = component.callLink.lineNumber;
88
+ // Get all owner components (from outermost like Sidebar to innermost like GlassPanel)
89
+ const ownerComponents = current.getOwnerComponents();
90
+ const outermost = ownerComponents[0];
91
+ if (outermost) {
92
+ item.ownerComponents = ownerComponents.map(treeNodeComponentToOwnerInfo);
93
+ // Use outermost component as the primary component name
94
+ item.componentName = outermost.label;
95
+ if (outermost.callLink) {
96
+ item.filePath = outermost.callLink.fileName;
97
+ item.line = outermost.callLink.lineNumber;
98
+ }
99
+ } else {
100
+ // Fallback to single component if getOwnerComponents not available
101
+ const component = current.getComponent();
102
+ if (component) {
103
+ item.componentName = component.label;
104
+ if (component.callLink) {
105
+ item.filePath = component.callLink.fileName;
106
+ item.line = component.callLink.lineNumber;
107
+ }
76
108
  }
77
109
  }
78
110
 
@@ -116,7 +148,12 @@ export function formatAncestryChain(items: AncestryItem[]): string {
116
148
  }
117
149
 
118
150
  let description = selector;
119
- if (item.componentName) {
151
+
152
+ // Show all owner components if available (Sidebar > GlassPanel)
153
+ if (item.ownerComponents && item.ownerComponents.length > 0) {
154
+ const componentChain = item.ownerComponents.map((c) => c.name).join(" > ");
155
+ description = `${selector} in ${componentChain}`;
156
+ } else if (item.componentName) {
120
157
  description = `${selector} in ${item.componentName}`;
121
158
  }
122
159
 
@@ -9,6 +9,12 @@ export interface TreeNode {
9
9
  getChildren(): TreeNode[];
10
10
  getSource(): Source | null;
11
11
  getComponent(): TreeNodeComponent | null;
12
+ /**
13
+ * Get all owner components in the ownership chain.
14
+ * For React: traverses _debugOwner chain until hitting parent DOM element's owner.
15
+ * Returns array from outermost (user's component like Sidebar) to innermost (wrapper like GlassPanel).
16
+ */
17
+ getOwnerComponents(): TreeNodeComponent[];
12
18
  }
13
19
 
14
20
  export type TreeNodeComponent = {