| 1 | import type { PickedElementPayload } from "@t3tools/contracts"; |
| 2 | import { describe, expect, it } from "vite-plus/test"; |
| 3 | |
| 4 | import { normalizeElementContextSelection } from "./elementContext"; |
| 5 | |
| 6 | function 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 | |
| 34 | describe("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 | }); |