1import type { PickedElementPayload } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3
4import { normalizeElementContextSelection } from "./elementContext";
5
6function makePayload(overrides?: Partial<PickedElementPayload>): PickedElementPayload {
7 return {
8 pageUrl: "https://example.com/dashboard",
9 pageTitle: "Dashboard",
10 tagName: "BUTTON",
11 selector: "button.submit",
12 htmlPreview: '<button class="submit">Save</button>',
13 componentName: "SubmitButton",
14 source: {
15 functionName: "SubmitButton",
16 fileName: "/repo/src/Button.tsx",
17 lineNumber: 12,
18 columnNumber: 5,
19 },
20 stack: [
21 {
22 functionName: "SubmitButton",
23 fileName: "/repo/src/Button.tsx",
24 lineNumber: 12,
25 columnNumber: 5,
26 },
27 ],
28 styles: ".submit { color: white; }",
29 pickedAt: "2026-05-03T18:00:00.000Z",
30 ...overrides,
31 };
32}
33
34describe("normalizeElementContextSelection", () => {
35 it("lowercases the tag, trims strings, and prefers `source` over `stack[0]`", () => {
36 const result = normalizeElementContextSelection(
37 makePayload({
38 tagName: " Button ",
39 pageUrl: " https://example.com ",
40 pageTitle: " Dashboard ",
41 selector: " ",
42 componentName: " ",
43 source: {
44 functionName: " Outer ",
45 fileName: " /repo/Outer.tsx ",
46 lineNumber: 7,
47 columnNumber: 0,
48 },
49 stack: [
50 {
51 functionName: "Inner",
52 fileName: "/repo/Inner.tsx",
53 lineNumber: 99,
54 columnNumber: 9,
55 },
56 ],
57 }),
58 );
59 expect(result).not.toBeNull();
60 expect(result?.tagName).toBe("button");
61 expect(result?.pageUrl).toBe("https://example.com");
62 expect(result?.pageTitle).toBe("Dashboard");
63 expect(result?.selector).toBeNull();
64 expect(result?.componentName).toBeNull();
65 expect(result?.source).toEqual({
66 functionName: "Outer",
67 fileName: "/repo/Outer.tsx",
68 lineNumber: 7,
69 columnNumber: 0,
70 });
71 });
72
73 it("returns null when pageUrl or tagName is empty", () => {
74 expect(normalizeElementContextSelection(makePayload({ pageUrl: "" }))).toBeNull();
75 expect(normalizeElementContextSelection(makePayload({ tagName: " " }))).toBeNull();
76 });
77
78 it("clamps oversized htmlPreview / styles so we don't blow localStorage", () => {
79 const huge = "x".repeat(10_000);
80 const result = normalizeElementContextSelection(
81 makePayload({ htmlPreview: huge, styles: huge }),
82 );
83 expect(result).not.toBeNull();
84 expect(result!.htmlPreview.length).toBeLessThanOrEqual(4000);
85 expect(result!.styles.length).toBeLessThanOrEqual(4000);
86 // Truncated values should end with the ellipsis sentinel
87 expect(result!.htmlPreview.endsWith("…")).toBe(true);
88 expect(result!.styles.endsWith("…")).toBe(true);
89 });
90
91 it("normalizes Windows line endings inside html/styles", () => {
92 const result = normalizeElementContextSelection(
93 makePayload({ htmlPreview: "<a>\r\nhi\r\n</a>", styles: ".a {\r\n color: red;\r\n}" }),
94 );
95 expect(result?.htmlPreview).toBe("<a>\nhi\n</a>");
96 expect(result?.styles).toBe(".a {\n color: red;\n}");
97 });
98
99 it("falls back to stack[0] when payload.source is null", () => {
100 const result = normalizeElementContextSelection(
101 makePayload({
102 source: null,
103 stack: [
104 {
105 functionName: "FromStack",
106 fileName: "/repo/FromStack.tsx",
107 lineNumber: 3,
108 columnNumber: null,
109 },
110 ],
111 }),
112 );
113 expect(result?.source).toEqual({
114 functionName: "FromStack",
115 fileName: "/repo/FromStack.tsx",
116 lineNumber: 3,
117 columnNumber: null,
118 });
119 });
120});