1import { describe, expect, it } from "vite-plus/test";
2
3import { isPickedElementPayload, isPreviewAnnotationPayload } from "./PickedElementPayload.ts";
4
5function validPayload(overrides?: Record<string, unknown>): Record<string, unknown> {
6 return {
7 pageUrl: "https://example.com/",
8 pageTitle: "Example",
9 tagName: "button",
10 selector: "button.submit",
11 htmlPreview: "<button>Save</button>",
12 componentName: "SubmitButton",
13 source: {
14 functionName: "SubmitButton",
15 fileName: "/repo/src/Button.tsx",
16 lineNumber: 12,
17 columnNumber: 5,
18 },
19 stack: [
20 {
21 functionName: "SubmitButton",
22 fileName: "/repo/src/Button.tsx",
23 lineNumber: 12,
24 columnNumber: 5,
25 },
26 ],
27 styles: ".submit { color: white; }",
28 pickedAt: "2026-05-03T18:00:00.000Z",
29 ...overrides,
30 };
31}
32
33describe("isPickedElementPayload", () => {
34 it("accepts a complete, well-typed payload", () => {
35 expect(isPickedElementPayload(validPayload())).toBe(true);
36 });
37
38 it("accepts nullable string fields when null", () => {
39 expect(
40 isPickedElementPayload(
41 validPayload({ pageTitle: null, selector: null, componentName: null, source: null }),
42 ),
43 ).toBe(true);
44 });
45
46 it("accepts an empty stack array", () => {
47 expect(isPickedElementPayload(validPayload({ stack: [] }))).toBe(true);
48 });
49
50 it("accepts stack frames with null fields", () => {
51 expect(
52 isPickedElementPayload(
53 validPayload({
54 stack: [
55 {
56 functionName: null,
57 fileName: null,
58 lineNumber: null,
59 columnNumber: null,
60 },
61 ],
62 }),
63 ),
64 ).toBe(true);
65 });
66
67 it("rejects null and primitive inputs", () => {
68 expect(isPickedElementPayload(null)).toBe(false);
69 expect(isPickedElementPayload(undefined)).toBe(false);
70 expect(isPickedElementPayload("string")).toBe(false);
71 expect(isPickedElementPayload(42)).toBe(false);
72 expect(isPickedElementPayload([])).toBe(false);
73 });
74
75 it.each<[string, Record<string, unknown>]>([
76 ["missing pageUrl", validPayload({ pageUrl: undefined })],
77 ["wrong-type pageUrl", validPayload({ pageUrl: 123 })],
78 ["missing tagName", validPayload({ tagName: undefined })],
79 ["missing htmlPreview", validPayload({ htmlPreview: undefined })],
80 ["missing styles", validPayload({ styles: undefined })],
81 ["missing pickedAt", validPayload({ pickedAt: undefined })],
82 ["wrong-type pageTitle", validPayload({ pageTitle: 99 })],
83 ["wrong-type selector", validPayload({ selector: 99 })],
84 ["wrong-type componentName", validPayload({ componentName: 99 })],
85 ])("rejects payloads with %s", (_label, value) => {
86 expect(isPickedElementPayload(value)).toBe(false);
87 });
88
89 it("rejects malformed source frames", () => {
90 expect(
91 isPickedElementPayload(
92 validPayload({
93 source: {
94 functionName: 0,
95 fileName: null,
96 lineNumber: null,
97 columnNumber: null,
98 },
99 }),
100 ),
101 ).toBe(false);
102 });
103
104 it("rejects non-finite numeric line/column numbers", () => {
105 expect(
106 isPickedElementPayload(
107 validPayload({
108 source: {
109 functionName: null,
110 fileName: null,
111 lineNumber: Number.POSITIVE_INFINITY,
112 columnNumber: null,
113 },
114 }),
115 ),
116 ).toBe(false);
117 expect(
118 isPickedElementPayload(
119 validPayload({
120 source: {
121 functionName: null,
122 fileName: null,
123 lineNumber: Number.NaN,
124 columnNumber: null,
125 },
126 }),
127 ),
128 ).toBe(false);
129 });
130
131 it("rejects malformed stack arrays", () => {
132 expect(isPickedElementPayload(validPayload({ stack: "not-an-array" }))).toBe(false);
133 expect(isPickedElementPayload(validPayload({ stack: [{ bogus: true }] }))).toBe(false);
134 });
135});
136
137function validAnnotation(overrides?: Record<string, unknown>): Record<string, unknown> {
138 return {
139 id: "annotation_1",
140 pageUrl: "https://example.com/",
141 pageTitle: "Example",
142 comment: "Make this clearer",
143 elements: [
144 {
145 id: "element_1",
146 element: validPayload(),
147 rect: { x: 10, y: 20, width: 100, height: 40 },
148 },
149 ],
150 regions: [{ id: "region_1", rect: { x: 5, y: 6, width: 20, height: 30 } }],
151 strokes: [
152 {
153 id: "stroke_1",
154 color: "#7c3aed",
155 width: 4,
156 points: [
157 { x: 10, y: 10 },
158 { x: 20, y: 20 },
159 ],
160 bounds: { x: 6, y: 6, width: 18, height: 18 },
161 },
162 ],
163 styleChanges: [
164 {
165 targetId: "element_1",
166 selector: "button.submit",
167 property: "opacity",
168 previousValue: "1",
169 value: "0.5",
170 },
171 ],
172 screenshot: null,
173 createdAt: "2026-06-11T00:00:00.000Z",
174 ...overrides,
175 };
176}
177
178describe("isPreviewAnnotationPayload", () => {
179 it("accepts a structured annotation draft before screenshot capture", () => {
180 expect(isPreviewAnnotationPayload(validAnnotation())).toBe(true);
181 });
182
183 it("rejects screenshots supplied by the guest preload", () => {
184 expect(isPreviewAnnotationPayload(validAnnotation({ screenshot: { dataUrl: "bad" } }))).toBe(
185 false,
186 );
187 });
188
189 it("rejects malformed geometry and nested element payloads", () => {
190 expect(
191 isPreviewAnnotationPayload(
192 validAnnotation({ regions: [{ id: "region_1", rect: { x: 0, y: 0, width: "wide" } }] }),
193 ),
194 ).toBe(false);
195 expect(
196 isPreviewAnnotationPayload(
197 validAnnotation({ elements: [{ id: "element_1", element: {}, rect: {} }] }),
198 ),
199 ).toBe(false);
200 });
201});