import type { PickedElementPayload } from "@t3tools/contracts"; import { describe, expect, it } from "vite-plus/test"; import { appendElementContextsToPrompt, buildElementContextBlock, type ElementContextSelection, elementContextDedupKey, extractTrailingElementContexts, formatElementContextLabel, formatElementContextSourceLabel, newElementContextId, normalizeElementContextSelection, } from "./elementContext"; function makePayload(overrides?: Partial): PickedElementPayload { return { pageUrl: "https://example.com/dashboard", pageTitle: "Dashboard", tagName: "BUTTON", selector: "button.submit", htmlPreview: '', componentName: "SubmitButton", source: { functionName: "SubmitButton", fileName: "/repo/src/Button.tsx", lineNumber: 12, columnNumber: 5, }, stack: [ { functionName: "SubmitButton", fileName: "/repo/src/Button.tsx", lineNumber: 12, columnNumber: 5, }, ], styles: ".submit { color: white; }", pickedAt: "2026-05-03T18:00:00.000Z", ...overrides, }; } function makeSelection(overrides?: Partial): ElementContextSelection { return { pageUrl: "https://example.com/dashboard", pageTitle: "Dashboard", tagName: "button", selector: "button.submit", htmlPreview: '', componentName: "SubmitButton", source: { functionName: "SubmitButton", fileName: "/repo/src/Button.tsx", lineNumber: 12, columnNumber: 5, }, styles: ".submit { color: white; }", ...overrides, }; } describe("normalizeElementContextSelection", () => { it("lowercases the tag, trims strings, and prefers `source` over `stack[0]`", () => { const result = normalizeElementContextSelection( makePayload({ tagName: " Button ", pageUrl: " https://example.com ", pageTitle: " Dashboard ", selector: " ", componentName: " ", source: { functionName: " Outer ", fileName: " /repo/Outer.tsx ", lineNumber: 7, columnNumber: 0, }, stack: [ { functionName: "Inner", fileName: "/repo/Inner.tsx", lineNumber: 99, columnNumber: 9, }, ], }), ); expect(result).not.toBeNull(); expect(result?.tagName).toBe("button"); expect(result?.pageUrl).toBe("https://example.com"); expect(result?.pageTitle).toBe("Dashboard"); expect(result?.selector).toBeNull(); expect(result?.componentName).toBeNull(); expect(result?.source).toEqual({ functionName: "Outer", fileName: "/repo/Outer.tsx", lineNumber: 7, columnNumber: 0, }); }); it("returns null when pageUrl or tagName is empty", () => { expect(normalizeElementContextSelection(makePayload({ pageUrl: "" }))).toBeNull(); expect(normalizeElementContextSelection(makePayload({ tagName: " " }))).toBeNull(); }); it("clamps oversized htmlPreview / styles so we don't blow localStorage", () => { const huge = "x".repeat(10_000); const result = normalizeElementContextSelection( makePayload({ htmlPreview: huge, styles: huge }), ); expect(result).not.toBeNull(); expect(result!.htmlPreview.length).toBeLessThanOrEqual(4000); expect(result!.styles.length).toBeLessThanOrEqual(4000); // Truncated values should end with the ellipsis sentinel expect(result!.htmlPreview.endsWith("…")).toBe(true); expect(result!.styles.endsWith("…")).toBe(true); }); it("normalizes Windows line endings inside html/styles", () => { const result = normalizeElementContextSelection( makePayload({ htmlPreview: "\r\nhi\r\n", styles: ".a {\r\n color: red;\r\n}" }), ); expect(result?.htmlPreview).toBe("\nhi\n"); expect(result?.styles).toBe(".a {\n color: red;\n}"); }); it("falls back to stack[0] when payload.source is null", () => { const result = normalizeElementContextSelection( makePayload({ source: null, stack: [ { functionName: "FromStack", fileName: "/repo/FromStack.tsx", lineNumber: 3, columnNumber: null, }, ], }), ); expect(result?.source).toEqual({ functionName: "FromStack", fileName: "/repo/FromStack.tsx", lineNumber: 3, columnNumber: null, }); }); }); describe("formatElementContextLabel", () => { it("prefers component name over tag name", () => { expect(formatElementContextLabel(makeSelection())).toBe(""); }); it("falls back to tag name when no component name is present", () => { expect(formatElementContextLabel(makeSelection({ componentName: null }))).toBe("', " styles:", " .submit { color: white; }", "", ].join("\n"), ); }); it("emits no leading blank when prompt is empty", () => { expect( appendElementContextsToPrompt("", [makeSelection()]).startsWith(""), ).toBe(true); }); }); describe("extractTrailingElementContexts", () => { it("round-trips appendElementContextsToPrompt and recovers prompt + entries", () => { const prompt = appendElementContextsToPrompt("Investigate this", [ makeSelection(), makeSelection({ selector: "button.cancel", componentName: "CancelButton" }), ]); const result = extractTrailingElementContexts(prompt); expect(result.promptText).toBe("Investigate this"); expect(result.contextCount).toBe(2); expect(result.contexts.map((c) => c.header)).toEqual([ " (Button.tsx:12)", " (Button.tsx:12)", ]); expect(result.contexts[0]?.body).toContain("url: https://example.com/dashboard"); expect(result.contexts[0]?.body).toContain("selector: button.submit"); }); it("returns the original prompt unchanged when no trailing block exists", () => { expect(extractTrailingElementContexts("hi")).toEqual({ promptText: "hi", contextCount: 0, contexts: [], }); }); }); describe("newElementContextId", () => { it("returns a non-empty string with the element prefix", () => { const id = newElementContextId(); expect(id.startsWith("el_")).toBe(true); expect(id.length).toBeGreaterThan(3); }); it("returns unique ids on repeated calls", () => { const ids = new Set(Array.from({ length: 10 }, () => newElementContextId())); expect(ids.size).toBe(10); }); });