@treelocator/runtime 0.1.3 → 0.1.5
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.
- package/.turbo/turbo-build.log +13 -13
- package/dist/adapters/jsx/jsxAdapter.js +4 -4
- package/dist/functions/formatAncestryChain.d.ts +2 -0
- package/dist/functions/formatAncestryChain.js +44 -2
- package/dist/functions/formatAncestryChain.test.d.ts +1 -0
- package/dist/functions/formatAncestryChain.test.js +81 -0
- package/package.json +2 -2
- package/src/adapters/jsx/jsxAdapter.ts +4 -4
- package/src/functions/formatAncestryChain.test.ts +94 -0
- package/src/functions/formatAncestryChain.ts +52 -3
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
|
|
2
|
-
> @treelocator/runtime@0.1.
|
|
2
|
+
> @treelocator/runtime@0.1.5 build /Users/wende/projects/locatorjs/packages/runtime
|
|
3
3
|
> concurrently pnpm:build:*
|
|
4
4
|
|
|
5
|
-
[tailwind]
|
|
6
|
-
[tailwind] > @treelocator/runtime@0.1.3 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
|
|
7
|
-
[tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
|
|
8
|
-
[tailwind]
|
|
9
|
-
[babel]
|
|
10
|
-
[babel] > @treelocator/runtime@0.1.3 build:babel /Users/wende/projects/locatorjs/packages/runtime
|
|
11
|
-
[babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
|
|
12
|
-
[babel]
|
|
13
5
|
[ts]
|
|
14
|
-
[ts] > @treelocator/runtime@0.1.
|
|
6
|
+
[ts] > @treelocator/runtime@0.1.5 build:ts /Users/wende/projects/locatorjs/packages/runtime
|
|
15
7
|
[ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
|
|
16
8
|
[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]
|
|
17
13
|
[wrapImage]
|
|
18
|
-
[wrapImage] > @treelocator/runtime@0.1.
|
|
14
|
+
[wrapImage] > @treelocator/runtime@0.1.5 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
|
|
19
15
|
[wrapImage] > node ./scripts/wrapImage.js
|
|
20
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]
|
|
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
|
|
26
|
+
[tailwind] Done in 294ms.
|
|
27
27
|
[tailwind] pnpm run build:tailwind exited with code 0
|
|
28
|
-
[babel] Successfully compiled
|
|
28
|
+
[babel] Successfully compiled 72 files with Babel (2364ms).
|
|
29
29
|
[babel] pnpm run build:babel exited with code 0
|
|
30
30
|
[ts] pnpm run build:ts exited with code 0
|
|
@@ -179,13 +179,13 @@ function getTree(element) {
|
|
|
179
179
|
function getParentsPaths(element) {
|
|
180
180
|
const path = [];
|
|
181
181
|
let currentElement = element;
|
|
182
|
-
let
|
|
182
|
+
let previousElementKey = null;
|
|
183
183
|
do {
|
|
184
184
|
if (currentElement) {
|
|
185
185
|
const info = getElementInfo(currentElement);
|
|
186
|
-
const
|
|
187
|
-
if (info &&
|
|
188
|
-
|
|
186
|
+
const currentElementKey = JSON.stringify(info?.thisElement.link);
|
|
187
|
+
if (info && currentElementKey !== previousElementKey) {
|
|
188
|
+
previousElementKey = currentElementKey;
|
|
189
189
|
const link = info.thisElement.link;
|
|
190
190
|
const label = info.thisElement.label;
|
|
191
191
|
if (link) {
|
|
@@ -4,6 +4,8 @@ export interface AncestryItem {
|
|
|
4
4
|
componentName?: string;
|
|
5
5
|
filePath?: string;
|
|
6
6
|
line?: number;
|
|
7
|
+
id?: string;
|
|
8
|
+
nthChild?: number;
|
|
7
9
|
}
|
|
8
10
|
export declare function collectAncestry(node: TreeNode): AncestryItem[];
|
|
9
11
|
export declare function formatAncestryChain(items: AncestryItem[]): string;
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
// Elements to exclude from ancestry (not useful for debugging)
|
|
2
2
|
const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
|
|
3
|
+
function isTreeNodeElement(node) {
|
|
4
|
+
return "getElement" in node && typeof node.getElement === "function";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Calculate nth-child position if there are multiple siblings of the same type.
|
|
9
|
+
* Returns undefined if the element is unique among its siblings, or 1-indexed position otherwise.
|
|
10
|
+
*/
|
|
11
|
+
function getNthChildIfAmbiguous(element) {
|
|
12
|
+
const parent = element.parentElement;
|
|
13
|
+
if (!parent) return undefined;
|
|
14
|
+
const tagName = element.tagName;
|
|
15
|
+
const siblings = Array.from(parent.children).filter(child => child.tagName === tagName);
|
|
16
|
+
|
|
17
|
+
// Only return position if there are multiple siblings of the same type
|
|
18
|
+
if (siblings.length <= 1) return undefined;
|
|
19
|
+
const index = siblings.indexOf(element);
|
|
20
|
+
return index + 1; // 1-indexed for CSS nth-child compatibility
|
|
21
|
+
}
|
|
3
22
|
export function collectAncestry(node) {
|
|
4
23
|
const items = [];
|
|
5
24
|
let current = node;
|
|
@@ -14,6 +33,20 @@ export function collectAncestry(node) {
|
|
|
14
33
|
const item = {
|
|
15
34
|
elementName: current.name
|
|
16
35
|
};
|
|
36
|
+
|
|
37
|
+
// Extract ID and nth-child from the DOM element if available
|
|
38
|
+
if (isTreeNodeElement(current)) {
|
|
39
|
+
const element = current.getElement();
|
|
40
|
+
if (element instanceof Element) {
|
|
41
|
+
if (element.id) {
|
|
42
|
+
item.id = element.id;
|
|
43
|
+
}
|
|
44
|
+
const nthChild = getNthChildIfAmbiguous(element);
|
|
45
|
+
if (nthChild !== undefined) {
|
|
46
|
+
item.nthChild = nthChild;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
17
50
|
if (component) {
|
|
18
51
|
item.componentName = component.label;
|
|
19
52
|
if (component.callLink) {
|
|
@@ -45,9 +78,18 @@ export function formatAncestryChain(items) {
|
|
|
45
78
|
reversed.forEach((item, index) => {
|
|
46
79
|
const indent = " ".repeat(index);
|
|
47
80
|
const prefix = index === 0 ? "" : "└─ ";
|
|
48
|
-
|
|
81
|
+
|
|
82
|
+
// Build element selector: elementName:nth-child(n)#id
|
|
83
|
+
let selector = item.elementName;
|
|
84
|
+
if (item.nthChild !== undefined) {
|
|
85
|
+
selector += `:nth-child(${item.nthChild})`;
|
|
86
|
+
}
|
|
87
|
+
if (item.id) {
|
|
88
|
+
selector += `#${item.id}`;
|
|
89
|
+
}
|
|
90
|
+
let description = selector;
|
|
49
91
|
if (item.componentName) {
|
|
50
|
-
description = `${
|
|
92
|
+
description = `${selector} in ${item.componentName}`;
|
|
51
93
|
}
|
|
52
94
|
const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
|
|
53
95
|
lines.push(`${indent}${prefix}${description}${location}`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatAncestryChain } from "./formatAncestryChain";
|
|
3
|
+
describe("formatAncestryChain", () => {
|
|
4
|
+
it("formats basic ancestry without ID or nth-child", () => {
|
|
5
|
+
const items = [{
|
|
6
|
+
elementName: "button",
|
|
7
|
+
componentName: "Button"
|
|
8
|
+
}, {
|
|
9
|
+
elementName: "div",
|
|
10
|
+
componentName: "App"
|
|
11
|
+
}];
|
|
12
|
+
const result = formatAncestryChain(items);
|
|
13
|
+
expect(result).toBe(`div in App
|
|
14
|
+
└─ button in Button`);
|
|
15
|
+
});
|
|
16
|
+
it("includes ID when present", () => {
|
|
17
|
+
const items = [{
|
|
18
|
+
elementName: "button",
|
|
19
|
+
componentName: "Button",
|
|
20
|
+
id: "submit-btn"
|
|
21
|
+
}, {
|
|
22
|
+
elementName: "div",
|
|
23
|
+
componentName: "App"
|
|
24
|
+
}];
|
|
25
|
+
const result = formatAncestryChain(items);
|
|
26
|
+
expect(result).toBe(`div in App
|
|
27
|
+
└─ button#submit-btn in Button`);
|
|
28
|
+
});
|
|
29
|
+
it("includes nth-child when present", () => {
|
|
30
|
+
const items = [{
|
|
31
|
+
elementName: "li",
|
|
32
|
+
componentName: "ListItem",
|
|
33
|
+
nthChild: 3
|
|
34
|
+
}, {
|
|
35
|
+
elementName: "ul",
|
|
36
|
+
componentName: "List"
|
|
37
|
+
}];
|
|
38
|
+
const result = formatAncestryChain(items);
|
|
39
|
+
expect(result).toBe(`ul in List
|
|
40
|
+
└─ li:nth-child(3) in ListItem`);
|
|
41
|
+
});
|
|
42
|
+
it("includes both nth-child and ID when present", () => {
|
|
43
|
+
const items = [{
|
|
44
|
+
elementName: "li",
|
|
45
|
+
componentName: "ListItem",
|
|
46
|
+
nthChild: 2,
|
|
47
|
+
id: "special-item"
|
|
48
|
+
}, {
|
|
49
|
+
elementName: "ul",
|
|
50
|
+
componentName: "List"
|
|
51
|
+
}];
|
|
52
|
+
const result = formatAncestryChain(items);
|
|
53
|
+
expect(result).toBe(`ul in List
|
|
54
|
+
└─ li:nth-child(2)#special-item in ListItem`);
|
|
55
|
+
});
|
|
56
|
+
it("includes file location when present", () => {
|
|
57
|
+
const items = [{
|
|
58
|
+
elementName: "button",
|
|
59
|
+
componentName: "Button",
|
|
60
|
+
id: "save",
|
|
61
|
+
filePath: "src/Button.tsx",
|
|
62
|
+
line: 42
|
|
63
|
+
}];
|
|
64
|
+
const result = formatAncestryChain(items);
|
|
65
|
+
expect(result).toBe("button#save in Button at src/Button.tsx:42");
|
|
66
|
+
});
|
|
67
|
+
it("formats element without component name", () => {
|
|
68
|
+
const items = [{
|
|
69
|
+
elementName: "div",
|
|
70
|
+
id: "main",
|
|
71
|
+
filePath: "src/App.tsx",
|
|
72
|
+
line: 10
|
|
73
|
+
}];
|
|
74
|
+
const result = formatAncestryChain(items);
|
|
75
|
+
expect(result).toBe("div#main at src/App.tsx:10");
|
|
76
|
+
});
|
|
77
|
+
it("returns empty string for empty items", () => {
|
|
78
|
+
const result = formatAncestryChain([]);
|
|
79
|
+
expect(result).toBe("");
|
|
80
|
+
});
|
|
81
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@treelocator/runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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": "
|
|
75
|
+
"gitHead": "49e1500d0dee128e4e7b549a012f406cb0b55568"
|
|
76
76
|
}
|
|
@@ -240,14 +240,14 @@ function getTree(element: HTMLElement): TreeState | null {
|
|
|
240
240
|
function getParentsPaths(element: HTMLElement): ParentPathItem[] {
|
|
241
241
|
const path: ParentPathItem[] = [];
|
|
242
242
|
let currentElement: HTMLElement | null = element;
|
|
243
|
-
let
|
|
243
|
+
let previousElementKey: string | null = null;
|
|
244
244
|
|
|
245
245
|
do {
|
|
246
246
|
if (currentElement) {
|
|
247
247
|
const info = getElementInfo(currentElement);
|
|
248
|
-
const
|
|
249
|
-
if (info &&
|
|
250
|
-
|
|
248
|
+
const currentElementKey = JSON.stringify(info?.thisElement.link);
|
|
249
|
+
if (info && currentElementKey !== previousElementKey) {
|
|
250
|
+
previousElementKey = currentElementKey;
|
|
251
251
|
|
|
252
252
|
const link = info.thisElement.link;
|
|
253
253
|
const label = info.thisElement.label;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
AncestryItem,
|
|
4
|
+
formatAncestryChain,
|
|
5
|
+
collectAncestry,
|
|
6
|
+
} from "./formatAncestryChain";
|
|
7
|
+
|
|
8
|
+
describe("formatAncestryChain", () => {
|
|
9
|
+
it("formats basic ancestry without ID or nth-child", () => {
|
|
10
|
+
const items: AncestryItem[] = [
|
|
11
|
+
{ elementName: "button", componentName: "Button" },
|
|
12
|
+
{ elementName: "div", componentName: "App" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const result = formatAncestryChain(items);
|
|
16
|
+
expect(result).toBe(
|
|
17
|
+
`div in App
|
|
18
|
+
└─ button in Button`
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("includes ID when present", () => {
|
|
23
|
+
const items: AncestryItem[] = [
|
|
24
|
+
{ elementName: "button", componentName: "Button", id: "submit-btn" },
|
|
25
|
+
{ elementName: "div", componentName: "App" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const result = formatAncestryChain(items);
|
|
29
|
+
expect(result).toBe(
|
|
30
|
+
`div in App
|
|
31
|
+
└─ button#submit-btn in Button`
|
|
32
|
+
);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("includes nth-child when present", () => {
|
|
36
|
+
const items: AncestryItem[] = [
|
|
37
|
+
{ elementName: "li", componentName: "ListItem", nthChild: 3 },
|
|
38
|
+
{ elementName: "ul", componentName: "List" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const result = formatAncestryChain(items);
|
|
42
|
+
expect(result).toBe(
|
|
43
|
+
`ul in List
|
|
44
|
+
└─ li:nth-child(3) in ListItem`
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("includes both nth-child and ID when present", () => {
|
|
49
|
+
const items: AncestryItem[] = [
|
|
50
|
+
{
|
|
51
|
+
elementName: "li",
|
|
52
|
+
componentName: "ListItem",
|
|
53
|
+
nthChild: 2,
|
|
54
|
+
id: "special-item",
|
|
55
|
+
},
|
|
56
|
+
{ elementName: "ul", componentName: "List" },
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const result = formatAncestryChain(items);
|
|
60
|
+
expect(result).toBe(
|
|
61
|
+
`ul in List
|
|
62
|
+
└─ li:nth-child(2)#special-item in ListItem`
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("includes file location when present", () => {
|
|
67
|
+
const items: AncestryItem[] = [
|
|
68
|
+
{
|
|
69
|
+
elementName: "button",
|
|
70
|
+
componentName: "Button",
|
|
71
|
+
id: "save",
|
|
72
|
+
filePath: "src/Button.tsx",
|
|
73
|
+
line: 42,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const result = formatAncestryChain(items);
|
|
78
|
+
expect(result).toBe("button#save in Button at src/Button.tsx:42");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("formats element without component name", () => {
|
|
82
|
+
const items: AncestryItem[] = [
|
|
83
|
+
{ elementName: "div", id: "main", filePath: "src/App.tsx", line: 10 },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const result = formatAncestryChain(items);
|
|
87
|
+
expect(result).toBe("div#main at src/App.tsx:10");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns empty string for empty items", () => {
|
|
91
|
+
const result = formatAncestryChain([]);
|
|
92
|
+
expect(result).toBe("");
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -1,15 +1,41 @@
|
|
|
1
|
-
import { TreeNode } from "../types/TreeNode";
|
|
1
|
+
import { TreeNode, TreeNodeElement } from "../types/TreeNode";
|
|
2
2
|
|
|
3
3
|
export interface AncestryItem {
|
|
4
4
|
elementName: string;
|
|
5
5
|
componentName?: string;
|
|
6
6
|
filePath?: string;
|
|
7
7
|
line?: number;
|
|
8
|
+
id?: string;
|
|
9
|
+
nthChild?: number; // 1-indexed, only set when there are ambiguous siblings
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
// Elements to exclude from ancestry (not useful for debugging)
|
|
11
13
|
const EXCLUDED_ELEMENTS = new Set(["html", "body", "head"]);
|
|
12
14
|
|
|
15
|
+
function isTreeNodeElement(node: TreeNode): node is TreeNodeElement {
|
|
16
|
+
return "getElement" in node && typeof node.getElement === "function";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Calculate nth-child position if there are multiple siblings of the same type.
|
|
21
|
+
* Returns undefined if the element is unique among its siblings, or 1-indexed position otherwise.
|
|
22
|
+
*/
|
|
23
|
+
function getNthChildIfAmbiguous(element: Element): number | undefined {
|
|
24
|
+
const parent = element.parentElement;
|
|
25
|
+
if (!parent) return undefined;
|
|
26
|
+
|
|
27
|
+
const tagName = element.tagName;
|
|
28
|
+
const siblings = Array.from(parent.children).filter(
|
|
29
|
+
(child) => child.tagName === tagName
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
// Only return position if there are multiple siblings of the same type
|
|
33
|
+
if (siblings.length <= 1) return undefined;
|
|
34
|
+
|
|
35
|
+
const index = siblings.indexOf(element);
|
|
36
|
+
return index + 1; // 1-indexed for CSS nth-child compatibility
|
|
37
|
+
}
|
|
38
|
+
|
|
13
39
|
export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
14
40
|
const items: AncestryItem[] = [];
|
|
15
41
|
let current: TreeNode | null = node;
|
|
@@ -28,6 +54,20 @@ export function collectAncestry(node: TreeNode): AncestryItem[] {
|
|
|
28
54
|
elementName: current.name,
|
|
29
55
|
};
|
|
30
56
|
|
|
57
|
+
// Extract ID and nth-child from the DOM element if available
|
|
58
|
+
if (isTreeNodeElement(current)) {
|
|
59
|
+
const element = current.getElement();
|
|
60
|
+
if (element instanceof Element) {
|
|
61
|
+
if (element.id) {
|
|
62
|
+
item.id = element.id;
|
|
63
|
+
}
|
|
64
|
+
const nthChild = getNthChildIfAmbiguous(element);
|
|
65
|
+
if (nthChild !== undefined) {
|
|
66
|
+
item.nthChild = nthChild;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
if (component) {
|
|
32
72
|
item.componentName = component.label;
|
|
33
73
|
if (component.callLink) {
|
|
@@ -66,9 +106,18 @@ export function formatAncestryChain(items: AncestryItem[]): string {
|
|
|
66
106
|
const indent = " ".repeat(index);
|
|
67
107
|
const prefix = index === 0 ? "" : "└─ ";
|
|
68
108
|
|
|
69
|
-
|
|
109
|
+
// Build element selector: elementName:nth-child(n)#id
|
|
110
|
+
let selector = item.elementName;
|
|
111
|
+
if (item.nthChild !== undefined) {
|
|
112
|
+
selector += `:nth-child(${item.nthChild})`;
|
|
113
|
+
}
|
|
114
|
+
if (item.id) {
|
|
115
|
+
selector += `#${item.id}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let description = selector;
|
|
70
119
|
if (item.componentName) {
|
|
71
|
-
description = `${
|
|
120
|
+
description = `${selector} in ${item.componentName}`;
|
|
72
121
|
}
|
|
73
122
|
|
|
74
123
|
const location = item.filePath ? ` at ${item.filePath}:${item.line}` : "";
|