@demon-utils/playwright 0.1.5 → 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.
@@ -1,15 +1,26 @@
1
1
  import { describe, test, expect } from "bun:test";
2
2
 
3
- import type { ReviewMetadata } from "./review-types.ts";
3
+ import type { ReviewMetadata, CodeReview } from "./review-types.ts";
4
4
  import { generateReviewHtml } from "./html-generator.ts";
5
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
+
6
17
  function makeMetadata(overrides?: Partial<ReviewMetadata>): ReviewMetadata {
7
18
  return {
8
19
  demos: [
9
20
  {
10
21
  file: "login-flow.webm",
11
22
  summary: "Shows the login flow end to end",
12
- annotations: [
23
+ steps: [
13
24
  { timestampSeconds: 0, text: "Page loads" },
14
25
  { timestampSeconds: 5, text: "User types credentials" },
15
26
  { timestampSeconds: 12, text: "Login succeeds" },
@@ -18,12 +29,13 @@ function makeMetadata(overrides?: Partial<ReviewMetadata>): ReviewMetadata {
18
29
  {
19
30
  file: "signup.webm",
20
31
  summary: "Demonstrates the signup process",
21
- annotations: [
32
+ steps: [
22
33
  { timestampSeconds: 0, text: "Signup form appears" },
23
34
  { timestampSeconds: 8, text: "Form submitted" },
24
35
  ],
25
36
  },
26
37
  ],
38
+ review: makeReview(),
27
39
  ...overrides,
28
40
  };
29
41
  }
@@ -54,9 +66,13 @@ describe("generateReviewHtml", () => {
54
66
  expect(html).toContain('src="login-flow.webm"');
55
67
  });
56
68
 
57
- test("has controls attribute", () => {
69
+ test("has custom controls bar instead of native controls", () => {
58
70
  const html = generateReviewHtml({ metadata: makeMetadata() });
59
- expect(html).toContain("<video id=\"review-video\" controls");
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"');
60
76
  });
61
77
  });
62
78
 
@@ -119,7 +135,7 @@ describe("generateReviewHtml", () => {
119
135
  {
120
136
  file: "test.webm",
121
137
  summary: "contains </script> tag",
122
- annotations: [],
138
+ steps: [],
123
139
  },
124
140
  ],
125
141
  });
@@ -136,7 +152,7 @@ describe("generateReviewHtml", () => {
136
152
  {
137
153
  file: '<img src="x">.webm',
138
154
  summary: "normal summary",
139
- annotations: [],
155
+ steps: [],
140
156
  },
141
157
  ],
142
158
  });
@@ -161,19 +177,19 @@ describe("generateReviewHtml", () => {
161
177
  ).toThrow("metadata.demos must not be empty");
162
178
  });
163
179
 
164
- test("handles demo with many annotations", () => {
165
- const annotations = Array.from({ length: 100 }, (_, i) => ({
180
+ test("handles demo with many steps", () => {
181
+ const steps = Array.from({ length: 100 }, (_, i) => ({
166
182
  timestampSeconds: i * 10,
167
- text: `Annotation ${i}`,
183
+ text: `Step ${i}`,
168
184
  }));
169
185
  const metadata: ReviewMetadata = {
170
186
  demos: [
171
- { file: "long.webm", summary: "Long demo", annotations },
187
+ { file: "long.webm", summary: "Long demo", steps },
172
188
  ],
173
189
  };
174
190
  const html = generateReviewHtml({ metadata });
175
- expect(html).toContain("Annotation 0");
176
- expect(html).toContain("Annotation 99");
191
+ expect(html).toContain("Step 0");
192
+ expect(html).toContain("Step 99");
177
193
  });
178
194
 
179
195
  test("handles single demo", () => {
@@ -182,7 +198,7 @@ describe("generateReviewHtml", () => {
182
198
  {
183
199
  file: "only.webm",
184
200
  summary: "The only demo",
185
- annotations: [{ timestampSeconds: 3, text: "Something happens" }],
201
+ steps: [{ timestampSeconds: 3, text: "Something happens" }],
186
202
  },
187
203
  ],
188
204
  };
@@ -192,4 +208,354 @@ describe("generateReviewHtml", () => {
192
208
  expect(html).toContain('src="only.webm"');
193
209
  });
194
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
+ });
195
561
  });