@demon-utils/playwright 0.1.5 → 0.1.7

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,195 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
-
3
- import type { ReviewMetadata } from "./review-types.ts";
4
- import { generateReviewHtml } from "./html-generator.ts";
5
-
6
- function makeMetadata(overrides?: Partial<ReviewMetadata>): ReviewMetadata {
7
- return {
8
- demos: [
9
- {
10
- file: "login-flow.webm",
11
- summary: "Shows the login flow end to end",
12
- annotations: [
13
- { timestampSeconds: 0, text: "Page loads" },
14
- { timestampSeconds: 5, text: "User types credentials" },
15
- { timestampSeconds: 12, text: "Login succeeds" },
16
- ],
17
- },
18
- {
19
- file: "signup.webm",
20
- summary: "Demonstrates the signup process",
21
- annotations: [
22
- { timestampSeconds: 0, text: "Signup form appears" },
23
- { timestampSeconds: 8, text: "Form submitted" },
24
- ],
25
- },
26
- ],
27
- ...overrides,
28
- };
29
- }
30
-
31
- describe("generateReviewHtml", () => {
32
- describe("structure", () => {
33
- test("starts with <!DOCTYPE html>", () => {
34
- const html = generateReviewHtml({ metadata: makeMetadata() });
35
- expect(html).toStartWith("<!DOCTYPE html>");
36
- });
37
-
38
- test("contains required elements", () => {
39
- const html = generateReviewHtml({ metadata: makeMetadata() });
40
- expect(html).toContain("<style>");
41
- expect(html).toContain("</style>");
42
- expect(html).toContain("<script>");
43
- expect(html).toContain("</script>");
44
- expect(html).toContain("<video");
45
- expect(html).toContain("<header>");
46
- expect(html).toContain('class="review-layout"');
47
- });
48
- });
49
-
50
- describe("video element", () => {
51
- test("has correct id and initial src", () => {
52
- const html = generateReviewHtml({ metadata: makeMetadata() });
53
- expect(html).toContain('id="review-video"');
54
- expect(html).toContain('src="login-flow.webm"');
55
- });
56
-
57
- test("has controls attribute", () => {
58
- const html = generateReviewHtml({ metadata: makeMetadata() });
59
- expect(html).toContain("<video id=\"review-video\" controls");
60
- });
61
- });
62
-
63
- describe("title", () => {
64
- test("defaults to Demo Review", () => {
65
- const html = generateReviewHtml({ metadata: makeMetadata() });
66
- expect(html).toContain("<title>Demo Review</title>");
67
- expect(html).toContain("<h1>Demo Review</h1>");
68
- });
69
-
70
- test("uses custom title when provided", () => {
71
- const html = generateReviewHtml({
72
- metadata: makeMetadata(),
73
- title: "My Custom Review",
74
- });
75
- expect(html).toContain("<title>My Custom Review</title>");
76
- expect(html).toContain("<h1>My Custom Review</h1>");
77
- });
78
- });
79
-
80
- describe("demo navigation", () => {
81
- test("renders one button per demo", () => {
82
- const html = generateReviewHtml({ metadata: makeMetadata() });
83
- expect(html).toContain('data-index="0"');
84
- expect(html).toContain('data-index="1"');
85
- expect(html).not.toContain('data-index="2"');
86
- });
87
-
88
- test("first button has active class", () => {
89
- const html = generateReviewHtml({ metadata: makeMetadata() });
90
- expect(html).toContain('data-index="0" class="active"');
91
- });
92
-
93
- test("second button does not have active class", () => {
94
- const html = generateReviewHtml({ metadata: makeMetadata() });
95
- const idx1Match = html.match(/data-index="1"([^>]*)/);
96
- expect(idx1Match).toBeTruthy();
97
- expect(idx1Match![1]).not.toContain("active");
98
- });
99
-
100
- test("displays demo filenames", () => {
101
- const html = generateReviewHtml({ metadata: makeMetadata() });
102
- expect(html).toContain("login-flow.webm");
103
- expect(html).toContain("signup.webm");
104
- });
105
- });
106
-
107
- describe("metadata embedding", () => {
108
- test("embeds metadata JSON in script", () => {
109
- const metadata = makeMetadata();
110
- const html = generateReviewHtml({ metadata });
111
- expect(html).toContain("login-flow.webm");
112
- expect(html).toContain("Shows the login flow end to end");
113
- expect(html).toContain("var metadata =");
114
- });
115
-
116
- test("escapes </ in JSON to prevent script breakout", () => {
117
- const metadata = makeMetadata({
118
- demos: [
119
- {
120
- file: "test.webm",
121
- summary: "contains </script> tag",
122
- annotations: [],
123
- },
124
- ],
125
- });
126
- const html = generateReviewHtml({ metadata });
127
- expect(html).not.toContain("</script> tag");
128
- expect(html).toContain("<\\/script> tag");
129
- });
130
- });
131
-
132
- describe("HTML escaping", () => {
133
- test("escapes special chars in filenames", () => {
134
- const metadata = makeMetadata({
135
- demos: [
136
- {
137
- file: '<img src="x">.webm',
138
- summary: "normal summary",
139
- annotations: [],
140
- },
141
- ],
142
- });
143
- const html = generateReviewHtml({ metadata });
144
- expect(html).not.toContain('<img src="x">');
145
- expect(html).toContain("&lt;img src=&quot;x&quot;&gt;.webm");
146
- });
147
-
148
- test("escapes special chars in title", () => {
149
- const html = generateReviewHtml({
150
- metadata: makeMetadata(),
151
- title: 'Test & <Review> "Page"',
152
- });
153
- expect(html).toContain("Test &amp; &lt;Review&gt; &quot;Page&quot;");
154
- });
155
- });
156
-
157
- describe("edge cases", () => {
158
- test("throws on empty demos array", () => {
159
- expect(() =>
160
- generateReviewHtml({ metadata: { demos: [] } }),
161
- ).toThrow("metadata.demos must not be empty");
162
- });
163
-
164
- test("handles demo with many annotations", () => {
165
- const annotations = Array.from({ length: 100 }, (_, i) => ({
166
- timestampSeconds: i * 10,
167
- text: `Annotation ${i}`,
168
- }));
169
- const metadata: ReviewMetadata = {
170
- demos: [
171
- { file: "long.webm", summary: "Long demo", annotations },
172
- ],
173
- };
174
- const html = generateReviewHtml({ metadata });
175
- expect(html).toContain("Annotation 0");
176
- expect(html).toContain("Annotation 99");
177
- });
178
-
179
- test("handles single demo", () => {
180
- const metadata: ReviewMetadata = {
181
- demos: [
182
- {
183
- file: "only.webm",
184
- summary: "The only demo",
185
- annotations: [{ timestampSeconds: 3, text: "Something happens" }],
186
- },
187
- ],
188
- };
189
- const html = generateReviewHtml({ metadata });
190
- expect(html).toContain('data-index="0"');
191
- expect(html).not.toContain('data-index="1"');
192
- expect(html).toContain('src="only.webm"');
193
- });
194
- });
195
- });
@@ -1,152 +0,0 @@
1
- import type { ReviewMetadata } from "./review-types.ts";
2
-
3
- export interface GenerateReviewHtmlOptions {
4
- metadata: ReviewMetadata;
5
- title?: string;
6
- }
7
-
8
- function escapeHtml(s: string): string {
9
- return s
10
- .replace(/&/g, "&amp;")
11
- .replace(/</g, "&lt;")
12
- .replace(/>/g, "&gt;")
13
- .replace(/"/g, "&quot;")
14
- .replace(/'/g, "&#39;");
15
- }
16
-
17
- function escapeAttr(s: string): string {
18
- return escapeHtml(s);
19
- }
20
-
21
- export function generateReviewHtml(options: GenerateReviewHtmlOptions): string {
22
- const { metadata, title = "Demo Review" } = options;
23
-
24
- if (metadata.demos.length === 0) {
25
- throw new Error("metadata.demos must not be empty");
26
- }
27
-
28
- const firstDemo = metadata.demos[0];
29
-
30
- const demoButtons = metadata.demos
31
- .map((demo, i) => {
32
- const activeClass = i === 0 ? ' class="active"' : "";
33
- return `<li><button data-index="${i}"${activeClass}>${escapeHtml(demo.file)}</button></li>`;
34
- })
35
- .join("\n ");
36
-
37
- const metadataJson = JSON.stringify(metadata).replace(/<\//g, "<\\/");
38
-
39
- return `<!DOCTYPE html>
40
- <html lang="en">
41
- <head>
42
- <meta charset="UTF-8">
43
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
44
- <title>${escapeHtml(title)}</title>
45
- <style>
46
- * { margin: 0; padding: 0; box-sizing: border-box; }
47
- body { font-family: system-ui, -apple-system, sans-serif; background: #1a1a2e; color: #e0e0e0; min-height: 100vh; }
48
- header { padding: 1rem 2rem; background: #16213e; border-bottom: 1px solid #0f3460; }
49
- header h1 { font-size: 1.4rem; color: #e94560; }
50
- .review-layout { display: flex; height: calc(100vh - 60px); }
51
- .video-panel { flex: 4; padding: 1rem; display: flex; align-items: center; justify-content: center; background: #0f0f23; }
52
- .video-panel video { width: 100%; max-height: 100%; border-radius: 4px; }
53
- .side-panel { flex: 1; min-width: 260px; max-width: 360px; padding: 1rem; overflow-y: auto; background: #16213e; border-left: 1px solid #0f3460; }
54
- .side-panel h2 { font-size: 1rem; margin-bottom: 0.5rem; color: #e94560; }
55
- .side-panel section { margin-bottom: 1.5rem; }
56
- #demo-list { list-style: none; }
57
- #demo-list li { margin-bottom: 0.25rem; }
58
- #demo-list button { width: 100%; text-align: left; padding: 0.4rem 0.6rem; background: #1a1a2e; color: #e0e0e0; border: 1px solid #0f3460; border-radius: 4px; cursor: pointer; font-size: 0.85rem; }
59
- #demo-list button:hover { background: #0f3460; }
60
- #demo-list button.active { background: #e94560; color: #fff; border-color: #e94560; }
61
- #summary-text { font-size: 0.9rem; line-height: 1.5; color: #ccc; }
62
- #annotations-list { list-style: none; }
63
- #annotations-list li { margin-bottom: 0.3rem; }
64
- #annotations-list button { width: 100%; text-align: left; padding: 0.3rem 0.5rem; background: transparent; color: #53a8b6; border: none; cursor: pointer; font-size: 0.85rem; }
65
- #annotations-list button:hover { color: #e94560; text-decoration: underline; }
66
- .timestamp { font-weight: bold; margin-right: 0.4rem; color: #e94560; }
67
- </style>
68
- </head>
69
- <body>
70
- <header>
71
- <h1>${escapeHtml(title)}</h1>
72
- </header>
73
- <main class="review-layout">
74
- <div class="video-panel">
75
- <video id="review-video" controls src="${escapeAttr(firstDemo.file)}"></video>
76
- </div>
77
- <div class="side-panel">
78
- <section>
79
- <h2>Demos</h2>
80
- <ul id="demo-list">
81
- ${demoButtons}
82
- </ul>
83
- </section>
84
- <section>
85
- <h2>Summary</h2>
86
- <p id="summary-text"></p>
87
- </section>
88
- <section>
89
- <h2>Annotations</h2>
90
- <ul id="annotations-list"></ul>
91
- </section>
92
- </div>
93
- </main>
94
- <script>
95
- (function() {
96
- var metadata = ${metadataJson};
97
- var video = document.getElementById("review-video");
98
- var summaryText = document.getElementById("summary-text");
99
- var annotationsList = document.getElementById("annotations-list");
100
- var demoButtons = document.querySelectorAll("#demo-list button");
101
-
102
- function esc(s) {
103
- var d = document.createElement("div");
104
- d.appendChild(document.createTextNode(s));
105
- return d.innerHTML;
106
- }
107
-
108
- function formatTime(seconds) {
109
- var m = Math.floor(seconds / 60);
110
- var s = Math.floor(seconds % 60);
111
- return m + ":" + (s < 10 ? "0" : "") + s;
112
- }
113
-
114
- function selectDemo(index) {
115
- var demo = metadata.demos[index];
116
- video.src = demo.file;
117
- video.load();
118
- summaryText.textContent = demo.summary;
119
-
120
- demoButtons.forEach(function(btn, i) {
121
- btn.classList.toggle("active", i === index);
122
- });
123
-
124
- var html = "";
125
- demo.annotations.forEach(function(ann) {
126
- html += '<li><button data-time="' + ann.timestampSeconds + '">' +
127
- '<span class="timestamp">' + esc(formatTime(ann.timestampSeconds)) + '</span>' +
128
- esc(ann.text) + '</button></li>';
129
- });
130
- annotationsList.innerHTML = html;
131
- }
132
-
133
- demoButtons.forEach(function(btn) {
134
- btn.addEventListener("click", function() {
135
- selectDemo(parseInt(btn.getAttribute("data-index"), 10));
136
- });
137
- });
138
-
139
- annotationsList.addEventListener("click", function(e) {
140
- var btn = e.target.closest("button[data-time]");
141
- if (btn) {
142
- video.currentTime = parseFloat(btn.getAttribute("data-time"));
143
- video.play();
144
- }
145
- });
146
-
147
- selectDemo(0);
148
- })();
149
- </script>
150
- </body>
151
- </html>`;
152
- }