@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.
- package/dist/bin/demon-demo-init.js +56 -0
- package/dist/bin/demon-demo-init.js.map +10 -0
- package/dist/bin/demon-demo-review.js +331 -185
- package/dist/bin/demon-demo-review.js.map +7 -6
- package/dist/bin/demoon.js +1445 -0
- package/dist/bin/demoon.js.map +22 -0
- package/dist/bin/review-template.html +62 -0
- package/dist/github-issue.js +749 -0
- package/dist/github-issue.js.map +16 -0
- package/dist/index.js +1537 -236
- package/dist/index.js.map +16 -5
- package/dist/orchestrator.js +1421 -0
- package/dist/orchestrator.js.map +20 -0
- package/dist/review-generator.js +424 -0
- package/dist/review-generator.js.map +12 -0
- package/dist/review-template.html +62 -0
- package/package.json +11 -2
- package/src/bin/demon-demo-init.ts +59 -0
- package/src/bin/demon-demo-review.ts +19 -32
- package/src/bin/demoon.ts +140 -0
- package/src/feedback-server.ts +138 -0
- package/src/git-context.test.ts +156 -0
- package/src/git-context.ts +101 -0
- package/src/github-issue.test.ts +188 -0
- package/src/github-issue.ts +139 -0
- package/src/html-generator.e2e.test.ts +630 -0
- package/src/index.ts +17 -5
- package/src/orchestrator.test.ts +183 -0
- package/src/orchestrator.ts +341 -0
- package/src/recorder.test.ts +161 -0
- package/src/recorder.ts +74 -0
- package/src/review-generator.ts +221 -0
- package/src/review-types.ts +22 -1
- package/src/review.test.ts +257 -59
- package/src/review.ts +160 -38
- package/src/html-generator.test.ts +0 -195
- package/src/html-generator.ts +0 -152
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
import { test, expect, type Page } from "@playwright/test";
|
|
2
|
+
import { readFileSync, writeFileSync, mkdtempSync } from "node:fs";
|
|
3
|
+
import { join, dirname } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import type { CodeReview, ReviewMetadata } from "./review-types.ts";
|
|
8
|
+
|
|
9
|
+
interface ReviewAppData {
|
|
10
|
+
metadata: ReviewMetadata;
|
|
11
|
+
title: string;
|
|
12
|
+
videos: Record<string, string>;
|
|
13
|
+
logs?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
17
|
+
const currentDir = dirname(currentFile);
|
|
18
|
+
const templatePath = join(currentDir, "..", "dist", "review-template.html");
|
|
19
|
+
const tempDir = mkdtempSync(join(tmpdir(), "review-test-"));
|
|
20
|
+
|
|
21
|
+
function getTemplate(): string {
|
|
22
|
+
return readFileSync(templatePath, "utf-8");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeReview(overrides?: Partial<CodeReview>): CodeReview {
|
|
26
|
+
return {
|
|
27
|
+
summary: "Good changes overall",
|
|
28
|
+
highlights: ["Clean implementation", "Good test coverage"],
|
|
29
|
+
verdict: "approve",
|
|
30
|
+
verdictReason: "No major issues found",
|
|
31
|
+
issues: [
|
|
32
|
+
{ severity: "major", description: "Memory leak in handler" },
|
|
33
|
+
{ severity: "minor", description: "Missing edge case test" },
|
|
34
|
+
{ severity: "nit", description: "Rename variable for clarity" },
|
|
35
|
+
],
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function makeMetadata(overrides?: Partial<ReviewMetadata>): ReviewMetadata {
|
|
41
|
+
return {
|
|
42
|
+
demos: [
|
|
43
|
+
{
|
|
44
|
+
file: "login-flow.webm",
|
|
45
|
+
type: "web-ux",
|
|
46
|
+
summary: "Shows the login flow end to end",
|
|
47
|
+
steps: [
|
|
48
|
+
{ timestampSeconds: 0, text: "Page loads" },
|
|
49
|
+
{ timestampSeconds: 5, text: "User types credentials" },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
review: makeReview(),
|
|
54
|
+
...overrides,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface GeneratePageOptions {
|
|
59
|
+
metadataOverrides?: Partial<ReviewMetadata>;
|
|
60
|
+
logs?: Record<string, string>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function generatePage(options: GeneratePageOptions | Partial<ReviewMetadata> = {}): string {
|
|
64
|
+
// Support legacy signature (just metadata overrides)
|
|
65
|
+
const isLegacy = !('metadataOverrides' in options) && !('logs' in options);
|
|
66
|
+
const metadataOverrides = isLegacy ? options as Partial<ReviewMetadata> : (options as GeneratePageOptions).metadataOverrides;
|
|
67
|
+
const logs = isLegacy ? undefined : (options as GeneratePageOptions).logs;
|
|
68
|
+
|
|
69
|
+
const appData: ReviewAppData = {
|
|
70
|
+
metadata: makeMetadata(metadataOverrides),
|
|
71
|
+
title: "Demo Review",
|
|
72
|
+
videos: {},
|
|
73
|
+
logs,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const template = getTemplate();
|
|
77
|
+
const html = template.replace('"{{__INJECT_REVIEW_DATA__}}"', JSON.stringify(appData));
|
|
78
|
+
|
|
79
|
+
// Write to temp file and return file URL
|
|
80
|
+
const tempFile = join(tempDir, `test-${Date.now()}.html`);
|
|
81
|
+
writeFileSync(tempFile, html);
|
|
82
|
+
return `file://${tempFile}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Helper to click tab via JavaScript (Vuetify tabs need JS click for proper event handling)
|
|
86
|
+
async function clickTab(page: Page, tabId: string) {
|
|
87
|
+
await page.locator(`[data-tab="${tabId}"]`).evaluate(el => (el as HTMLElement).click());
|
|
88
|
+
await page.waitForTimeout(100);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
test.describe("feedback tab e2e", () => {
|
|
92
|
+
test.describe("tab navigation", () => {
|
|
93
|
+
test("clicking Feedback tab shows feedback panel and hides others", async ({
|
|
94
|
+
page,
|
|
95
|
+
}) => {
|
|
96
|
+
await page.goto(generatePage());
|
|
97
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
98
|
+
|
|
99
|
+
await clickTab(page, "feedback");
|
|
100
|
+
|
|
101
|
+
await expect(page.locator("#tab-feedback")).toBeVisible();
|
|
102
|
+
await expect(page.locator("#tab-summary")).not.toBeVisible();
|
|
103
|
+
await expect(page.locator("#tab-demos")).not.toBeVisible();
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("feedback panel is not visible by default", async ({ page }) => {
|
|
107
|
+
await page.goto(generatePage());
|
|
108
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
109
|
+
|
|
110
|
+
await expect(page.locator("#tab-feedback")).not.toBeVisible();
|
|
111
|
+
await expect(page.locator("#tab-summary")).toBeVisible();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test.describe("issue + buttons", () => {
|
|
116
|
+
test("clicking + button adds issue to feedback list", async ({ page }) => {
|
|
117
|
+
await page.goto(generatePage());
|
|
118
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
119
|
+
|
|
120
|
+
await clickTab(page, "feedback");
|
|
121
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(0);
|
|
122
|
+
|
|
123
|
+
await clickTab(page, "summary");
|
|
124
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
125
|
+
|
|
126
|
+
await clickTab(page, "feedback");
|
|
127
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(1);
|
|
128
|
+
await expect(page.locator('[data-testid="feedback-item"]').first()).toContainText(
|
|
129
|
+
"Memory leak in handler",
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("clicking multiple + buttons adds multiple items", async ({
|
|
134
|
+
page,
|
|
135
|
+
}) => {
|
|
136
|
+
await page.goto(generatePage());
|
|
137
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
138
|
+
|
|
139
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
140
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Missing edge case test"]');
|
|
141
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Rename variable for clarity"]');
|
|
142
|
+
|
|
143
|
+
await clickTab(page, "feedback");
|
|
144
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(3);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("clicking same + button twice does not duplicate", async ({
|
|
148
|
+
page,
|
|
149
|
+
}) => {
|
|
150
|
+
await page.goto(generatePage());
|
|
151
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
152
|
+
|
|
153
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
154
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
155
|
+
|
|
156
|
+
await clickTab(page, "feedback");
|
|
157
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(1);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test.describe("feedback preview", () => {
|
|
162
|
+
test("preview updates when items are added", async ({ page }) => {
|
|
163
|
+
await page.goto(generatePage());
|
|
164
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
165
|
+
|
|
166
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
167
|
+
await clickTab(page, "feedback");
|
|
168
|
+
|
|
169
|
+
await expect(page.locator("#feedback-preview")).toContainText(
|
|
170
|
+
"1. Address: Memory leak in handler",
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("preview shows numbered list for multiple items", async ({
|
|
175
|
+
page,
|
|
176
|
+
}) => {
|
|
177
|
+
await page.goto(generatePage());
|
|
178
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
179
|
+
|
|
180
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
181
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Missing edge case test"]');
|
|
182
|
+
await clickTab(page, "feedback");
|
|
183
|
+
|
|
184
|
+
const preview = page.locator("#feedback-preview");
|
|
185
|
+
await expect(preview).toContainText("1. Address: Memory leak in handler");
|
|
186
|
+
await expect(preview).toContainText(
|
|
187
|
+
"2. Address: Missing edge case test",
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("preview includes general feedback from textarea", async ({
|
|
192
|
+
page,
|
|
193
|
+
}) => {
|
|
194
|
+
await page.goto(generatePage());
|
|
195
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
196
|
+
|
|
197
|
+
await clickTab(page, "feedback");
|
|
198
|
+
await page.locator('[data-testid="feedback-general"] textarea:not([readonly])').fill("Overall good work, minor fixes needed");
|
|
199
|
+
|
|
200
|
+
await expect(page.locator("#feedback-preview")).toContainText(
|
|
201
|
+
"General feedback:",
|
|
202
|
+
);
|
|
203
|
+
await expect(page.locator("#feedback-preview")).toContainText(
|
|
204
|
+
"Overall good work, minor fixes needed",
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("preview combines items and general feedback", async ({ page }) => {
|
|
209
|
+
await page.goto(generatePage());
|
|
210
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
211
|
+
|
|
212
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
213
|
+
await clickTab(page, "feedback");
|
|
214
|
+
await page.locator('[data-testid="feedback-general"] textarea:not([readonly])').fill("Please address ASAP");
|
|
215
|
+
|
|
216
|
+
const preview = page.locator("#feedback-preview");
|
|
217
|
+
await expect(preview).toContainText("1. Address: Memory leak in handler");
|
|
218
|
+
await expect(preview).toContainText("General feedback:");
|
|
219
|
+
await expect(preview).toContainText("Please address ASAP");
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test.describe("remove feedback items", () => {
|
|
224
|
+
test("clicking X removes item from list and preview", async ({ page }) => {
|
|
225
|
+
await page.goto(generatePage());
|
|
226
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
227
|
+
|
|
228
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
229
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Missing edge case test"]');
|
|
230
|
+
await clickTab(page, "feedback");
|
|
231
|
+
|
|
232
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(2);
|
|
233
|
+
|
|
234
|
+
await page.locator('[data-testid="feedback-remove"]').first().click();
|
|
235
|
+
|
|
236
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(1);
|
|
237
|
+
await expect(page.locator('[data-testid="feedback-item"]').first()).toContainText(
|
|
238
|
+
"Missing edge case test",
|
|
239
|
+
);
|
|
240
|
+
await expect(page.locator("#feedback-preview")).not.toContainText(
|
|
241
|
+
"Memory leak in handler",
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test.describe("copy button", () => {
|
|
247
|
+
test("copy button changes text to Copied! on click", async ({ page }) => {
|
|
248
|
+
await page.goto(generatePage());
|
|
249
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
250
|
+
|
|
251
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
252
|
+
await clickTab(page, "feedback");
|
|
253
|
+
|
|
254
|
+
// Grant clipboard permissions
|
|
255
|
+
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
256
|
+
|
|
257
|
+
await page.click("#feedback-copy");
|
|
258
|
+
await expect(page.locator("#feedback-copy")).toContainText("Copied!");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("copy button reverts to original text after delay", async ({
|
|
262
|
+
page,
|
|
263
|
+
}) => {
|
|
264
|
+
await page.goto(generatePage());
|
|
265
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
266
|
+
|
|
267
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
268
|
+
await clickTab(page, "feedback");
|
|
269
|
+
|
|
270
|
+
await page.context().grantPermissions(["clipboard-read", "clipboard-write"]);
|
|
271
|
+
|
|
272
|
+
await page.click("#feedback-copy");
|
|
273
|
+
await expect(page.locator("#feedback-copy")).toContainText("Copied!");
|
|
274
|
+
|
|
275
|
+
await expect(page.locator("#feedback-copy")).toContainText(
|
|
276
|
+
"Copy to clipboard",
|
|
277
|
+
{ timeout: 3000 },
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test.describe("text selection floating button", () => {
|
|
283
|
+
test("floating button appears when selecting text in summary tab", async ({
|
|
284
|
+
page,
|
|
285
|
+
}) => {
|
|
286
|
+
await page.goto(generatePage());
|
|
287
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
288
|
+
|
|
289
|
+
await expect(page.locator("#feedback-selection-btn")).not.toBeVisible();
|
|
290
|
+
|
|
291
|
+
// Select text within the summary tab review body
|
|
292
|
+
const summaryText = page.locator(".review-body p").first();
|
|
293
|
+
await summaryText.evaluate((el) => {
|
|
294
|
+
const range = document.createRange();
|
|
295
|
+
range.selectNodeContents(el);
|
|
296
|
+
const sel = window.getSelection()!;
|
|
297
|
+
sel.removeAllRanges();
|
|
298
|
+
sel.addRange(range);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// Trigger mouseup to activate the button
|
|
302
|
+
await summaryText.dispatchEvent("mouseup", { bubbles: true });
|
|
303
|
+
|
|
304
|
+
await expect(page.locator("#feedback-selection-btn")).toBeVisible({
|
|
305
|
+
timeout: 2000,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
test("clicking floating button adds selected text to feedback", async ({
|
|
310
|
+
page,
|
|
311
|
+
}) => {
|
|
312
|
+
await page.goto(generatePage());
|
|
313
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
314
|
+
|
|
315
|
+
// Select the summary text
|
|
316
|
+
const summaryText = page.locator(".review-body p").first();
|
|
317
|
+
await summaryText.evaluate((el) => {
|
|
318
|
+
const range = document.createRange();
|
|
319
|
+
range.selectNodeContents(el);
|
|
320
|
+
const sel = window.getSelection()!;
|
|
321
|
+
sel.removeAllRanges();
|
|
322
|
+
sel.addRange(range);
|
|
323
|
+
});
|
|
324
|
+
await summaryText.dispatchEvent("mouseup", { bubbles: true });
|
|
325
|
+
|
|
326
|
+
await expect(page.locator("#feedback-selection-btn")).toBeVisible({
|
|
327
|
+
timeout: 2000,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Click via JS since synthetic mouseup positions the button off-viewport
|
|
331
|
+
await page.locator("#feedback-selection-btn").evaluate((el: HTMLElement) => el.click());
|
|
332
|
+
|
|
333
|
+
await expect(page.locator("#feedback-selection-btn")).not.toBeVisible();
|
|
334
|
+
|
|
335
|
+
await clickTab(page, "feedback");
|
|
336
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(1);
|
|
337
|
+
await expect(page.locator('[data-testid="feedback-item"]').first()).toContainText(
|
|
338
|
+
"Good changes overall",
|
|
339
|
+
);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("floating button hides when clicking elsewhere", async ({ page }) => {
|
|
343
|
+
await page.goto(generatePage());
|
|
344
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
345
|
+
|
|
346
|
+
const summaryText = page.locator(".review-body p").first();
|
|
347
|
+
await summaryText.evaluate((el) => {
|
|
348
|
+
const range = document.createRange();
|
|
349
|
+
range.selectNodeContents(el);
|
|
350
|
+
const sel = window.getSelection()!;
|
|
351
|
+
sel.removeAllRanges();
|
|
352
|
+
sel.addRange(range);
|
|
353
|
+
});
|
|
354
|
+
await summaryText.dispatchEvent("mouseup", { bubbles: true });
|
|
355
|
+
|
|
356
|
+
await expect(page.locator("#feedback-selection-btn")).toBeVisible({
|
|
357
|
+
timeout: 2000,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Click elsewhere
|
|
361
|
+
await page.locator('[data-testid="review-header"]').click({ force: true });
|
|
362
|
+
|
|
363
|
+
await expect(page.locator("#feedback-selection-btn")).not.toBeVisible();
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test.describe("no review", () => {
|
|
368
|
+
test("no feedback elements when review is absent", async ({ page }) => {
|
|
369
|
+
await page.goto(generatePage({ review: undefined }));
|
|
370
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
371
|
+
|
|
372
|
+
await expect(page.locator('[data-tab="feedback"]')).toHaveCount(0);
|
|
373
|
+
await expect(page.locator("#tab-feedback")).toHaveCount(0);
|
|
374
|
+
await expect(page.locator("#feedback-selection-btn")).toHaveCount(0);
|
|
375
|
+
await expect(page.locator('[data-testid="issue-add-feedback"]')).toHaveCount(0);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
test.describe("feedback persists across tab switches", () => {
|
|
380
|
+
test("feedback items survive switching tabs", async ({ page }) => {
|
|
381
|
+
await page.goto(generatePage());
|
|
382
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
383
|
+
|
|
384
|
+
await page.click('[data-testid="issue-add-feedback"][data-issue="Memory leak in handler"]');
|
|
385
|
+
|
|
386
|
+
await clickTab(page, "feedback");
|
|
387
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(1);
|
|
388
|
+
|
|
389
|
+
// Switch to demos and back
|
|
390
|
+
await clickTab(page, "demos");
|
|
391
|
+
await clickTab(page, "feedback");
|
|
392
|
+
|
|
393
|
+
await expect(page.locator('[data-testid="feedback-item"]')).toHaveCount(1);
|
|
394
|
+
await expect(page.locator('[data-testid="feedback-item"]').first()).toContainText(
|
|
395
|
+
"Memory leak in handler",
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("general feedback text persists across tab switches", async ({
|
|
400
|
+
page,
|
|
401
|
+
}) => {
|
|
402
|
+
await page.goto(generatePage());
|
|
403
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
404
|
+
|
|
405
|
+
await clickTab(page, "feedback");
|
|
406
|
+
await page.locator('[data-testid="feedback-general"] textarea:not([readonly])').fill("Some general notes");
|
|
407
|
+
|
|
408
|
+
await clickTab(page, "demos");
|
|
409
|
+
await clickTab(page, "feedback");
|
|
410
|
+
|
|
411
|
+
await expect(page.locator('[data-testid="feedback-general"] textarea:not([readonly])')).toHaveValue(
|
|
412
|
+
"Some general notes",
|
|
413
|
+
);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test.describe("log-based demos e2e", () => {
|
|
419
|
+
const mockLogContent = [
|
|
420
|
+
'{"timestamp":"2024-01-15T10:30:00.123Z","level":"info","message":"Starting migration..."}',
|
|
421
|
+
'{"timestamp":"2024-01-15T10:30:01.001Z","level":"info","message":"Applied migration 001","demon__highlight":true}',
|
|
422
|
+
'{"timestamp":"2024-01-15T10:30:02.001Z","level":"warn","message":"Skipping migration 002"}',
|
|
423
|
+
'{"timestamp":"2024-01-15T10:30:03.001Z","level":"error","message":"Migration failed"}',
|
|
424
|
+
'{"timestamp":"2024-01-15T10:30:04.001Z","level":"info","message":"Complete","demon__highlight":"This is the commentary text"}',
|
|
425
|
+
].join("\n");
|
|
426
|
+
|
|
427
|
+
function generateLogPage() {
|
|
428
|
+
return generatePage({
|
|
429
|
+
metadataOverrides: {
|
|
430
|
+
demos: [
|
|
431
|
+
{
|
|
432
|
+
file: "db-migration.jsonl",
|
|
433
|
+
type: "log-based",
|
|
434
|
+
summary: "Database migration script execution",
|
|
435
|
+
steps: [],
|
|
436
|
+
},
|
|
437
|
+
],
|
|
438
|
+
},
|
|
439
|
+
logs: {
|
|
440
|
+
"db-migration.jsonl": mockLogContent,
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function generateMixedPage() {
|
|
446
|
+
return generatePage({
|
|
447
|
+
metadataOverrides: {
|
|
448
|
+
demos: [
|
|
449
|
+
{
|
|
450
|
+
file: "login-flow.webm",
|
|
451
|
+
type: "web-ux",
|
|
452
|
+
summary: "Shows the login flow end to end",
|
|
453
|
+
steps: [
|
|
454
|
+
{ timestampSeconds: 0, text: "Page loads" },
|
|
455
|
+
{ timestampSeconds: 5, text: "User types credentials" },
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
file: "db-migration.jsonl",
|
|
460
|
+
type: "log-based",
|
|
461
|
+
summary: "Database migration script execution",
|
|
462
|
+
steps: [],
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
},
|
|
466
|
+
logs: {
|
|
467
|
+
"db-migration.jsonl": mockLogContent,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
test.describe("log viewer rendering", () => {
|
|
473
|
+
test("displays log viewer for log-based demo", async ({ page }) => {
|
|
474
|
+
await page.goto(generateLogPage());
|
|
475
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
476
|
+
|
|
477
|
+
await clickTab(page, "demos");
|
|
478
|
+
|
|
479
|
+
await expect(page.locator('[data-testid="log-viewer"]')).toBeVisible();
|
|
480
|
+
await expect(page.locator('[data-testid="video-player"]')).not.toBeVisible();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("displays log lines with line numbers", async ({ page }) => {
|
|
484
|
+
await page.goto(generateLogPage());
|
|
485
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
486
|
+
|
|
487
|
+
await clickTab(page, "demos");
|
|
488
|
+
|
|
489
|
+
const logViewer = page.locator('[data-testid="log-viewer"]');
|
|
490
|
+
await expect(logViewer).toBeVisible();
|
|
491
|
+
|
|
492
|
+
// Check that log lines are present
|
|
493
|
+
const logLines = logViewer.locator(".log-line");
|
|
494
|
+
await expect(logLines).toHaveCount(5);
|
|
495
|
+
|
|
496
|
+
// Check line numbers
|
|
497
|
+
await expect(logLines.nth(0).locator(".line-number")).toContainText("1");
|
|
498
|
+
await expect(logLines.nth(4).locator(".line-number")).toContainText("5");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test("displays log messages", async ({ page }) => {
|
|
502
|
+
await page.goto(generateLogPage());
|
|
503
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
504
|
+
|
|
505
|
+
await clickTab(page, "demos");
|
|
506
|
+
|
|
507
|
+
const logViewer = page.locator('[data-testid="log-viewer"]');
|
|
508
|
+
await expect(logViewer).toContainText("Starting migration...");
|
|
509
|
+
await expect(logViewer).toContainText("Applied migration 001");
|
|
510
|
+
await expect(logViewer).toContainText("Migration failed");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("displays level chips with correct colors", async ({ page }) => {
|
|
514
|
+
await page.goto(generateLogPage());
|
|
515
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
516
|
+
|
|
517
|
+
await clickTab(page, "demos");
|
|
518
|
+
|
|
519
|
+
const logViewer = page.locator('[data-testid="log-viewer"]');
|
|
520
|
+
|
|
521
|
+
// Check for level chips
|
|
522
|
+
await expect(logViewer.locator(".level-chip").filter({ hasText: "INFO" })).toHaveCount(3);
|
|
523
|
+
await expect(logViewer.locator(".level-chip").filter({ hasText: "WARN" })).toHaveCount(1);
|
|
524
|
+
await expect(logViewer.locator(".level-chip").filter({ hasText: "ERROR" })).toHaveCount(1);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("highlights lines with demon__highlight: true", async ({ page }) => {
|
|
528
|
+
await page.goto(generateLogPage());
|
|
529
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
530
|
+
|
|
531
|
+
await clickTab(page, "demos");
|
|
532
|
+
|
|
533
|
+
const logViewer = page.locator('[data-testid="log-viewer"]');
|
|
534
|
+
const highlightedLines = logViewer.locator(".log-line--highlighted");
|
|
535
|
+
|
|
536
|
+
// Two lines have demon__highlight
|
|
537
|
+
await expect(highlightedLines).toHaveCount(2);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
test("displays inline commentary for string highlights", async ({ page }) => {
|
|
541
|
+
await page.goto(generateLogPage());
|
|
542
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
543
|
+
|
|
544
|
+
await clickTab(page, "demos");
|
|
545
|
+
|
|
546
|
+
const logViewer = page.locator('[data-testid="log-viewer"]');
|
|
547
|
+
const commentary = logViewer.locator(".commentary");
|
|
548
|
+
|
|
549
|
+
await expect(commentary).toHaveCount(1);
|
|
550
|
+
await expect(commentary).toContainText("This is the commentary text");
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
test.describe("steps list visibility", () => {
|
|
555
|
+
test("hides steps list for log-based demo", async ({ page }) => {
|
|
556
|
+
await page.goto(generateLogPage());
|
|
557
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
558
|
+
|
|
559
|
+
await clickTab(page, "demos");
|
|
560
|
+
|
|
561
|
+
await expect(page.locator('[data-testid="log-viewer"]')).toBeVisible();
|
|
562
|
+
await expect(page.locator('[data-testid="steps-list"]')).not.toBeVisible();
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("shows steps list for web-ux demo", async ({ page }) => {
|
|
566
|
+
await page.goto(generateMixedPage());
|
|
567
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
568
|
+
|
|
569
|
+
await clickTab(page, "demos");
|
|
570
|
+
|
|
571
|
+
// First demo is web-ux
|
|
572
|
+
await expect(page.locator('[data-testid="video-player"]')).toBeVisible();
|
|
573
|
+
await expect(page.locator('[data-testid="steps-list"]')).toBeVisible();
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
test.describe("switching between demo types", () => {
|
|
578
|
+
test("switches from web-ux to log-based demo", async ({ page }) => {
|
|
579
|
+
await page.goto(generateMixedPage());
|
|
580
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
581
|
+
|
|
582
|
+
await clickTab(page, "demos");
|
|
583
|
+
|
|
584
|
+
// Initially shows video player (first demo is web-ux)
|
|
585
|
+
await expect(page.locator('[data-testid="video-player"]')).toBeVisible();
|
|
586
|
+
await expect(page.locator('[data-testid="log-viewer"]')).not.toBeVisible();
|
|
587
|
+
|
|
588
|
+
// Click on the second demo (log-based)
|
|
589
|
+
await page.locator('[data-testid="demo-list"] [data-testid="demo-item"]').nth(1).click();
|
|
590
|
+
|
|
591
|
+
// Should now show log viewer
|
|
592
|
+
await expect(page.locator('[data-testid="log-viewer"]')).toBeVisible();
|
|
593
|
+
await expect(page.locator('[data-testid="video-player"]')).not.toBeVisible();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("switches from log-based to web-ux demo", async ({ page }) => {
|
|
597
|
+
await page.goto(generateMixedPage());
|
|
598
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
599
|
+
|
|
600
|
+
await clickTab(page, "demos");
|
|
601
|
+
|
|
602
|
+
// Click on the second demo (log-based) first
|
|
603
|
+
await page.locator('[data-testid="demo-list"] [data-testid="demo-item"]').nth(1).click();
|
|
604
|
+
await expect(page.locator('[data-testid="log-viewer"]')).toBeVisible();
|
|
605
|
+
|
|
606
|
+
// Click back to first demo (web-ux)
|
|
607
|
+
await page.locator('[data-testid="demo-list"] [data-testid="demo-item"]').nth(0).click();
|
|
608
|
+
|
|
609
|
+
// Should show video player again
|
|
610
|
+
await expect(page.locator('[data-testid="video-player"]')).toBeVisible();
|
|
611
|
+
await expect(page.locator('[data-testid="log-viewer"]')).not.toBeVisible();
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("updates summary text when switching demos", async ({ page }) => {
|
|
615
|
+
await page.goto(generateMixedPage());
|
|
616
|
+
await page.waitForSelector("#app .v-application", { timeout: 5000 });
|
|
617
|
+
|
|
618
|
+
await clickTab(page, "demos");
|
|
619
|
+
|
|
620
|
+
// Check initial summary
|
|
621
|
+
await expect(page.locator("#summary-text")).toContainText("Shows the login flow");
|
|
622
|
+
|
|
623
|
+
// Switch to log-based demo
|
|
624
|
+
await page.locator('[data-testid="demo-list"] [data-testid="demo-item"]').nth(1).click();
|
|
625
|
+
|
|
626
|
+
// Check summary updated
|
|
627
|
+
await expect(page.locator("#summary-text")).toContainText("Database migration");
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
export { showCommentary, hideCommentary } from "./commentary.ts";
|
|
2
2
|
export type { ShowCommentaryOptions } from "./commentary.ts";
|
|
3
3
|
|
|
4
|
-
export type { DemoMetadata, ReviewMetadata } from "./review-types.ts";
|
|
5
|
-
export { buildReviewPrompt, invokeClaude,
|
|
6
|
-
export type { InvokeClaudeOptions, SpawnFn } from "./review.ts";
|
|
4
|
+
export type { DemoMetadata, ReviewMetadata, IssueSeverity, ReviewIssue, ReviewVerdict, CodeReview } from "./review-types.ts";
|
|
5
|
+
export { buildReviewPrompt, extractJson, invokeClaude, parseLlmResponse } from "./review.ts";
|
|
6
|
+
export type { InvokeClaudeOptions, SpawnFn, LlmReviewResponse, BuildReviewPromptOptions } from "./review.ts";
|
|
7
7
|
|
|
8
|
-
export {
|
|
9
|
-
export type {
|
|
8
|
+
export { getRepoContext } from "./git-context.ts";
|
|
9
|
+
export type { ExecFn, ReadFileFn, RepoContext, GetRepoContextOptions } from "./git-context.ts";
|
|
10
|
+
|
|
11
|
+
export { DemoRecorder } from "./recorder.ts";
|
|
12
|
+
export type { DemoStep } from "./recorder.ts";
|
|
13
|
+
|
|
14
|
+
export { generateReview, discoverDemoFiles, generateReviewHtml, getReviewTemplate } from "./review-generator.ts";
|
|
15
|
+
export type { GenerateReviewOptions, GenerateReviewResult, ReviewAppData, DemoFile } from "./review-generator.ts";
|
|
16
|
+
|
|
17
|
+
export { runReviewOrchestration } from "./orchestrator.ts";
|
|
18
|
+
export type { OrchestratorOptions, OrchestratorResult } from "./orchestrator.ts";
|
|
19
|
+
|
|
20
|
+
export { startFeedbackServer } from "./feedback-server.ts";
|
|
21
|
+
export type { FeedbackPayload, FeedbackServerResult } from "./feedback-server.ts";
|