@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,62 @@
1
+ import { readFileSync } from "node:fs";
2
+
3
+ export type ExecFn = (cmd: string[], cwd: string) => Promise<string>;
4
+ export type ReadFileFn = (path: string) => string;
5
+
6
+ export interface RepoContext {
7
+ gitDiff: string;
8
+ guidelines: string[];
9
+ }
10
+
11
+ export interface GetRepoContextOptions {
12
+ exec?: ExecFn;
13
+ readFile?: ReadFileFn;
14
+ }
15
+
16
+ const defaultExec: ExecFn = async (cmd, cwd) => {
17
+ const proc = Bun.spawnSync(cmd, { cwd });
18
+ if (proc.exitCode !== 0) {
19
+ const stderr = proc.stderr.toString().trim();
20
+ throw new Error(`Command failed (exit ${proc.exitCode}): ${cmd.join(" ")}${stderr ? `: ${stderr}` : ""}`);
21
+ }
22
+ return proc.stdout.toString();
23
+ };
24
+
25
+ const defaultReadFile: ReadFileFn = (path) => {
26
+ return readFileSync(path, "utf-8");
27
+ };
28
+
29
+ export async function getRepoContext(
30
+ demosDir: string,
31
+ options?: GetRepoContextOptions,
32
+ ): Promise<RepoContext> {
33
+ const exec = options?.exec ?? defaultExec;
34
+ const readFile = options?.readFile ?? defaultReadFile;
35
+
36
+ const gitRoot = (await exec(["git", "rev-parse", "--show-toplevel"], demosDir)).trim();
37
+
38
+ let gitDiff: string;
39
+ const workingDiff = (await exec(["git", "diff", "HEAD"], gitRoot)).trim();
40
+ if (workingDiff.length > 0) {
41
+ gitDiff = workingDiff;
42
+ } else {
43
+ gitDiff = (await exec(["git", "diff", "HEAD~1..HEAD"], gitRoot)).trim();
44
+ }
45
+
46
+ const lsOutput = (await exec(["git", "ls-files"], gitRoot)).trim();
47
+ const files = lsOutput.split("\n").filter((f) => f.length > 0);
48
+
49
+ const guidelinePatterns = ["CLAUDE.md", "SKILL.md"];
50
+ const guidelines: string[] = [];
51
+
52
+ for (const file of files) {
53
+ const basename = file.split("/").pop() ?? "";
54
+ if (guidelinePatterns.includes(basename)) {
55
+ const fullPath = `${gitRoot}/${file}`;
56
+ const content = readFile(fullPath);
57
+ guidelines.push(`# ${file}\n${content}`);
58
+ }
59
+ }
60
+
61
+ return { gitDiff, guidelines };
62
+ }
@@ -0,0 +1,349 @@
1
+ import { test, expect } from "@playwright/test";
2
+
3
+ import type { CodeReview, ReviewMetadata } 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
+ { severity: "major", description: "Memory leak in handler" },
14
+ { severity: "minor", description: "Missing edge case test" },
15
+ { severity: "nit", description: "Rename variable for clarity" },
16
+ ],
17
+ ...overrides,
18
+ };
19
+ }
20
+
21
+ function makeMetadata(overrides?: Partial<ReviewMetadata>): ReviewMetadata {
22
+ return {
23
+ demos: [
24
+ {
25
+ file: "login-flow.webm",
26
+ summary: "Shows the login flow end to end",
27
+ steps: [
28
+ { timestampSeconds: 0, text: "Page loads" },
29
+ { timestampSeconds: 5, text: "User types credentials" },
30
+ ],
31
+ },
32
+ ],
33
+ review: makeReview(),
34
+ ...overrides,
35
+ };
36
+ }
37
+
38
+ function generatePage(overrides?: Partial<ReviewMetadata>): string {
39
+ return generateReviewHtml({ metadata: makeMetadata(overrides) });
40
+ }
41
+
42
+ test.describe("feedback tab e2e", () => {
43
+ test.describe("tab navigation", () => {
44
+ test("clicking Feedback tab shows feedback panel and hides others", async ({
45
+ page,
46
+ }) => {
47
+ await page.setContent(generatePage());
48
+
49
+ await page.click('[data-tab="feedback"]');
50
+
51
+ await expect(page.locator("#tab-feedback")).toBeVisible();
52
+ await expect(page.locator("#tab-summary")).not.toBeVisible();
53
+ await expect(page.locator("#tab-demos")).not.toBeVisible();
54
+ });
55
+
56
+ test("feedback panel is not visible by default", async ({ page }) => {
57
+ await page.setContent(generatePage());
58
+
59
+ await expect(page.locator("#tab-feedback")).not.toBeVisible();
60
+ await expect(page.locator("#tab-summary")).toBeVisible();
61
+ });
62
+ });
63
+
64
+ test.describe("issue + buttons", () => {
65
+ test("clicking + button adds issue to feedback list", async ({ page }) => {
66
+ await page.setContent(generatePage());
67
+
68
+ await page.click('[data-tab="feedback"]');
69
+ await expect(page.locator("#feedback-list li")).toHaveCount(0);
70
+
71
+ await page.click('[data-tab="summary"]');
72
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
73
+
74
+ await page.click('[data-tab="feedback"]');
75
+ await expect(page.locator("#feedback-list li")).toHaveCount(1);
76
+ await expect(page.locator("#feedback-list li span").first()).toHaveText(
77
+ "Memory leak in handler",
78
+ );
79
+ });
80
+
81
+ test("clicking multiple + buttons adds multiple items", async ({
82
+ page,
83
+ }) => {
84
+ await page.setContent(generatePage());
85
+
86
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
87
+ await page.click('.feedback-add-issue[data-issue="Missing edge case test"]');
88
+ await page.click('.feedback-add-issue[data-issue="Rename variable for clarity"]');
89
+
90
+ await page.click('[data-tab="feedback"]');
91
+ await expect(page.locator("#feedback-list li")).toHaveCount(3);
92
+ });
93
+
94
+ test("clicking same + button twice does not duplicate", async ({
95
+ page,
96
+ }) => {
97
+ await page.setContent(generatePage());
98
+
99
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
100
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
101
+
102
+ await page.click('[data-tab="feedback"]');
103
+ await expect(page.locator("#feedback-list li")).toHaveCount(1);
104
+ });
105
+ });
106
+
107
+ test.describe("feedback preview", () => {
108
+ test("preview updates when items are added", async ({ page }) => {
109
+ await page.setContent(generatePage());
110
+
111
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
112
+ await page.click('[data-tab="feedback"]');
113
+
114
+ await expect(page.locator("#feedback-preview")).toContainText(
115
+ "1. Address: Memory leak in handler",
116
+ );
117
+ });
118
+
119
+ test("preview shows numbered list for multiple items", async ({
120
+ page,
121
+ }) => {
122
+ await page.setContent(generatePage());
123
+
124
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
125
+ await page.click('.feedback-add-issue[data-issue="Missing edge case test"]');
126
+ await page.click('[data-tab="feedback"]');
127
+
128
+ const preview = page.locator("#feedback-preview");
129
+ await expect(preview).toContainText("1. Address: Memory leak in handler");
130
+ await expect(preview).toContainText(
131
+ "2. Address: Missing edge case test",
132
+ );
133
+ });
134
+
135
+ test("preview includes general feedback from textarea", async ({
136
+ page,
137
+ }) => {
138
+ await page.setContent(generatePage());
139
+
140
+ await page.click('[data-tab="feedback"]');
141
+ await page.fill("#feedback-general", "Overall good work, minor fixes needed");
142
+
143
+ await expect(page.locator("#feedback-preview")).toContainText(
144
+ "General feedback:",
145
+ );
146
+ await expect(page.locator("#feedback-preview")).toContainText(
147
+ "Overall good work, minor fixes needed",
148
+ );
149
+ });
150
+
151
+ test("preview combines items and general feedback", async ({ page }) => {
152
+ await page.setContent(generatePage());
153
+
154
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
155
+ await page.click('[data-tab="feedback"]');
156
+ await page.fill("#feedback-general", "Please address ASAP");
157
+
158
+ const preview = page.locator("#feedback-preview");
159
+ await expect(preview).toContainText("1. Address: Memory leak in handler");
160
+ await expect(preview).toContainText("General feedback:");
161
+ await expect(preview).toContainText("Please address ASAP");
162
+ });
163
+ });
164
+
165
+ test.describe("remove feedback items", () => {
166
+ test("clicking X removes item from list and preview", async ({ page }) => {
167
+ await page.setContent(generatePage());
168
+
169
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
170
+ await page.click('.feedback-add-issue[data-issue="Missing edge case test"]');
171
+ await page.click('[data-tab="feedback"]');
172
+
173
+ await expect(page.locator("#feedback-list li")).toHaveCount(2);
174
+
175
+ await page.click("#feedback-list .feedback-remove >> nth=0");
176
+
177
+ await expect(page.locator("#feedback-list li")).toHaveCount(1);
178
+ await expect(page.locator("#feedback-list li span").first()).toHaveText(
179
+ "Missing edge case test",
180
+ );
181
+ await expect(page.locator("#feedback-preview")).not.toContainText(
182
+ "Memory leak in handler",
183
+ );
184
+ });
185
+ });
186
+
187
+ test.describe("copy button", () => {
188
+ test("copy button changes text to Copied! on click", async ({ page }) => {
189
+ await page.setContent(generatePage());
190
+
191
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
192
+ await page.click('[data-tab="feedback"]');
193
+
194
+ // Grant clipboard permissions
195
+ await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
196
+
197
+ await page.click("#feedback-copy");
198
+ await expect(page.locator("#feedback-copy")).toHaveText("Copied!");
199
+ });
200
+
201
+ test("copy button reverts to original text after delay", async ({
202
+ page,
203
+ }) => {
204
+ await page.setContent(generatePage());
205
+
206
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
207
+ await page.click('[data-tab="feedback"]');
208
+
209
+ await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
210
+
211
+ await page.click("#feedback-copy");
212
+ await expect(page.locator("#feedback-copy")).toHaveText("Copied!");
213
+
214
+ await expect(page.locator("#feedback-copy")).toHaveText(
215
+ "Copy to clipboard",
216
+ { timeout: 3000 },
217
+ );
218
+ });
219
+ });
220
+
221
+ test.describe("text selection floating button", () => {
222
+ test("floating button appears when selecting text in summary tab", async ({
223
+ page,
224
+ }) => {
225
+ await page.setContent(generatePage());
226
+
227
+ await expect(page.locator("#feedback-selection-btn")).not.toBeVisible();
228
+
229
+ // Select text within the summary tab review body
230
+ const summaryText = page.locator(".review-body p").first();
231
+ await summaryText.evaluate((el) => {
232
+ const range = document.createRange();
233
+ range.selectNodeContents(el);
234
+ const sel = window.getSelection()!;
235
+ sel.removeAllRanges();
236
+ sel.addRange(range);
237
+ });
238
+
239
+ // Trigger mouseup to activate the button
240
+ await summaryText.dispatchEvent("mouseup", { bubbles: true });
241
+
242
+ await expect(page.locator("#feedback-selection-btn")).toBeVisible({
243
+ timeout: 2000,
244
+ });
245
+ });
246
+
247
+ test("clicking floating button adds selected text to feedback", async ({
248
+ page,
249
+ }) => {
250
+ await page.setContent(generatePage());
251
+
252
+ // Select the summary text
253
+ const summaryText = page.locator(".review-body p").first();
254
+ await summaryText.evaluate((el) => {
255
+ const range = document.createRange();
256
+ range.selectNodeContents(el);
257
+ const sel = window.getSelection()!;
258
+ sel.removeAllRanges();
259
+ sel.addRange(range);
260
+ });
261
+ await summaryText.dispatchEvent("mouseup", { bubbles: true });
262
+
263
+ await expect(page.locator("#feedback-selection-btn")).toBeVisible({
264
+ timeout: 2000,
265
+ });
266
+
267
+ // Click via JS since synthetic mouseup positions the button off-viewport
268
+ await page.locator("#feedback-selection-btn").evaluate((el: HTMLElement) => el.click());
269
+
270
+ await expect(page.locator("#feedback-selection-btn")).not.toBeVisible();
271
+
272
+ await page.click('[data-tab="feedback"]');
273
+ await expect(page.locator("#feedback-list li")).toHaveCount(1);
274
+ await expect(page.locator("#feedback-list li span").first()).toHaveText(
275
+ "Good changes overall",
276
+ );
277
+ });
278
+
279
+ test("floating button hides when clicking elsewhere", async ({ page }) => {
280
+ await page.setContent(generatePage());
281
+
282
+ const summaryText = page.locator(".review-body p").first();
283
+ await summaryText.evaluate((el) => {
284
+ const range = document.createRange();
285
+ range.selectNodeContents(el);
286
+ const sel = window.getSelection()!;
287
+ sel.removeAllRanges();
288
+ sel.addRange(range);
289
+ });
290
+ await summaryText.dispatchEvent("mouseup", { bubbles: true });
291
+
292
+ await expect(page.locator("#feedback-selection-btn")).toBeVisible({
293
+ timeout: 2000,
294
+ });
295
+
296
+ // Click elsewhere
297
+ await page.click("header");
298
+
299
+ await expect(page.locator("#feedback-selection-btn")).not.toBeVisible();
300
+ });
301
+ });
302
+
303
+ test.describe("no review", () => {
304
+ test("no feedback elements when review is absent", async ({ page }) => {
305
+ await page.setContent(generatePage({ review: undefined }));
306
+
307
+ await expect(page.locator('[data-tab="feedback"]')).toHaveCount(0);
308
+ await expect(page.locator("#tab-feedback")).toHaveCount(0);
309
+ await expect(page.locator("#feedback-selection-btn")).toHaveCount(0);
310
+ await expect(page.locator(".feedback-add-issue")).toHaveCount(0);
311
+ });
312
+ });
313
+
314
+ test.describe("feedback persists across tab switches", () => {
315
+ test("feedback items survive switching tabs", async ({ page }) => {
316
+ await page.setContent(generatePage());
317
+
318
+ await page.click('.feedback-add-issue[data-issue="Memory leak in handler"]');
319
+
320
+ await page.click('[data-tab="feedback"]');
321
+ await expect(page.locator("#feedback-list li")).toHaveCount(1);
322
+
323
+ // Switch to demos and back
324
+ await page.click('[data-tab="demos"]');
325
+ await page.click('[data-tab="feedback"]');
326
+
327
+ await expect(page.locator("#feedback-list li")).toHaveCount(1);
328
+ await expect(page.locator("#feedback-list li span").first()).toHaveText(
329
+ "Memory leak in handler",
330
+ );
331
+ });
332
+
333
+ test("general feedback text persists across tab switches", async ({
334
+ page,
335
+ }) => {
336
+ await page.setContent(generatePage());
337
+
338
+ await page.click('[data-tab="feedback"]');
339
+ await page.fill("#feedback-general", "Some general notes");
340
+
341
+ await page.click('[data-tab="demos"]');
342
+ await page.click('[data-tab="feedback"]');
343
+
344
+ await expect(page.locator("#feedback-general")).toHaveValue(
345
+ "Some general notes",
346
+ );
347
+ });
348
+ });
349
+ });