blob: aa129df8dbed47568046f3fc4e198e7dceb224e3 [file] [log] [blame]
bankseancdb08a52025-07-02 20:28:29 +00001/* eslint-disable @typescript-eslint/no-explicit-any */
2import { test, expect } from "@sand4rt/experimental-ct-web";
3import { SketchTodoPanel } from "./sketch-todo-panel";
4import { TodoItem } from "../types";
5
6// Helper function to create mock todo items
7function createMockTodoItem(props: Partial<TodoItem> = {}): TodoItem {
8 return {
9 id: props.id || "task-1",
10 status: props.status || "queued",
11 task: props.task || "Sample task description",
12 ...props,
13 };
14}
15
16test("initializes with default properties", async ({ mount }) => {
17 const component = await mount(SketchTodoPanel, {});
18
19 // Check default properties
20 const visible = await component.evaluate((el: SketchTodoPanel) => el.visible);
21 expect(visible).toBe(false);
22
23 // When not visible, component should not render content
24 const content = await component.textContent();
25 expect(content?.trim()).toBe("");
26});
27
28test("displays empty state when visible but no data", async ({ mount }) => {
29 const component = await mount(SketchTodoPanel, {
30 props: {
31 visible: true,
32 },
33 });
34
35 // Should show empty state message
36 await expect(component).toContainText("No todos available");
37});
38
39test("displays loading state correctly", async ({ mount }) => {
40 const component = await mount(SketchTodoPanel, {
41 props: {
42 visible: true,
43 },
44 });
45
46 // Set loading state through component method
47 await component.evaluate((el: SketchTodoPanel) => {
48 (el as any).loading = true;
49 });
50
51 // Should show loading message and spinner
52 await expect(component).toContainText("Loading todos...");
53 // Check for spinner element (it has animate-spin class)
54 await expect(component.locator(".animate-spin")).toBeVisible();
55});
56
57test("displays error state correctly", async ({ mount }) => {
58 const component = await mount(SketchTodoPanel, {
59 props: {
60 visible: true,
61 },
62 });
63
64 // Set error state through component method
65 await component.evaluate((el: SketchTodoPanel) => {
66 (el as any).error = "Failed to load todo data";
67 });
68
69 // Should show error message
70 await expect(component).toContainText("Error: Failed to load todo data");
71 // Error text should have red color
72 await expect(component.locator(".text-red-600")).toBeVisible();
73});
74
75test("renders todo items correctly", async ({ mount }) => {
76 const mockTodos = [
77 createMockTodoItem({
78 id: "task-1",
79 status: "completed",
80 task: "Complete the first task",
81 }),
82 createMockTodoItem({
83 id: "task-2",
84 status: "in-progress",
85 task: "Work on the second task",
86 }),
87 createMockTodoItem({
88 id: "task-3",
89 status: "queued",
90 task: "Start the third task",
91 }),
92 ];
93
94 const component = await mount(SketchTodoPanel, {
95 props: {
96 visible: true,
97 },
98 });
99
100 // Update component with todo data
101 await component.evaluate((el: SketchTodoPanel, todos) => {
102 el.updateTodoContent(JSON.stringify({ items: todos }));
103 }, mockTodos);
104
105 // Check that all tasks are rendered
106 await expect(component).toContainText("Complete the first task");
107 await expect(component).toContainText("Work on the second task");
108 await expect(component).toContainText("Start the third task");
109
110 // Check that status icons are present (emojis)
111 await expect(component).toContainText("✅"); // completed
112 await expect(component).toContainText("🦉"); // in-progress
113 await expect(component).toContainText("⚪"); // queued
114});
115
116test("displays correct todo count in header", async ({ mount }) => {
117 const mockTodos = [
118 createMockTodoItem({ id: "task-1", status: "completed" }),
119 createMockTodoItem({ id: "task-2", status: "completed" }),
120 createMockTodoItem({ id: "task-3", status: "in-progress" }),
121 createMockTodoItem({ id: "task-4", status: "queued" }),
122 ];
123
124 const component = await mount(SketchTodoPanel, {
125 props: {
126 visible: true,
127 },
128 });
129
130 await component.evaluate((el: SketchTodoPanel, todos) => {
131 el.updateTodoContent(JSON.stringify({ items: todos }));
132 }, mockTodos);
133
134 // Should show "2/4" (2 completed out of 4 total)
135 await expect(component).toContainText("2/4");
136 await expect(component).toContainText("Sketching...");
137});
138
139test("shows comment button only for non-completed items", async ({ mount }) => {
140 const mockTodos = [
141 createMockTodoItem({ id: "task-1", status: "completed" }),
142 createMockTodoItem({ id: "task-2", status: "in-progress" }),
143 createMockTodoItem({ id: "task-3", status: "queued" }),
144 ];
145
146 const component = await mount(SketchTodoPanel, {
147 props: {
148 visible: true,
149 },
150 });
151
152 await component.evaluate((el: SketchTodoPanel, todos) => {
153 el.updateTodoContent(JSON.stringify({ items: todos }));
154 }, mockTodos);
155
156 // Comment buttons (💬) should only appear for in-progress and queued items
157 const commentButtons = component.locator('button[title*="Add comment"]');
158 await expect(commentButtons).toHaveCount(2); // Only for in-progress and queued
159});
160
161test("opens comment box when comment button is clicked", async ({ mount }) => {
162 const mockTodos = [
163 createMockTodoItem({
164 id: "task-1",
165 status: "in-progress",
166 task: "Work on important task",
167 }),
168 ];
169
170 const component = await mount(SketchTodoPanel, {
171 props: {
172 visible: true,
173 },
174 });
175
176 await component.evaluate((el: SketchTodoPanel, todos) => {
177 el.updateTodoContent(JSON.stringify({ items: todos }));
178 }, mockTodos);
179
180 // Click the comment button
181 await component.locator('button[title*="Add comment"]').click();
182
183 // Comment overlay should be visible
184 await expect(component.locator(".fixed.inset-0")).toBeVisible();
185 await expect(component).toContainText("Comment on TODO Item");
186 await expect(component).toContainText("Status: In Progress");
187 await expect(component).toContainText("Work on important task");
188});
189
190test("closes comment box when cancel is clicked", async ({ mount }) => {
191 const mockTodos = [createMockTodoItem({ id: "task-1", status: "queued" })];
192
193 const component = await mount(SketchTodoPanel, {
194 props: {
195 visible: true,
196 },
197 });
198
199 await component.evaluate((el: SketchTodoPanel, todos) => {
200 el.updateTodoContent(JSON.stringify({ items: todos }));
201 }, mockTodos);
202
203 // Open comment box
204 await component.locator('button[title*="Add comment"]').click();
205 await expect(component.locator(".fixed.inset-0")).toBeVisible();
206
207 // Click cancel
208 await component.locator('button:has-text("Cancel")').click();
209
210 // Comment overlay should be hidden
211 await expect(component.locator(".fixed.inset-0")).not.toBeVisible();
212});
213
214test("dispatches todo-comment event when comment is submitted", async ({
215 mount,
216}) => {
217 const mockTodos = [
218 createMockTodoItem({
219 id: "task-1",
220 status: "in-progress",
221 task: "Important task",
222 }),
223 ];
224
225 const component = await mount(SketchTodoPanel, {
226 props: {
227 visible: true,
228 },
229 });
230
231 await component.evaluate((el: SketchTodoPanel, todos) => {
232 el.updateTodoContent(JSON.stringify({ items: todos }));
233 }, mockTodos);
234
235 // Set up event listener
236 const eventPromise = component.evaluate((el: SketchTodoPanel) => {
237 return new Promise((resolve) => {
238 (el as any).addEventListener(
239 "todo-comment",
240 (e: any) => {
241 resolve(e.detail.comment);
242 },
243 { once: true },
244 );
245 });
246 });
247
248 // Open comment box
249 await component.locator('button[title*="Add comment"]').click();
250
251 // Fill in comment
252 await component
253 .locator('textarea[placeholder*="Type your comment"]')
254 .fill("This is a test comment");
255
256 // Submit comment
257 await component.locator('button:has-text("Add Comment")').click();
258
259 // Wait for event and check content
260 const eventDetail = await eventPromise;
261 expect(eventDetail).toContain("This is a test comment");
262 expect(eventDetail).toContain("Important task");
263 expect(eventDetail).toContain("In Progress");
264
265 // Comment box should be closed after submission
266 await expect(component.locator(".fixed.inset-0")).not.toBeVisible();
267});
268
269test("handles invalid JSON gracefully", async ({ mount }) => {
270 const component = await mount(SketchTodoPanel, {
271 props: {
272 visible: true,
273 },
274 });
275
276 // Update with invalid JSON
277 await component.evaluate((el: SketchTodoPanel) => {
278 el.updateTodoContent("{invalid json}");
279 });
280
281 // Should show error state
282 await expect(component).toContainText("Error: Failed to parse todo data");
283});
284
285test("handles empty content gracefully", async ({ mount }) => {
286 const component = await mount(SketchTodoPanel, {
287 props: {
288 visible: true,
289 },
290 });
291
292 // Update with empty content
293 await component.evaluate((el: SketchTodoPanel) => {
294 el.updateTodoContent("");
295 });
296
297 // Should show empty state
298 await expect(component).toContainText("No todos available");
299});
300
301test("renders with proper Tailwind classes", async ({ mount }) => {
302 const component = await mount(SketchTodoPanel, {
303 props: {
304 visible: true,
305 },
306 });
307
308 // Check main container has correct Tailwind classes
309 const container = component.locator(".flex.flex-col.h-full");
310 await expect(container).toBeVisible();
bankseancdb08a52025-07-02 20:28:29 +0000311});
312
313test("displays todos in scrollable container", async ({ mount }) => {
314 const mockTodos = Array.from({ length: 10 }, (_, i) =>
315 createMockTodoItem({
316 id: `task-${i + 1}`,
317 status:
318 i % 3 === 0 ? "completed" : i % 3 === 1 ? "in-progress" : "queued",
319 task: `Task number ${i + 1} with some description text`,
320 }),
321 );
322
323 const component = await mount(SketchTodoPanel, {
324 props: {
325 visible: true,
326 },
327 });
328
329 await component.evaluate((el: SketchTodoPanel, todos) => {
330 el.updateTodoContent(JSON.stringify({ items: todos }));
331 }, mockTodos);
332
333 // Check that scrollable container exists
334 const scrollContainer = component.locator(".overflow-y-auto");
335 await expect(scrollContainer).toBeVisible();
336
337 // All tasks should be rendered
338 for (let i = 1; i <= 10; i++) {
339 await expect(component).toContainText(`Task number ${i}`);
340 }
341});