@treelocator/runtime 0.1.2 → 0.1.4
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 +30 -0
- package/.turbo/turbo-test.log +18 -0
- package/.turbo/turbo-ts.log +4 -0
- package/LICENSE +22 -0
- 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 +3 -2
- package/src/functions/formatAncestryChain.test.ts +94 -0
- package/src/functions/formatAncestryChain.ts +52 -3
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
|
|
2
|
+
> @treelocator/runtime@0.1.4 build /Users/wende/projects/locatorjs/packages/runtime
|
|
3
|
+
> concurrently pnpm:build:*
|
|
4
|
+
|
|
5
|
+
[wrapImage]
|
|
6
|
+
[wrapImage] > @treelocator/runtime@0.1.4 build:wrapImage /Users/wende/projects/locatorjs/packages/runtime
|
|
7
|
+
[wrapImage] > node ./scripts/wrapImage.js
|
|
8
|
+
[wrapImage]
|
|
9
|
+
[tailwind]
|
|
10
|
+
[tailwind] > @treelocator/runtime@0.1.4 build:tailwind /Users/wende/projects/locatorjs/packages/runtime
|
|
11
|
+
[tailwind] > tailwindcss -i ./src/main.css -o ./dist/output.css
|
|
12
|
+
[tailwind]
|
|
13
|
+
[ts]
|
|
14
|
+
[ts] > @treelocator/runtime@0.1.4 build:ts /Users/wende/projects/locatorjs/packages/runtime
|
|
15
|
+
[ts] > tsc --declaration --emitDeclarationOnly --noEmit false --outDir dist
|
|
16
|
+
[ts]
|
|
17
|
+
[babel]
|
|
18
|
+
[babel] > @treelocator/runtime@0.1.4 build:babel /Users/wende/projects/locatorjs/packages/runtime
|
|
19
|
+
[babel] > babel src --out-dir dist --extensions .js,.jsx,.ts,.tsx
|
|
20
|
+
[babel]
|
|
21
|
+
[wrapImage] Tree icon file generated
|
|
22
|
+
[wrapImage] pnpm run build:wrapImage exited with code 0
|
|
23
|
+
[tailwind]
|
|
24
|
+
[tailwind] Rebuilding...
|
|
25
|
+
[tailwind]
|
|
26
|
+
[tailwind] Done in 160ms.
|
|
27
|
+
[tailwind] pnpm run build:tailwind exited with code 0
|
|
28
|
+
[babel] Successfully compiled 72 files with Babel (416ms).
|
|
29
|
+
[babel] pnpm run build:babel exited with code 0
|
|
30
|
+
[ts] pnpm run build:ts exited with code 0
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
> @treelocator/runtime@0.1.2 test /Users/wende/projects/locatorjs/packages/runtime
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
RUN v2.1.9 /Users/wende/projects/locatorjs/packages/runtime
|
|
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
|
|
13
|
+
|
|
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)
|
|
18
|
+
|
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Krzysztof Wende
|
|
4
|
+
Copyright (c) 2023 Michael Musil (original LocatorJS)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
|
@@ -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.4",
|
|
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",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"e2e"
|
|
22
22
|
],
|
|
23
23
|
"main": "dist/index.js",
|
|
24
|
+
"sideEffects": false,
|
|
24
25
|
"publishConfig": {
|
|
25
26
|
"access": "public"
|
|
26
27
|
},
|
|
@@ -71,5 +72,5 @@
|
|
|
71
72
|
"directory": "packages/runtime"
|
|
72
73
|
},
|
|
73
74
|
"license": "MIT",
|
|
74
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "a36be153e103505d6814e0d8b5c3a5cb17c1df0c"
|
|
75
76
|
}
|
|
@@ -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}` : "";
|