@treelocator/runtime 0.5.2 → 0.6.0
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/.eslintignore +1 -0
- package/dist/_generated_styles.d.ts +1 -1
- package/dist/_generated_styles.js +20 -0
- package/dist/_generated_tree_icon.d.ts +1 -1
- package/dist/adapters/HtmlElementTreeNode.d.ts +2 -2
- package/dist/adapters/HtmlElementTreeNode.js +4 -6
- package/dist/adapters/createTreeNode.js +17 -44
- package/dist/adapters/detectFramework.d.ts +8 -0
- package/dist/adapters/detectFramework.js +25 -0
- package/dist/adapters/detectFramework.test.d.ts +1 -0
- package/dist/adapters/detectFramework.test.js +60 -0
- package/dist/adapters/jsx/jsxAdapter.js +54 -89
- package/dist/adapters/jsx/jsxAdapter.test.d.ts +1 -0
- package/dist/adapters/jsx/jsxAdapter.test.js +273 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.js +1 -1
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.d.ts +1 -0
- package/dist/adapters/nextjs/parseNextjsDataAttributes.test.js +158 -0
- package/dist/adapters/react/findFiberByHtmlElement.d.ts +1 -1
- package/dist/adapters/react/findFiberByHtmlElement.js +1 -1
- package/dist/adapters/react/getAllParentsElementsAndRootComponent.js +4 -0
- package/dist/adapters/resolveAdapter.d.ts +1 -1
- package/dist/adapters/resolveAdapter.js +4 -8
- package/dist/adapters/svelte/svelteAdapter.test.d.ts +1 -0
- package/dist/adapters/svelte/svelteAdapter.test.js +280 -0
- package/dist/adapters/vue/vueAdapter.test.d.ts +1 -0
- package/dist/adapters/vue/vueAdapter.test.js +222 -0
- package/dist/browserApi.d.ts +148 -0
- package/dist/browserApi.js +146 -5
- package/dist/browserApi.test.d.ts +1 -0
- package/dist/browserApi.test.js +287 -0
- package/dist/components/RecordingPillButton.d.ts +11 -0
- package/dist/components/RecordingPillButton.js +202 -0
- package/dist/components/RecordingResults.d.ts +2 -0
- package/dist/components/RecordingResults.js +213 -78
- package/dist/components/Runtime.js +161 -554
- package/dist/components/SettingsPanel.d.ts +5 -0
- package/dist/components/SettingsPanel.js +312 -0
- package/dist/consoleCapture.d.ts +9 -0
- package/dist/consoleCapture.js +95 -0
- package/dist/functions/cssRuleInspector.d.ts +83 -0
- package/dist/functions/cssRuleInspector.js +608 -0
- package/dist/functions/cssRuleInspector.test.d.ts +1 -0
- package/dist/functions/cssRuleInspector.test.js +439 -0
- package/dist/functions/deduplicateLabels.test.d.ts +1 -0
- package/dist/functions/deduplicateLabels.test.js +178 -0
- package/dist/functions/enrichAncestrySourceMaps.js +0 -1
- package/dist/functions/extractComputedStyles.d.ts +51 -0
- package/dist/functions/extractComputedStyles.js +447 -0
- package/dist/functions/extractComputedStyles.test.d.ts +1 -0
- package/dist/functions/extractComputedStyles.test.js +549 -0
- package/dist/functions/formatAncestryChain.d.ts +8 -0
- package/dist/functions/formatAncestryChain.js +21 -1
- package/dist/functions/formatAncestryChain.test.js +18 -0
- package/dist/functions/getUsableName.test.d.ts +1 -0
- package/dist/functions/getUsableName.test.js +219 -0
- package/dist/functions/isCombinationModifiersPressed.test.d.ts +1 -0
- package/dist/functions/isCombinationModifiersPressed.test.js +192 -0
- package/dist/functions/mergeRects.test.js +210 -1
- package/dist/functions/namedSnapshots.d.ts +52 -0
- package/dist/functions/namedSnapshots.js +161 -0
- package/dist/functions/namedSnapshots.test.d.ts +1 -0
- package/dist/functions/namedSnapshots.test.js +85 -0
- package/dist/functions/normalizeFilePath.test.d.ts +1 -0
- package/dist/functions/normalizeFilePath.test.js +66 -0
- package/dist/functions/parseDataId.test.d.ts +1 -0
- package/dist/functions/parseDataId.test.js +101 -0
- package/dist/hooks/getStorage.d.ts +3 -0
- package/dist/hooks/getStorage.js +17 -0
- package/dist/hooks/useEventListeners.d.ts +15 -0
- package/dist/hooks/useEventListeners.js +56 -0
- package/dist/hooks/useLocatorStorage.d.ts +18 -0
- package/dist/hooks/useLocatorStorage.js +41 -0
- package/dist/hooks/useLocatorStorage.test.d.ts +1 -0
- package/dist/hooks/useLocatorStorage.test.js +124 -0
- package/dist/hooks/useRecordingState.d.ts +43 -0
- package/dist/hooks/useRecordingState.js +387 -0
- package/dist/hooks/useSettings.d.ts +13 -0
- package/dist/hooks/useSettings.js +66 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/initRuntime.d.ts +3 -1
- package/dist/initRuntime.js +4 -1
- package/dist/mcpBridge.d.ts +61 -0
- package/dist/mcpBridge.js +534 -0
- package/dist/mcpBridge.test.d.ts +1 -0
- package/dist/mcpBridge.test.js +248 -0
- package/dist/output.css +20 -0
- package/dist/visualDiff/diff.d.ts +9 -0
- package/dist/visualDiff/diff.js +209 -0
- package/dist/visualDiff/diff.test.d.ts +1 -0
- package/dist/visualDiff/diff.test.js +253 -0
- package/dist/visualDiff/settle.d.ts +3 -0
- package/dist/visualDiff/settle.js +50 -0
- package/dist/visualDiff/settle.test.d.ts +1 -0
- package/dist/visualDiff/settle.test.js +65 -0
- package/dist/visualDiff/snapshot.d.ts +4 -0
- package/dist/visualDiff/snapshot.js +84 -0
- package/dist/visualDiff/snapshot.test.d.ts +1 -0
- package/dist/visualDiff/snapshot.test.js +245 -0
- package/dist/visualDiff/types.d.ts +37 -0
- package/dist/visualDiff/types.js +1 -0
- package/package.json +2 -2
- package/scripts/wrapCSS.js +1 -1
- package/scripts/wrapImage.js +1 -1
- package/src/_generated_styles.ts +21 -1
- package/src/_generated_tree_icon.ts +1 -1
- package/src/adapters/HtmlElementTreeNode.ts +10 -7
- package/src/adapters/createTreeNode.ts +12 -51
- package/src/adapters/detectFramework.test.ts +73 -0
- package/src/adapters/detectFramework.ts +28 -0
- package/src/adapters/jsx/jsxAdapter.test.ts +240 -0
- package/src/adapters/jsx/jsxAdapter.ts +53 -106
- package/src/adapters/nextjs/parseNextjsDataAttributes.test.ts +212 -0
- package/src/adapters/nextjs/parseNextjsDataAttributes.ts +1 -1
- package/src/adapters/react/findDebugSource.ts +5 -6
- package/src/adapters/react/findFiberByHtmlElement.ts +3 -3
- package/src/adapters/react/getAllParentsElementsAndRootComponent.ts +3 -0
- package/src/adapters/react/reactAdapter.ts +1 -2
- package/src/adapters/resolveAdapter.ts +4 -14
- package/src/adapters/svelte/svelteAdapter.test.ts +334 -0
- package/src/adapters/vue/vueAdapter.test.ts +259 -0
- package/src/browserApi.test.ts +329 -0
- package/src/browserApi.ts +351 -4
- package/src/components/RecordingPillButton.tsx +301 -0
- package/src/components/RecordingResults.tsx +114 -13
- package/src/components/Runtime.tsx +176 -621
- package/src/components/SettingsPanel.tsx +339 -0
- package/src/consoleCapture.ts +113 -0
- package/src/functions/cssRuleInspector.test.ts +517 -0
- package/src/functions/cssRuleInspector.ts +708 -0
- package/src/functions/deduplicateLabels.test.ts +115 -0
- package/src/functions/enrichAncestrySourceMaps.ts +6 -3
- package/src/functions/extractComputedStyles.test.ts +681 -0
- package/src/functions/extractComputedStyles.ts +768 -0
- package/src/functions/formatAncestryChain.test.ts +23 -1
- package/src/functions/formatAncestryChain.ts +22 -1
- package/src/functions/getUsableName.test.ts +242 -0
- package/src/functions/isCombinationModifiersPressed.test.ts +156 -0
- package/src/functions/mergeRects.test.ts +111 -1
- package/src/functions/namedSnapshots.test.ts +106 -0
- package/src/functions/namedSnapshots.ts +232 -0
- package/src/functions/normalizeFilePath.test.ts +80 -0
- package/src/functions/parseDataId.test.ts +125 -0
- package/src/hooks/getStorage.ts +26 -0
- package/src/hooks/useEventListeners.ts +97 -0
- package/src/hooks/useLocatorStorage.test.ts +127 -0
- package/src/hooks/useLocatorStorage.ts +60 -0
- package/src/hooks/useRecordingState.ts +516 -0
- package/src/hooks/useSettings.ts +83 -0
- package/src/index.ts +10 -5
- package/src/initRuntime.ts +5 -0
- package/src/mcpBridge.test.ts +260 -0
- package/src/mcpBridge.ts +677 -0
- package/src/visualDiff/diff.test.ts +167 -0
- package/src/visualDiff/diff.ts +242 -0
- package/src/visualDiff/settle.test.ts +77 -0
- package/src/visualDiff/settle.ts +62 -0
- package/src/visualDiff/snapshot.test.ts +200 -0
- package/src/visualDiff/snapshot.ts +119 -0
- package/src/visualDiff/types.ts +40 -0
- package/tsconfig.json +3 -1
- package/vitest.config.ts +18 -0
- package/jest.config.ts +0 -195
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, test, vi, beforeEach } from "vitest";
|
|
2
|
+
import { detectFramework } from "./detectFramework";
|
|
3
|
+
|
|
4
|
+
vi.mock("@locator/shared", () => ({
|
|
5
|
+
detectSvelte: vi.fn(() => false),
|
|
6
|
+
detectVue: vi.fn(() => false),
|
|
7
|
+
detectReact: vi.fn(() => false),
|
|
8
|
+
detectJSX: vi.fn(() => false),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("./phoenix/detectPhoenix", () => ({
|
|
12
|
+
detectPhoenix: vi.fn(() => false),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { detectSvelte, detectVue, detectReact, detectJSX } from "@locator/shared";
|
|
16
|
+
import { detectPhoenix } from "./phoenix/detectPhoenix";
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
vi.mocked(detectSvelte).mockReturnValue(false);
|
|
20
|
+
vi.mocked(detectVue).mockReturnValue(false);
|
|
21
|
+
vi.mocked(detectReact).mockReturnValue(false);
|
|
22
|
+
vi.mocked(detectJSX).mockReturnValue(false);
|
|
23
|
+
vi.mocked(detectPhoenix).mockReturnValue(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("detectFramework", () => {
|
|
27
|
+
test("returns null when all detect functions return false and no element", () => {
|
|
28
|
+
expect(detectFramework()).toBe(null);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("returns 'svelte' when detectSvelte returns true", () => {
|
|
32
|
+
vi.mocked(detectSvelte).mockReturnValue(true);
|
|
33
|
+
expect(detectFramework()).toBe("svelte");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("returns 'vue' when detectVue returns true", () => {
|
|
37
|
+
vi.mocked(detectVue).mockReturnValue(true);
|
|
38
|
+
expect(detectFramework()).toBe("vue");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("returns 'react' when detectReact returns true", () => {
|
|
42
|
+
vi.mocked(detectReact).mockReturnValue(true);
|
|
43
|
+
expect(detectFramework()).toBe("react");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns 'jsx' when detectJSX returns true", () => {
|
|
47
|
+
vi.mocked(detectJSX).mockReturnValue(true);
|
|
48
|
+
expect(detectFramework()).toBe("jsx");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("returns 'jsx' when detectPhoenix returns true", () => {
|
|
52
|
+
vi.mocked(detectPhoenix).mockReturnValue(true);
|
|
53
|
+
expect(detectFramework()).toBe("jsx");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("returns 'react' when element has __reactFiber$ key even when detectReact returns false", () => {
|
|
57
|
+
const element = document.createElement("div");
|
|
58
|
+
(element as any)["__reactFiber$abc123"] = {};
|
|
59
|
+
expect(detectFramework(element)).toBe("react");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("returns 'jsx' when element has dataset.locatorjsId", () => {
|
|
63
|
+
const element = document.createElement("div");
|
|
64
|
+
element.dataset.locatorjsId = "some-id";
|
|
65
|
+
expect(detectFramework(element)).toBe("jsx");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("returns 'svelte' over 'react' when both detectSvelte and detectReact return true", () => {
|
|
69
|
+
vi.mocked(detectSvelte).mockReturnValue(true);
|
|
70
|
+
vi.mocked(detectReact).mockReturnValue(true);
|
|
71
|
+
expect(detectFramework()).toBe("svelte");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { detectJSX, detectReact, detectSvelte, detectVue } from "@locator/shared";
|
|
2
|
+
import { detectPhoenix } from "./phoenix/detectPhoenix";
|
|
3
|
+
|
|
4
|
+
export type FrameworkId = "svelte" | "vue" | "react" | "jsx" | null;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if a DOM element has __reactFiber$ keys.
|
|
8
|
+
* Works without React DevTools extension.
|
|
9
|
+
*/
|
|
10
|
+
function hasReactFiberKeys(element?: HTMLElement): boolean {
|
|
11
|
+
if (!element) return false;
|
|
12
|
+
return Object.keys(element).some((k) => k.startsWith("__reactFiber$"));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Detect the active framework, optionally considering element-level hints.
|
|
17
|
+
*
|
|
18
|
+
* Priority order: Svelte > Vue > React > JSX > Phoenix (uses JSX adapter)
|
|
19
|
+
* JSX must be last because global data can leak from the LocatorJS extension.
|
|
20
|
+
*/
|
|
21
|
+
export function detectFramework(element?: HTMLElement): FrameworkId {
|
|
22
|
+
if (detectSvelte()) return "svelte";
|
|
23
|
+
if (detectVue()) return "vue";
|
|
24
|
+
if (detectReact() || hasReactFiberKeys(element)) return "react";
|
|
25
|
+
if (detectJSX() || (element && element.dataset.locatorjsId)) return "jsx";
|
|
26
|
+
if (detectPhoenix()) return "jsx";
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { getElementInfo, JSXTreeNodeElement } from "./jsxAdapter";
|
|
3
|
+
|
|
4
|
+
function makeElement(attrs: Record<string, string> = {}): HTMLElement {
|
|
5
|
+
const el = document.createElement("div");
|
|
6
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
7
|
+
el.setAttribute(k, v);
|
|
8
|
+
}
|
|
9
|
+
return el;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
delete (window as any).__LOCATOR_DATA__;
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// getElementInfo
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
describe("getElementInfo", () => {
|
|
21
|
+
test("returns null when element has no data-locatorjs attributes", () => {
|
|
22
|
+
const el = makeElement({ class: "foo" });
|
|
23
|
+
expect(getElementInfo(el)).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns FullElementInfo from data-locatorjs path attribute (no __LOCATOR_DATA__)", () => {
|
|
27
|
+
// Format: /project/src/Button.tsx:10:5
|
|
28
|
+
const el = makeElement({ "data-locatorjs": "/project/src/Button.tsx:10:5" });
|
|
29
|
+
document.body.appendChild(el);
|
|
30
|
+
|
|
31
|
+
const info = getElementInfo(el);
|
|
32
|
+
expect(info).not.toBeNull();
|
|
33
|
+
|
|
34
|
+
// splitFullPath splits on /src/ → projectPath="/project", filePath="/src/Button.tsx"
|
|
35
|
+
expect(info!.thisElement.link).toMatchObject({
|
|
36
|
+
filePath: "/src/Button.tsx",
|
|
37
|
+
projectPath: "/project",
|
|
38
|
+
line: 10,
|
|
39
|
+
column: 6, // column + 1
|
|
40
|
+
});
|
|
41
|
+
expect(info!.htmlElement).toBe(el);
|
|
42
|
+
|
|
43
|
+
document.body.removeChild(el);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns FullElementInfo using fileData from __LOCATOR_DATA__ when data-locatorjs-id is set", () => {
|
|
47
|
+
const fileFullPath = "/project/src/Button.tsx";
|
|
48
|
+
const fileData = {
|
|
49
|
+
filePath: "/src/Button.tsx",
|
|
50
|
+
projectPath: "/project",
|
|
51
|
+
expressions: [
|
|
52
|
+
{
|
|
53
|
+
name: "Button",
|
|
54
|
+
loc: { start: { line: 5, column: 2 }, end: { line: 5, column: 10 } },
|
|
55
|
+
wrappingComponentId: null,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
components: [],
|
|
59
|
+
styledDefinitions: [],
|
|
60
|
+
};
|
|
61
|
+
(window as any).__LOCATOR_DATA__ = { [fileFullPath]: fileData };
|
|
62
|
+
|
|
63
|
+
const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
|
|
64
|
+
document.body.appendChild(el);
|
|
65
|
+
|
|
66
|
+
const info = getElementInfo(el);
|
|
67
|
+
expect(info).not.toBeNull();
|
|
68
|
+
expect(info!.thisElement.link).toMatchObject({
|
|
69
|
+
filePath: "/src/Button.tsx",
|
|
70
|
+
projectPath: "/project",
|
|
71
|
+
line: 5,
|
|
72
|
+
column: 3,
|
|
73
|
+
});
|
|
74
|
+
expect(info!.thisElement.label).toBe("Button");
|
|
75
|
+
|
|
76
|
+
document.body.removeChild(el);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("finds attributes on nearest ancestor via closest()", () => {
|
|
80
|
+
const parent = makeElement({
|
|
81
|
+
"data-locatorjs": "/project/src/Layout.tsx:20:0",
|
|
82
|
+
});
|
|
83
|
+
const child = makeElement();
|
|
84
|
+
parent.appendChild(child);
|
|
85
|
+
document.body.appendChild(parent);
|
|
86
|
+
|
|
87
|
+
const info = getElementInfo(child);
|
|
88
|
+
expect(info).not.toBeNull();
|
|
89
|
+
expect(info!.thisElement.link).toMatchObject({
|
|
90
|
+
line: 20,
|
|
91
|
+
column: 1,
|
|
92
|
+
});
|
|
93
|
+
expect(info!.htmlElement).toBe(parent);
|
|
94
|
+
|
|
95
|
+
document.body.removeChild(parent);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("returns null when data-locatorjs attribute has malformed path (only one colon)", () => {
|
|
99
|
+
// parseDataPath returns null for a path with only one colon
|
|
100
|
+
const el = makeElement({ "data-locatorjs": "/project/src/Button.tsx:10" });
|
|
101
|
+
document.body.appendChild(el);
|
|
102
|
+
const info = getElementInfo(el);
|
|
103
|
+
expect(info).toBeNull();
|
|
104
|
+
document.body.removeChild(el);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
// JSXTreeNodeElement.getSource
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
describe("JSXTreeNodeElement.getSource", () => {
|
|
113
|
+
test("returns null when element has no data-locatorjs attributes", () => {
|
|
114
|
+
const el = makeElement();
|
|
115
|
+
const node = new JSXTreeNodeElement(el);
|
|
116
|
+
expect(node.getSource()).toBeNull();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("returns Source from data-locatorjs attribute without __LOCATOR_DATA__", () => {
|
|
120
|
+
const el = makeElement({ "data-locatorjs": "/project/src/App.tsx:15:3" });
|
|
121
|
+
const node = new JSXTreeNodeElement(el);
|
|
122
|
+
|
|
123
|
+
const source = node.getSource();
|
|
124
|
+
expect(source).not.toBeNull();
|
|
125
|
+
expect(source).toMatchObject({
|
|
126
|
+
fileName: "/src/App.tsx",
|
|
127
|
+
projectPath: "/project",
|
|
128
|
+
lineNumber: 15,
|
|
129
|
+
columnNumber: 4, // column + 1
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("returns Source using fileData from __LOCATOR_DATA__ when data-locatorjs-id is set", () => {
|
|
134
|
+
const fileFullPath = "/project/src/App.tsx";
|
|
135
|
+
const fileData = {
|
|
136
|
+
filePath: "/src/App.tsx",
|
|
137
|
+
projectPath: "/project",
|
|
138
|
+
expressions: [
|
|
139
|
+
{
|
|
140
|
+
name: "App",
|
|
141
|
+
loc: { start: { line: 8, column: 4 }, end: { line: 8, column: 12 } },
|
|
142
|
+
wrappingComponentId: null,
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
components: [],
|
|
146
|
+
styledDefinitions: [],
|
|
147
|
+
};
|
|
148
|
+
(window as any).__LOCATOR_DATA__ = { [fileFullPath]: fileData };
|
|
149
|
+
|
|
150
|
+
const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
|
|
151
|
+
const node = new JSXTreeNodeElement(el);
|
|
152
|
+
|
|
153
|
+
const source = node.getSource();
|
|
154
|
+
expect(source).not.toBeNull();
|
|
155
|
+
expect(source).toMatchObject({
|
|
156
|
+
fileName: "/src/App.tsx",
|
|
157
|
+
projectPath: "/project",
|
|
158
|
+
lineNumber: 8,
|
|
159
|
+
columnNumber: 5,
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// JSXTreeNodeElement.getComponent
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
describe("JSXTreeNodeElement.getComponent", () => {
|
|
169
|
+
test("returns null when element has no data-locatorjs attributes", () => {
|
|
170
|
+
const el = makeElement();
|
|
171
|
+
const node = new JSXTreeNodeElement(el);
|
|
172
|
+
expect(node.getComponent()).toBeNull();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("returns null when __LOCATOR_DATA__ is not set (no fileData)", () => {
|
|
176
|
+
const el = makeElement({ "data-locatorjs": "/project/src/Card.tsx:5:0" });
|
|
177
|
+
const node = new JSXTreeNodeElement(el);
|
|
178
|
+
expect(node.getComponent()).toBeNull();
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("returns null when expression has no wrappingComponentId", () => {
|
|
182
|
+
const fileFullPath = "/project/src/Card.tsx";
|
|
183
|
+
(window as any).__LOCATOR_DATA__ = {
|
|
184
|
+
[fileFullPath]: {
|
|
185
|
+
filePath: "/src/Card.tsx",
|
|
186
|
+
projectPath: "/project",
|
|
187
|
+
expressions: [
|
|
188
|
+
{
|
|
189
|
+
name: "div",
|
|
190
|
+
loc: { start: { line: 3, column: 0 }, end: { line: 3, column: 5 } },
|
|
191
|
+
wrappingComponentId: null,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
components: [],
|
|
195
|
+
styledDefinitions: [],
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
|
|
200
|
+
const node = new JSXTreeNodeElement(el);
|
|
201
|
+
expect(node.getComponent()).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("returns TreeNodeComponent when fileData and wrappingComponentId are present", () => {
|
|
205
|
+
const fileFullPath = "/project/src/Form.tsx";
|
|
206
|
+
(window as any).__LOCATOR_DATA__ = {
|
|
207
|
+
[fileFullPath]: {
|
|
208
|
+
filePath: "/src/Form.tsx",
|
|
209
|
+
projectPath: "/project",
|
|
210
|
+
expressions: [
|
|
211
|
+
{
|
|
212
|
+
name: "input",
|
|
213
|
+
loc: { start: { line: 12, column: 4 }, end: { line: 12, column: 12 } },
|
|
214
|
+
wrappingComponentId: 0,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
components: [
|
|
218
|
+
{
|
|
219
|
+
name: "Form",
|
|
220
|
+
loc: { start: { line: 1, column: 0 }, end: { line: 20, column: 1 } },
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
styledDefinitions: [],
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const el = makeElement({ "data-locatorjs-id": `${fileFullPath}::0` });
|
|
228
|
+
const node = new JSXTreeNodeElement(el);
|
|
229
|
+
|
|
230
|
+
const component = node.getComponent();
|
|
231
|
+
expect(component).not.toBeNull();
|
|
232
|
+
expect(component!.label).toBe("Form");
|
|
233
|
+
expect(component!.definitionLink).toMatchObject({
|
|
234
|
+
fileName: "/src/Form.tsx",
|
|
235
|
+
projectPath: "/project",
|
|
236
|
+
lineNumber: 1,
|
|
237
|
+
columnNumber: 1,
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -17,58 +17,68 @@ import { HtmlElementTreeNode } from "../HtmlElementTreeNode";
|
|
|
17
17
|
import { getExpressionData } from "./getExpressionData";
|
|
18
18
|
import { getJSXComponentBoundingBox } from "./getJSXComponentBoundingBox";
|
|
19
19
|
|
|
20
|
+
type JSXLocatorData = {
|
|
21
|
+
fileFullPath: string;
|
|
22
|
+
fileData: FileStorage | undefined;
|
|
23
|
+
filePath: string;
|
|
24
|
+
projectPath: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function resolveJSXLocatorData(element: Element): JSXLocatorData | null {
|
|
28
|
+
const dataId = element.getAttribute("data-locatorjs-id");
|
|
29
|
+
const dataPath = element.getAttribute("data-locatorjs");
|
|
30
|
+
|
|
31
|
+
if (!dataId && !dataPath) return null;
|
|
32
|
+
|
|
33
|
+
let fileFullPath: string;
|
|
34
|
+
|
|
35
|
+
if (dataPath) {
|
|
36
|
+
const parsed = parseDataPath(dataPath);
|
|
37
|
+
if (!parsed) return null;
|
|
38
|
+
[fileFullPath] = parsed;
|
|
39
|
+
} else if (dataId) {
|
|
40
|
+
[fileFullPath] = parseDataId(dataId);
|
|
41
|
+
} else {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const locatorData = window.__LOCATOR_DATA__;
|
|
46
|
+
const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
|
|
47
|
+
|
|
48
|
+
let filePath: string;
|
|
49
|
+
let projectPath: string;
|
|
50
|
+
|
|
51
|
+
if (fileData) {
|
|
52
|
+
filePath = fileData.filePath;
|
|
53
|
+
projectPath = fileData.projectPath;
|
|
54
|
+
} else {
|
|
55
|
+
[projectPath, filePath] = splitFullPath(fileFullPath);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { fileFullPath, fileData, filePath, projectPath };
|
|
59
|
+
}
|
|
60
|
+
|
|
20
61
|
export function getElementInfo(target: HTMLElement): FullElementInfo | null {
|
|
21
62
|
const found = target.closest("[data-locatorjs-id], [data-locatorjs]");
|
|
22
63
|
|
|
23
64
|
// Support both HTMLElement and SVGElement
|
|
24
65
|
// SVG elements don't have dataset, so use getAttribute instead
|
|
25
|
-
const dataId = found?.getAttribute("data-locatorjs-id");
|
|
26
|
-
const dataPath = found?.getAttribute("data-locatorjs");
|
|
27
66
|
const styledDataId = found?.getAttribute("data-locatorjs-styled");
|
|
28
67
|
|
|
29
68
|
if (
|
|
30
69
|
found &&
|
|
31
|
-
(found instanceof HTMLElement || found instanceof SVGElement)
|
|
32
|
-
(dataId || dataPath || styledDataId)
|
|
70
|
+
(found instanceof HTMLElement || found instanceof SVGElement)
|
|
33
71
|
) {
|
|
34
|
-
|
|
35
|
-
if (!
|
|
72
|
+
const resolved = resolveJSXLocatorData(found);
|
|
73
|
+
if (!resolved && !styledDataId) {
|
|
36
74
|
return null;
|
|
37
75
|
}
|
|
38
|
-
|
|
39
|
-
let fileFullPath: string;
|
|
40
|
-
|
|
41
|
-
if (dataPath) {
|
|
42
|
-
const parsed = parseDataPath(dataPath);
|
|
43
|
-
if (!parsed) {
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
[fileFullPath] = parsed;
|
|
47
|
-
} else if (dataId) {
|
|
48
|
-
[fileFullPath] = parseDataId(dataId);
|
|
49
|
-
} else {
|
|
76
|
+
if (!resolved) {
|
|
50
77
|
return null;
|
|
51
78
|
}
|
|
52
79
|
|
|
80
|
+
const { fileFullPath, fileData, filePath, projectPath } = resolved;
|
|
53
81
|
const locatorData = window.__LOCATOR_DATA__;
|
|
54
|
-
const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
|
|
55
|
-
|
|
56
|
-
// Handle styled components (only when locatorData is available)
|
|
57
|
-
const [styledFileFullPath, styledId] = styledDataId
|
|
58
|
-
? parseDataId(styledDataId)
|
|
59
|
-
: [null, null];
|
|
60
|
-
const styledFileData: FileStorage | undefined =
|
|
61
|
-
styledFileFullPath && locatorData?.[styledFileFullPath];
|
|
62
|
-
const styledExpData =
|
|
63
|
-
styledFileData && styledFileData.styledDefinitions[Number(styledId)];
|
|
64
|
-
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
66
|
-
const styledLink = styledExpData && {
|
|
67
|
-
filePath: styledFileData.filePath,
|
|
68
|
-
projectPath: styledFileData.projectPath,
|
|
69
|
-
column: (styledExpData.loc?.start.column || 0) + 1,
|
|
70
|
-
line: styledExpData.loc?.start.line || 0,
|
|
71
|
-
};
|
|
72
82
|
|
|
73
83
|
// Get expression data (works with or without locatorData)
|
|
74
84
|
const expData = getExpressionData(found, fileData || null);
|
|
@@ -76,18 +86,6 @@ export function getElementInfo(target: HTMLElement): FullElementInfo | null {
|
|
|
76
86
|
return null;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
|
-
// Extract file path components
|
|
80
|
-
let filePath: string;
|
|
81
|
-
let projectPath: string;
|
|
82
|
-
|
|
83
|
-
if (fileData) {
|
|
84
|
-
filePath = fileData.filePath;
|
|
85
|
-
projectPath = fileData.projectPath;
|
|
86
|
-
} else {
|
|
87
|
-
// If no fileData, split the full path
|
|
88
|
-
[projectPath, filePath] = splitFullPath(fileFullPath);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
89
|
const wrappingComponent =
|
|
92
90
|
expData.wrappingComponentId !== null && fileData
|
|
93
91
|
? fileData.components[Number(expData.wrappingComponentId)]
|
|
@@ -133,47 +131,16 @@ export function getElementInfo(target: HTMLElement): FullElementInfo | null {
|
|
|
133
131
|
|
|
134
132
|
export class JSXTreeNodeElement extends HtmlElementTreeNode {
|
|
135
133
|
getSource(): Source | null {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const dataPath = this.element.getAttribute("data-locatorjs");
|
|
139
|
-
|
|
140
|
-
if (!dataId && !dataPath) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
134
|
+
const resolved = resolveJSXLocatorData(this.element);
|
|
135
|
+
if (!resolved) return null;
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if (dataPath) {
|
|
147
|
-
const parsed = parseDataPath(dataPath);
|
|
148
|
-
if (!parsed) {
|
|
149
|
-
return null;
|
|
150
|
-
}
|
|
151
|
-
[fileFullPath] = parsed;
|
|
152
|
-
} else if (dataId) {
|
|
153
|
-
[fileFullPath] = parseDataId(dataId);
|
|
154
|
-
} else {
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const locatorData = window.__LOCATOR_DATA__;
|
|
159
|
-
const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
|
|
137
|
+
const { fileData, filePath, projectPath } = resolved;
|
|
160
138
|
|
|
161
139
|
// Get expression data (works with or without locatorData)
|
|
162
140
|
const expData = getExpressionData(this.element, fileData || null);
|
|
163
141
|
if (expData) {
|
|
164
|
-
let fileName: string;
|
|
165
|
-
let projectPath: string;
|
|
166
|
-
|
|
167
|
-
if (fileData) {
|
|
168
|
-
fileName = fileData.filePath;
|
|
169
|
-
projectPath = fileData.projectPath;
|
|
170
|
-
} else {
|
|
171
|
-
// If no fileData, split the full path
|
|
172
|
-
[projectPath, fileName] = splitFullPath(fileFullPath);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
142
|
return {
|
|
176
|
-
fileName,
|
|
143
|
+
fileName: filePath,
|
|
177
144
|
projectPath,
|
|
178
145
|
columnNumber: (expData.loc.start.column || 0) + 1,
|
|
179
146
|
lineNumber: expData.loc.start.line || 0,
|
|
@@ -183,30 +150,10 @@ export class JSXTreeNodeElement extends HtmlElementTreeNode {
|
|
|
183
150
|
return null;
|
|
184
151
|
}
|
|
185
152
|
getComponent(): TreeNodeComponent | null {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const dataPath = this.element.getAttribute("data-locatorjs");
|
|
189
|
-
|
|
190
|
-
if (!dataId && !dataPath) {
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
let fileFullPath: string;
|
|
153
|
+
const resolved = resolveJSXLocatorData(this.element);
|
|
154
|
+
if (!resolved) return null;
|
|
195
155
|
|
|
196
|
-
|
|
197
|
-
const parsed = parseDataPath(dataPath);
|
|
198
|
-
if (!parsed) {
|
|
199
|
-
return null;
|
|
200
|
-
}
|
|
201
|
-
[fileFullPath] = parsed;
|
|
202
|
-
} else if (dataId) {
|
|
203
|
-
[fileFullPath] = parseDataId(dataId);
|
|
204
|
-
} else {
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
const locatorData = window.__LOCATOR_DATA__;
|
|
209
|
-
const fileData: FileStorage | undefined = locatorData?.[fileFullPath];
|
|
156
|
+
const { fileData } = resolved;
|
|
210
157
|
|
|
211
158
|
// Component information is only available when we have fileData
|
|
212
159
|
if (fileData) {
|