@demon-utils/playwright 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,561 @@
1
+ import { describe, test, expect } from "bun:test";
2
+
3
+ import type { ReviewMetadata, CodeReview } from "./review-types.ts";
4
+ import { generateReviewHtml } from "./html-generator.ts";
5
+
6
+ function makeReview(overrides?: Partial<CodeReview>): CodeReview {
7
+ return {
8
+ summary: "Good changes overall",
9
+ highlights: ["Clean implementation", "Good test coverage"],
10
+ verdict: "approve",
11
+ verdictReason: "No major issues found",
12
+ issues: [],
13
+ ...overrides,
14
+ };
15
+ }
16
+
17
+ function makeMetadata(overrides?: Partial<ReviewMetadata>): ReviewMetadata {
18
+ return {
19
+ demos: [
20
+ {
21
+ file: "login-flow.webm",
22
+ summary: "Shows the login flow end to end",
23
+ steps: [
24
+ { timestampSeconds: 0, text: "Page loads" },
25
+ { timestampSeconds: 5, text: "User types credentials" },
26
+ { timestampSeconds: 12, text: "Login succeeds" },
27
+ ],
28
+ },
29
+ {
30
+ file: "signup.webm",
31
+ summary: "Demonstrates the signup process",
32
+ steps: [
33
+ { timestampSeconds: 0, text: "Signup form appears" },
34
+ { timestampSeconds: 8, text: "Form submitted" },
35
+ ],
36
+ },
37
+ ],
38
+ review: makeReview(),
39
+ ...overrides,
40
+ };
41
+ }
42
+
43
+ describe("generateReviewHtml", () => {
44
+ describe("structure", () => {
45
+ test("starts with <!DOCTYPE html>", () => {
46
+ const html = generateReviewHtml({ metadata: makeMetadata() });
47
+ expect(html).toStartWith("<!DOCTYPE html>");
48
+ });
49
+
50
+ test("contains required elements", () => {
51
+ const html = generateReviewHtml({ metadata: makeMetadata() });
52
+ expect(html).toContain("<style>");
53
+ expect(html).toContain("</style>");
54
+ expect(html).toContain("<script>");
55
+ expect(html).toContain("</script>");
56
+ expect(html).toContain("<video");
57
+ expect(html).toContain("<header>");
58
+ expect(html).toContain('class="review-layout"');
59
+ });
60
+ });
61
+
62
+ describe("video element", () => {
63
+ test("has correct id and initial src", () => {
64
+ const html = generateReviewHtml({ metadata: makeMetadata() });
65
+ expect(html).toContain('id="review-video"');
66
+ expect(html).toContain('src="login-flow.webm"');
67
+ });
68
+
69
+ test("has custom controls bar instead of native controls", () => {
70
+ const html = generateReviewHtml({ metadata: makeMetadata() });
71
+ expect(html).not.toMatch(/<video[^>]*\bcontrols\b/);
72
+ expect(html).toContain('class="video-controls"');
73
+ expect(html).toContain('id="vc-play"');
74
+ expect(html).toContain('id="vc-seek"');
75
+ expect(html).toContain('id="vc-time"');
76
+ });
77
+ });
78
+
79
+ describe("title", () => {
80
+ test("defaults to Demo Review", () => {
81
+ const html = generateReviewHtml({ metadata: makeMetadata() });
82
+ expect(html).toContain("<title>Demo Review</title>");
83
+ expect(html).toContain("<h1>Demo Review</h1>");
84
+ });
85
+
86
+ test("uses custom title when provided", () => {
87
+ const html = generateReviewHtml({
88
+ metadata: makeMetadata(),
89
+ title: "My Custom Review",
90
+ });
91
+ expect(html).toContain("<title>My Custom Review</title>");
92
+ expect(html).toContain("<h1>My Custom Review</h1>");
93
+ });
94
+ });
95
+
96
+ describe("demo navigation", () => {
97
+ test("renders one button per demo", () => {
98
+ const html = generateReviewHtml({ metadata: makeMetadata() });
99
+ expect(html).toContain('data-index="0"');
100
+ expect(html).toContain('data-index="1"');
101
+ expect(html).not.toContain('data-index="2"');
102
+ });
103
+
104
+ test("first button has active class", () => {
105
+ const html = generateReviewHtml({ metadata: makeMetadata() });
106
+ expect(html).toContain('data-index="0" class="active"');
107
+ });
108
+
109
+ test("second button does not have active class", () => {
110
+ const html = generateReviewHtml({ metadata: makeMetadata() });
111
+ const idx1Match = html.match(/data-index="1"([^>]*)/);
112
+ expect(idx1Match).toBeTruthy();
113
+ expect(idx1Match![1]).not.toContain("active");
114
+ });
115
+
116
+ test("displays demo filenames", () => {
117
+ const html = generateReviewHtml({ metadata: makeMetadata() });
118
+ expect(html).toContain("login-flow.webm");
119
+ expect(html).toContain("signup.webm");
120
+ });
121
+ });
122
+
123
+ describe("metadata embedding", () => {
124
+ test("embeds metadata JSON in script", () => {
125
+ const metadata = makeMetadata();
126
+ const html = generateReviewHtml({ metadata });
127
+ expect(html).toContain("login-flow.webm");
128
+ expect(html).toContain("Shows the login flow end to end");
129
+ expect(html).toContain("var metadata =");
130
+ });
131
+
132
+ test("escapes </ in JSON to prevent script breakout", () => {
133
+ const metadata = makeMetadata({
134
+ demos: [
135
+ {
136
+ file: "test.webm",
137
+ summary: "contains </script> tag",
138
+ steps: [],
139
+ },
140
+ ],
141
+ });
142
+ const html = generateReviewHtml({ metadata });
143
+ expect(html).not.toContain("</script> tag");
144
+ expect(html).toContain("<\\/script> tag");
145
+ });
146
+ });
147
+
148
+ describe("HTML escaping", () => {
149
+ test("escapes special chars in filenames", () => {
150
+ const metadata = makeMetadata({
151
+ demos: [
152
+ {
153
+ file: '<img src="x">.webm',
154
+ summary: "normal summary",
155
+ steps: [],
156
+ },
157
+ ],
158
+ });
159
+ const html = generateReviewHtml({ metadata });
160
+ expect(html).not.toContain('<img src="x">');
161
+ expect(html).toContain("&lt;img src=&quot;x&quot;&gt;.webm");
162
+ });
163
+
164
+ test("escapes special chars in title", () => {
165
+ const html = generateReviewHtml({
166
+ metadata: makeMetadata(),
167
+ title: 'Test & <Review> "Page"',
168
+ });
169
+ expect(html).toContain("Test &amp; &lt;Review&gt; &quot;Page&quot;");
170
+ });
171
+ });
172
+
173
+ describe("edge cases", () => {
174
+ test("throws on empty demos array", () => {
175
+ expect(() =>
176
+ generateReviewHtml({ metadata: { demos: [] } }),
177
+ ).toThrow("metadata.demos must not be empty");
178
+ });
179
+
180
+ test("handles demo with many steps", () => {
181
+ const steps = Array.from({ length: 100 }, (_, i) => ({
182
+ timestampSeconds: i * 10,
183
+ text: `Step ${i}`,
184
+ }));
185
+ const metadata: ReviewMetadata = {
186
+ demos: [
187
+ { file: "long.webm", summary: "Long demo", steps },
188
+ ],
189
+ };
190
+ const html = generateReviewHtml({ metadata });
191
+ expect(html).toContain("Step 0");
192
+ expect(html).toContain("Step 99");
193
+ });
194
+
195
+ test("handles single demo", () => {
196
+ const metadata: ReviewMetadata = {
197
+ demos: [
198
+ {
199
+ file: "only.webm",
200
+ summary: "The only demo",
201
+ steps: [{ timestampSeconds: 3, text: "Something happens" }],
202
+ },
203
+ ],
204
+ };
205
+ const html = generateReviewHtml({ metadata });
206
+ expect(html).toContain('data-index="0"');
207
+ expect(html).not.toContain('data-index="1"');
208
+ expect(html).toContain('src="only.webm"');
209
+ });
210
+ });
211
+
212
+ describe("steps section", () => {
213
+ test("renders Steps section", () => {
214
+ const html = generateReviewHtml({ metadata: makeMetadata() });
215
+ expect(html).toContain('id="steps-section"');
216
+ expect(html).toContain('id="steps-list"');
217
+ });
218
+
219
+ test("includes step-active CSS class", () => {
220
+ const html = generateReviewHtml({ metadata: makeMetadata() });
221
+ expect(html).toContain("step-active");
222
+ });
223
+
224
+ test("includes timeupdate event handler", () => {
225
+ const html = generateReviewHtml({ metadata: makeMetadata() });
226
+ expect(html).toContain("timeupdate");
227
+ expect(html).toContain("#steps-list button[data-time]");
228
+ });
229
+
230
+ test("embeds step data in metadata JSON", () => {
231
+ const metadata: ReviewMetadata = {
232
+ demos: [
233
+ {
234
+ file: "demo.webm",
235
+ summary: "Demo with steps",
236
+ steps: [
237
+ { timestampSeconds: 1.5, text: "Step one" },
238
+ ],
239
+ },
240
+ ],
241
+ };
242
+ const html = generateReviewHtml({ metadata });
243
+ expect(html).toContain("Step one");
244
+ expect(html).toContain("1.5");
245
+ });
246
+
247
+ test("does not contain annotations section", () => {
248
+ const html = generateReviewHtml({ metadata: makeMetadata() });
249
+ expect(html).not.toContain("annotations-section");
250
+ expect(html).not.toContain("annotations-list");
251
+ });
252
+ });
253
+
254
+ describe("tabs", () => {
255
+ test("renders tab bar with Summary and Demos tabs when review present", () => {
256
+ const html = generateReviewHtml({ metadata: makeMetadata() });
257
+ expect(html).toContain('class="tab-bar"');
258
+ expect(html).toContain('data-tab="summary"');
259
+ expect(html).toContain('data-tab="demos"');
260
+ expect(html).toContain(">Summary</button>");
261
+ expect(html).toContain(">Demos</button>");
262
+ });
263
+
264
+ test("renders only Demos tab when review is absent", () => {
265
+ const html = generateReviewHtml({
266
+ metadata: makeMetadata({ review: undefined }),
267
+ });
268
+ expect(html).toContain('class="tab-bar"');
269
+ expect(html).not.toContain('data-tab="summary"');
270
+ expect(html).toContain('data-tab="demos"');
271
+ });
272
+
273
+ test("Summary tab is active by default when review present", () => {
274
+ const html = generateReviewHtml({ metadata: makeMetadata() });
275
+ const summaryBtn = html.match(/class="tab-btn([^"]*)"[^>]*data-tab="summary"/);
276
+ expect(summaryBtn).toBeTruthy();
277
+ expect(summaryBtn![1]).toContain("active");
278
+
279
+ const demosBtn = html.match(/class="tab-btn([^"]*)"[^>]*data-tab="demos"/);
280
+ expect(demosBtn).toBeTruthy();
281
+ expect(demosBtn![1]).not.toContain("active");
282
+ });
283
+
284
+ test("Demos tab is active by default when review is absent", () => {
285
+ const html = generateReviewHtml({
286
+ metadata: makeMetadata({ review: undefined }),
287
+ });
288
+ const demosBtn = html.match(/class="tab-btn([^"]*)"[^>]*data-tab="demos"/);
289
+ expect(demosBtn).toBeTruthy();
290
+ expect(demosBtn![1]).toContain("active");
291
+ });
292
+
293
+ test("tab-summary panel is active when review present", () => {
294
+ const html = generateReviewHtml({ metadata: makeMetadata() });
295
+ expect(html).toContain('id="tab-summary"');
296
+ const panel = html.match(/id="tab-summary"[^>]*class="tab-panel([^"]*)"/);
297
+ expect(panel).toBeTruthy();
298
+ expect(panel![1]).toContain("active");
299
+ });
300
+
301
+ test("tab-demos panel is not active when review present", () => {
302
+ const html = generateReviewHtml({ metadata: makeMetadata() });
303
+ const panel = html.match(/id="tab-demos"[^>]*class="tab-panel([^"]*)"/);
304
+ expect(panel).toBeTruthy();
305
+ expect(panel![1]).not.toContain("active");
306
+ });
307
+
308
+ test("tab-demos panel is active when review absent", () => {
309
+ const html = generateReviewHtml({
310
+ metadata: makeMetadata({ review: undefined }),
311
+ });
312
+ const panel = html.match(/id="tab-demos"[^>]*class="tab-panel([^"]*)"/);
313
+ expect(panel).toBeTruthy();
314
+ expect(panel![1]).toContain("active");
315
+ });
316
+
317
+ test("no tab-summary panel when review absent", () => {
318
+ const html = generateReviewHtml({
319
+ metadata: makeMetadata({ review: undefined }),
320
+ });
321
+ expect(html).not.toContain('id="tab-summary"');
322
+ });
323
+
324
+ test("includes tab switching JS", () => {
325
+ const html = generateReviewHtml({ metadata: makeMetadata() });
326
+ expect(html).toContain("tabBtns");
327
+ expect(html).toContain("tabPanels");
328
+ expect(html).toContain('data-tab');
329
+ });
330
+ });
331
+
332
+ describe("review section", () => {
333
+ test("renders review section when review is present", () => {
334
+ const html = generateReviewHtml({ metadata: makeMetadata() });
335
+ expect(html).toContain('class="review-section"');
336
+ expect(html).toContain('class="review-body"');
337
+ });
338
+
339
+ test("does not render review section when review is absent", () => {
340
+ const html = generateReviewHtml({
341
+ metadata: makeMetadata({ review: undefined }),
342
+ });
343
+ expect(html).not.toContain('class="review-section"');
344
+ expect(html).not.toContain('class="verdict-banner');
345
+ });
346
+
347
+ test("verdict banner has approve class for approve verdict", () => {
348
+ const html = generateReviewHtml({
349
+ metadata: makeMetadata({ review: makeReview({ verdict: "approve" }) }),
350
+ });
351
+ expect(html).toContain('class="verdict-banner approve"');
352
+ expect(html).toContain("Approved");
353
+ });
354
+
355
+ test("verdict banner has request-changes class for request_changes verdict", () => {
356
+ const html = generateReviewHtml({
357
+ metadata: makeMetadata({ review: makeReview({ verdict: "request_changes" }) }),
358
+ });
359
+ expect(html).toContain('class="verdict-banner request-changes"');
360
+ expect(html).toContain("Changes Requested");
361
+ });
362
+
363
+ test("renders review summary", () => {
364
+ const html = generateReviewHtml({
365
+ metadata: makeMetadata({ review: makeReview({ summary: "Overall solid work" }) }),
366
+ });
367
+ expect(html).toContain("Overall solid work");
368
+ });
369
+
370
+ test("renders highlights list", () => {
371
+ const html = generateReviewHtml({
372
+ metadata: makeMetadata({
373
+ review: makeReview({ highlights: ["Fast response", "Good error handling"] }),
374
+ }),
375
+ });
376
+ expect(html).toContain('class="highlights-list"');
377
+ expect(html).toContain("Fast response");
378
+ expect(html).toContain("Good error handling");
379
+ });
380
+
381
+ test("renders issues with severity badges", () => {
382
+ const html = generateReviewHtml({
383
+ metadata: makeMetadata({
384
+ review: makeReview({
385
+ issues: [
386
+ { severity: "major", description: "Memory leak in handler" },
387
+ { severity: "minor", description: "Missing edge case" },
388
+ { severity: "nit", description: "Rename variable" },
389
+ ],
390
+ }),
391
+ }),
392
+ });
393
+ expect(html).toContain('class="issue major"');
394
+ expect(html).toContain('class="issue minor"');
395
+ expect(html).toContain('class="issue nit"');
396
+ expect(html).toContain('class="severity-badge">MAJOR');
397
+ expect(html).toContain('class="severity-badge">MINOR');
398
+ expect(html).toContain('class="severity-badge">NIT');
399
+ expect(html).toContain("Memory leak in handler");
400
+ expect(html).toContain("Missing edge case");
401
+ expect(html).toContain("Rename variable");
402
+ });
403
+
404
+ test("shows no-issues message when issues array is empty", () => {
405
+ const html = generateReviewHtml({
406
+ metadata: makeMetadata({ review: makeReview({ issues: [] }) }),
407
+ });
408
+ expect(html).toContain('class="no-issues"');
409
+ expect(html).toContain("No issues found");
410
+ });
411
+
412
+ test("escapes HTML in review text", () => {
413
+ const html = generateReviewHtml({
414
+ metadata: makeMetadata({
415
+ review: makeReview({
416
+ summary: '<script>alert("xss")</script>',
417
+ verdictReason: "Uses <b>dangerous</b> patterns",
418
+ highlights: ['Handles <img src="x"> gracefully'],
419
+ issues: [
420
+ { severity: "major", description: 'Found <script>alert("xss")</script>' },
421
+ ],
422
+ }),
423
+ }),
424
+ });
425
+ expect(html).not.toContain('<script>alert("xss")</script>');
426
+ expect(html).toContain("&lt;script&gt;alert");
427
+ expect(html).toContain("&lt;b&gt;dangerous&lt;/b&gt;");
428
+ });
429
+
430
+ test("review section is inside summary tab panel", () => {
431
+ const html = generateReviewHtml({ metadata: makeMetadata() });
432
+ const summaryPanelIdx = html.indexOf('id="tab-summary"');
433
+ const reviewSectionIdx = html.indexOf('class="review-section"');
434
+ const demosPanelIdx = html.indexOf('id="tab-demos"');
435
+ expect(summaryPanelIdx).toBeGreaterThan(-1);
436
+ expect(reviewSectionIdx).toBeGreaterThan(summaryPanelIdx);
437
+ expect(reviewSectionIdx).toBeLessThan(demosPanelIdx);
438
+ });
439
+
440
+ test("renders verdict reason", () => {
441
+ const html = generateReviewHtml({
442
+ metadata: makeMetadata({
443
+ review: makeReview({ verdictReason: "All tests pass and code is clean" }),
444
+ }),
445
+ });
446
+ expect(html).toContain("All tests pass and code is clean");
447
+ });
448
+ });
449
+
450
+ describe("feedback tab", () => {
451
+ test("renders Feedback tab button when review is present", () => {
452
+ const html = generateReviewHtml({ metadata: makeMetadata() });
453
+ expect(html).toContain('data-tab="feedback"');
454
+ expect(html).toContain(">Feedback</button>");
455
+ });
456
+
457
+ test("does not render Feedback tab button when review is absent", () => {
458
+ const html = generateReviewHtml({
459
+ metadata: makeMetadata({ review: undefined }),
460
+ });
461
+ expect(html).not.toContain('data-tab="feedback"');
462
+ });
463
+
464
+ test("renders Feedback tab panel when review is present", () => {
465
+ const html = generateReviewHtml({ metadata: makeMetadata() });
466
+ expect(html).toContain('id="tab-feedback"');
467
+ });
468
+
469
+ test("does not render Feedback tab panel when review is absent", () => {
470
+ const html = generateReviewHtml({
471
+ metadata: makeMetadata({ review: undefined }),
472
+ });
473
+ expect(html).not.toContain('id="tab-feedback"');
474
+ });
475
+
476
+ test("Feedback tab is not active by default", () => {
477
+ const html = generateReviewHtml({ metadata: makeMetadata() });
478
+ const feedbackBtn = html.match(/class="tab-btn([^"]*)"[^>]*data-tab="feedback"/);
479
+ expect(feedbackBtn).toBeTruthy();
480
+ expect(feedbackBtn![1]).not.toContain("active");
481
+
482
+ const feedbackPanel = html.match(/id="tab-feedback"[^>]*class="tab-panel([^"]*)"/);
483
+ expect(feedbackPanel).toBeTruthy();
484
+ expect(feedbackPanel![1]).not.toContain("active");
485
+ });
486
+
487
+ test("renders + buttons on issues matching issue count", () => {
488
+ const issues = [
489
+ { severity: "major" as const, description: "Memory leak" },
490
+ { severity: "minor" as const, description: "Missing test" },
491
+ { severity: "nit" as const, description: "Rename var" },
492
+ ];
493
+ const html = generateReviewHtml({
494
+ metadata: makeMetadata({ review: makeReview({ issues }) }),
495
+ });
496
+ const matches = html.match(/class="feedback-add-issue"/g);
497
+ expect(matches).toBeTruthy();
498
+ expect(matches!.length).toBe(3);
499
+ });
500
+
501
+ test("+ buttons have escaped data-issue attribute", () => {
502
+ const html = generateReviewHtml({
503
+ metadata: makeMetadata({
504
+ review: makeReview({
505
+ issues: [{ severity: "major", description: 'Use <b>"safe"</b> API' }],
506
+ }),
507
+ }),
508
+ });
509
+ expect(html).toContain('data-issue="Use &lt;b&gt;&quot;safe&quot;&lt;/b&gt; API"');
510
+ });
511
+
512
+ test("does not render + buttons when there are no issues", () => {
513
+ const html = generateReviewHtml({
514
+ metadata: makeMetadata({ review: makeReview({ issues: [] }) }),
515
+ });
516
+ expect(html).not.toContain('class="feedback-add-issue"');
517
+ });
518
+
519
+ test("renders floating selection button when review is present", () => {
520
+ const html = generateReviewHtml({ metadata: makeMetadata() });
521
+ expect(html).toContain('id="feedback-selection-btn"');
522
+ });
523
+
524
+ test("does not render floating selection button when review is absent", () => {
525
+ const html = generateReviewHtml({
526
+ metadata: makeMetadata({ review: undefined }),
527
+ });
528
+ expect(html).not.toContain('id="feedback-selection-btn"');
529
+ });
530
+
531
+ test("includes feedback CSS classes in output", () => {
532
+ const html = generateReviewHtml({ metadata: makeMetadata() });
533
+ expect(html).toContain(".feedback-add-issue");
534
+ expect(html).toContain("#feedback-selection-btn");
535
+ expect(html).toContain(".feedback-layout");
536
+ expect(html).toContain(".feedback-left");
537
+ expect(html).toContain(".feedback-right");
538
+ expect(html).toContain(".feedback-remove");
539
+ expect(html).toContain("#feedback-general");
540
+ expect(html).toContain("#feedback-preview");
541
+ expect(html).toContain("#feedback-copy");
542
+ });
543
+
544
+ test("includes feedback JS function names in output", () => {
545
+ const html = generateReviewHtml({ metadata: makeMetadata() });
546
+ expect(html).toContain("addFeedbackItem");
547
+ expect(html).toContain("removeFeedbackItem");
548
+ expect(html).toContain("renderFeedback");
549
+ expect(html).toContain("updatePreview");
550
+ });
551
+
552
+ test("feedback panel is inside main element", () => {
553
+ const html = generateReviewHtml({ metadata: makeMetadata() });
554
+ const mainIdx = html.indexOf("<main>");
555
+ const feedbackIdx = html.indexOf('id="tab-feedback"');
556
+ const mainCloseIdx = html.indexOf("</main>");
557
+ expect(feedbackIdx).toBeGreaterThan(mainIdx);
558
+ expect(feedbackIdx).toBeLessThan(mainCloseIdx);
559
+ });
560
+ });
561
+ });