playwright-e2e-mcp 0.1.0

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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/http.d.ts +27 -0
  4. package/dist/http.js +156 -0
  5. package/dist/http.js.map +1 -0
  6. package/dist/index.d.ts +12 -0
  7. package/dist/index.js +56 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/server.d.ts +23 -0
  10. package/dist/server.js +225 -0
  11. package/dist/server.js.map +1 -0
  12. package/dist/tools/compare-visual-state.d.ts +77 -0
  13. package/dist/tools/compare-visual-state.js +223 -0
  14. package/dist/tools/compare-visual-state.js.map +1 -0
  15. package/dist/tools/diagnose-flaky.d.ts +87 -0
  16. package/dist/tools/diagnose-flaky.js +258 -0
  17. package/dist/tools/diagnose-flaky.js.map +1 -0
  18. package/dist/tools/generate-e2e-test.d.ts +51 -0
  19. package/dist/tools/generate-e2e-test.js +274 -0
  20. package/dist/tools/generate-e2e-test.js.map +1 -0
  21. package/dist/tools/get-failure.d.ts +26 -0
  22. package/dist/tools/get-failure.js +220 -0
  23. package/dist/tools/get-failure.js.map +1 -0
  24. package/dist/tools/inspect-page.d.ts +57 -0
  25. package/dist/tools/inspect-page.js +124 -0
  26. package/dist/tools/inspect-page.js.map +1 -0
  27. package/dist/tools/list-tests.d.ts +35 -0
  28. package/dist/tools/list-tests.js +88 -0
  29. package/dist/tools/list-tests.js.map +1 -0
  30. package/dist/tools/run-test.d.ts +70 -0
  31. package/dist/tools/run-test.js +169 -0
  32. package/dist/tools/run-test.js.map +1 -0
  33. package/dist/tools/shared.d.ts +86 -0
  34. package/dist/tools/shared.js +490 -0
  35. package/dist/tools/shared.js.map +1 -0
  36. package/dist/tools/validate-selector.d.ts +32 -0
  37. package/dist/tools/validate-selector.js +93 -0
  38. package/dist/tools/validate-selector.js.map +1 -0
  39. package/dist/types/index.d.ts +344 -0
  40. package/dist/types/index.js +51 -0
  41. package/dist/types/index.js.map +1 -0
  42. package/dist/utils/change-analyzer.d.ts +46 -0
  43. package/dist/utils/change-analyzer.js +290 -0
  44. package/dist/utils/change-analyzer.js.map +1 -0
  45. package/dist/utils/image-diff.d.ts +61 -0
  46. package/dist/utils/image-diff.js +373 -0
  47. package/dist/utils/image-diff.js.map +1 -0
  48. package/dist/utils/logger.d.ts +21 -0
  49. package/dist/utils/logger.js +132 -0
  50. package/dist/utils/logger.js.map +1 -0
  51. package/dist/utils/path-utils.d.ts +55 -0
  52. package/dist/utils/path-utils.js +211 -0
  53. package/dist/utils/path-utils.js.map +1 -0
  54. package/dist/utils/playwright-runner.d.ts +63 -0
  55. package/dist/utils/playwright-runner.js +594 -0
  56. package/dist/utils/playwright-runner.js.map +1 -0
  57. package/dist/utils/project-detector.d.ts +43 -0
  58. package/dist/utils/project-detector.js +255 -0
  59. package/dist/utils/project-detector.js.map +1 -0
  60. package/dist/utils/report-parser.d.ts +60 -0
  61. package/dist/utils/report-parser.js +501 -0
  62. package/dist/utils/report-parser.js.map +1 -0
  63. package/dist/utils/trace-reader.d.ts +109 -0
  64. package/dist/utils/trace-reader.js +608 -0
  65. package/dist/utils/trace-reader.js.map +1 -0
  66. package/examples/sample-test.spec.ts +49 -0
  67. package/package.json +66 -0
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Change + selector analysis for the generate-e2e-test tool.
3
+ *
4
+ * "Look at the agent's recent file changes, identify the components,
5
+ * and scaffold a test using the correct selectors" (blueprint). This
6
+ * module answers the first two parts: which files changed (git working
7
+ * tree, last commit, or mtime fallback) and which locators those files
8
+ * actually declare (data-testid, roles, labels, ids…).
9
+ */
10
+ import { execFile } from 'node:child_process';
11
+ import { promisify } from 'node:util';
12
+ import { readdir, readFile, stat } from 'node:fs/promises';
13
+ import path from 'node:path';
14
+ import { normalizePath, relativeToRoot } from './path-utils.js';
15
+ const execFileAsync = promisify(execFile);
16
+ const SOURCE_EXT = /\.(?:[cm]?[jt]sx?|vue|svelte|css|scss|less|html)$/;
17
+ const IGNORED_DIRS = new Set([
18
+ 'node_modules',
19
+ 'dist',
20
+ 'build',
21
+ '.git',
22
+ 'test-results',
23
+ 'playwright-report',
24
+ 'blob-report',
25
+ 'coverage',
26
+ '.next',
27
+ '.nuxt',
28
+ '.cache',
29
+ '.turbo',
30
+ ]);
31
+ const MAX_FILE_BYTES = 512 * 1024;
32
+ const MAX_SELECTORS = 40;
33
+ const DEFAULT_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
34
+ /* ------------------------------------------------------------------ */
35
+ /* Recent changes */
36
+ /* ------------------------------------------------------------------ */
37
+ function cleanPorcelainPath(raw) {
38
+ let p = raw.trim();
39
+ if (p.includes(' -> '))
40
+ p = p.split(' -> ').pop() ?? p;
41
+ if (p.startsWith('"') && p.endsWith('"'))
42
+ p = p.slice(1, -1);
43
+ return normalizePath(p);
44
+ }
45
+ /** Drop build/vendor paths git may report (e.g. no .gitignore yet). */
46
+ function isIgnoredPath(file) {
47
+ const segments = file.split('/');
48
+ if (segments.length > 1 && segments.some((segment) => IGNORED_DIRS.has(segment)))
49
+ return true;
50
+ return segments.slice(0, -1).some((segment) => segment === '.git');
51
+ }
52
+ async function gitStatusFiles(root) {
53
+ try {
54
+ const { stdout } = await execFileAsync('git', ['status', '--porcelain=v1', '-uall', '--untracked-files=all'], { cwd: root, timeout: 4_000, maxBuffer: 1024 * 1024 });
55
+ return stdout
56
+ .split(/\r?\n/)
57
+ .filter((line) => line.length > 3)
58
+ .map((line) => cleanPorcelainPath(line.slice(3)));
59
+ }
60
+ catch {
61
+ return null;
62
+ }
63
+ }
64
+ async function gitLastCommitFiles(root) {
65
+ try {
66
+ const { stdout } = await execFileAsync('git', ['diff', '--name-only', 'HEAD~1'], {
67
+ cwd: root,
68
+ timeout: 4_000,
69
+ maxBuffer: 1024 * 1024,
70
+ });
71
+ const files = stdout
72
+ .split(/\r?\n/)
73
+ .map((line) => line.trim())
74
+ .filter(Boolean)
75
+ .map((line) => normalizePath(line));
76
+ return files.length > 0 ? files : null;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ async function recentMtimeFiles(root, windowMs, limit) {
83
+ const cutoff = Date.now() - windowMs;
84
+ const found = [];
85
+ const walk = async (dir, depth) => {
86
+ if (depth > 8 || found.length > 500)
87
+ return;
88
+ const entries = await readdir(dir, { withFileTypes: true, encoding: 'utf8' }).catch(() => null);
89
+ if (!entries)
90
+ return;
91
+ for (const entry of entries) {
92
+ if (entry.name.startsWith('.') || IGNORED_DIRS.has(entry.name))
93
+ continue;
94
+ const full = path.join(dir, entry.name);
95
+ if (entry.isDirectory()) {
96
+ await walk(full, depth + 1);
97
+ }
98
+ else if (entry.isFile() && SOURCE_EXT.test(entry.name)) {
99
+ try {
100
+ const info = await stat(full);
101
+ if (info.mtimeMs >= cutoff && info.size <= MAX_FILE_BYTES) {
102
+ found.push({ file: normalizePath(full), mtime: info.mtimeMs });
103
+ }
104
+ }
105
+ catch {
106
+ /* ignore */
107
+ }
108
+ }
109
+ }
110
+ };
111
+ await walk(root, 0);
112
+ found.sort((a, b) => b.mtime - a.mtime);
113
+ return found.slice(0, limit).map((entry) => entry.file);
114
+ }
115
+ /* ------------------------------------------------------------------ */
116
+ /* Selector extraction */
117
+ /* ------------------------------------------------------------------ */
118
+ function lineOf(source, index) {
119
+ let line = 1;
120
+ for (let i = 0; i < index && i < source.length; i += 1) {
121
+ if (source.charCodeAt(i) === 10)
122
+ line += 1;
123
+ }
124
+ return line;
125
+ }
126
+ function quote(value) {
127
+ return value.includes("'") ? `"${value}"` : `'${value}'`;
128
+ }
129
+ const PATTERNS = [
130
+ {
131
+ kind: 'test-id',
132
+ re: /\bdata-testid\s*=\s*["']([^"']{1,80})["']/g,
133
+ toLocator: (m) => `getByTestId(${quote(m[1])})`,
134
+ },
135
+ {
136
+ kind: 'test-id',
137
+ re: /\bdata-(test-id|test|cy)\s*=\s*["']([^"']{1,80})["']/g,
138
+ toLocator: (m) => `locator(${quote(`[data-${m[1]}="${m[2]}"]`)})`,
139
+ },
140
+ {
141
+ kind: 'test-id',
142
+ re: /\bgetByTestId\(\s*["'`]([^"'`]{1,80})["'`]/g,
143
+ toLocator: (m) => `getByTestId(${quote(m[1])})`,
144
+ },
145
+ {
146
+ kind: 'role',
147
+ re: /\bgetByRole\(\s*["'`]([a-zA-Z]+)["'`]\s*,\s*\{\s*name:\s*["'`]([^"'`]{1,80})["'`]/g,
148
+ toLocator: (m) => `getByRole(${quote(m[1])}, { name: ${quote(m[2])} })`,
149
+ },
150
+ {
151
+ kind: 'aria',
152
+ re: /\baria-label\s*=\s*["']([^"']{1,80})["']/g,
153
+ toLocator: (m) => `getByLabel(${quote(m[1])})`,
154
+ },
155
+ {
156
+ kind: 'placeholder',
157
+ re: /\bplaceholder\s*=\s*["']([^"']{1,80})["']/g,
158
+ toLocator: (m) => `getByPlaceholder(${quote(m[1])})`,
159
+ },
160
+ {
161
+ kind: 'id',
162
+ re: /(?<![\w.])id\s*=\s*["']([\w-]{1,60})["']/g,
163
+ toLocator: (m) => `locator(${quote(`#${m[1]}`)})`,
164
+ },
165
+ {
166
+ kind: 'name',
167
+ re: /(?<![\w.])name\s*=\s*["']([\w:-]{1,60})["']/g,
168
+ toLocator: (m) => `locator(${quote(`[name="${m[1]}"]`)})`,
169
+ },
170
+ {
171
+ kind: 'text',
172
+ re: /<(button|a)\b[^>]*>\s*([^<>{}\n]{2,60}?)\s*<\/\1>/g,
173
+ toLocator: (m) => `getByRole(${quote(m[1] === 'a' ? 'link' : 'button')}, { name: ${quote(m[2].trim())} })`,
174
+ },
175
+ {
176
+ kind: 'text',
177
+ re: /\bgetByText\(\s*["'`]([^"'`]{1,80})["'`]/g,
178
+ toLocator: (m) => `getByText(${quote(m[1])})`,
179
+ },
180
+ ];
181
+ /** Extract locators declared in one source file. */
182
+ export function extractSelectors(source, file) {
183
+ const out = [];
184
+ const seen = new Set();
185
+ const bounded = source.length > MAX_FILE_BYTES ? source.slice(0, MAX_FILE_BYTES) : source;
186
+ for (const pattern of PATTERNS) {
187
+ pattern.re.lastIndex = 0;
188
+ let match;
189
+ while ((match = pattern.re.exec(bounded)) !== null) {
190
+ const locator = pattern.toLocator(match);
191
+ if (!locator)
192
+ continue;
193
+ // The meaningful value is always the pattern's last capture group.
194
+ const value = match[match.length - 1] ?? '';
195
+ if (value === '')
196
+ continue;
197
+ const key = `${pattern.kind}:${value}`;
198
+ if (seen.has(key))
199
+ continue;
200
+ seen.add(key);
201
+ out.push({
202
+ kind: pattern.kind,
203
+ value,
204
+ locator,
205
+ file,
206
+ line: lineOf(bounded, match.index),
207
+ });
208
+ if (out.length >= MAX_SELECTORS)
209
+ return out;
210
+ }
211
+ }
212
+ return out;
213
+ }
214
+ export async function analyzeRecentChanges(projectRoot, options = {}) {
215
+ const root = normalizePath(projectRoot);
216
+ const limit = options.limit ?? 12;
217
+ let changeSource = 'mtime';
218
+ let files = (await gitStatusFiles(root))?.filter((file) => !isIgnoredPath(file)) ?? null;
219
+ if (files !== null && files.length === 0) {
220
+ files = (await gitLastCommitFiles(root))?.filter((file) => !isIgnoredPath(file)) ?? null;
221
+ if (files !== null)
222
+ changeSource = 'git-last-commit';
223
+ }
224
+ else if (files !== null) {
225
+ changeSource = 'git-status';
226
+ }
227
+ if (files === null) {
228
+ files = (await recentMtimeFiles(root, options.windowMs ?? DEFAULT_WINDOW_MS, limit)).filter((file) => !isIgnoredPath(file));
229
+ changeSource = 'mtime';
230
+ }
231
+ const selectors = [];
232
+ const seen = new Set();
233
+ const scanned = [];
234
+ let truncated = false;
235
+ for (const file of files) {
236
+ if (scanned.length >= limit) {
237
+ truncated = true;
238
+ break;
239
+ }
240
+ const absolute = path.isAbsolute(file) ? file : path.join(root, file);
241
+ let source;
242
+ try {
243
+ const info = await stat(absolute);
244
+ if (!info.isFile() || info.size > MAX_FILE_BYTES)
245
+ continue;
246
+ source = await readFile(absolute, 'utf8');
247
+ }
248
+ catch {
249
+ continue;
250
+ }
251
+ if (!SOURCE_EXT.test(absolute))
252
+ continue;
253
+ scanned.push(relativeToRoot(root, absolute));
254
+ for (const candidate of extractSelectors(source, relativeToRoot(root, absolute))) {
255
+ const key = `${candidate.kind}:${candidate.value}`;
256
+ if (seen.has(key))
257
+ continue;
258
+ seen.add(key);
259
+ selectors.push(candidate);
260
+ if (selectors.length >= MAX_SELECTORS) {
261
+ truncated = true;
262
+ break;
263
+ }
264
+ }
265
+ if (selectors.length >= MAX_SELECTORS)
266
+ break;
267
+ }
268
+ return {
269
+ changeSource,
270
+ files: files.map((file) => ({
271
+ path: path.isAbsolute(file) ? relativeToRoot(root, file) : normalizePath(file),
272
+ source: changeSource === 'mtime' ? 'mtime' : 'git',
273
+ })),
274
+ selectors,
275
+ scanned: scanned.length,
276
+ truncated,
277
+ };
278
+ }
279
+ /** Pull `baseURL` / `webServer.url` out of a playwright.config source. */
280
+ export function detectEntryUrl(configSource) {
281
+ const out = {};
282
+ const base = /baseURL\s*:\s*["']([^"']{3,200})["']/.exec(configSource);
283
+ if (base)
284
+ out.baseURL = base[1];
285
+ const server = /\burl\s*:\s*["'](https?:\/\/[^"']{3,200})["']/.exec(configSource);
286
+ if (server)
287
+ out.webServerUrl = server[1];
288
+ return out;
289
+ }
290
+ //# sourceMappingURL=change-analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"change-analyzer.js","sourceRoot":"","sources":["../../src/utils/change-analyzer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Visual regression core: PNG decode/encode and a pixel diff.
3
+ *
4
+ * Playwright screenshots are 8-bit non-interlaced PNGs, so a small
5
+ * built-in codec covers them without adding dependencies. The diff
6
+ * reports how much changed, *where* it changed (merged grid regions),
7
+ * and the average color shift — powering the "you changed the button
8
+ * from blue to red" message from the blueprint.
9
+ */
10
+ export interface RgbaImage {
11
+ width: number;
12
+ height: number;
13
+ /** RGBA8, row-major, length = width * height * 4. */
14
+ data: Uint8Array;
15
+ }
16
+ export interface Rgb {
17
+ r: number;
18
+ g: number;
19
+ b: number;
20
+ }
21
+ /** Decode an 8-bit, non-interlaced PNG (gray/RGB/palette/alpha variants). */
22
+ export declare function decodePng(buf: Buffer): RgbaImage;
23
+ /** Encode an RGBA image as a PNG (filter 0, default compression). */
24
+ export declare function encodePng(image: RgbaImage): Buffer;
25
+ export interface DiffOptions {
26
+ /** Per-pixel combined channel delta (0–765) above which a pixel differs. Default 60. */
27
+ pixelThreshold?: number;
28
+ /** Grid cell size used to cluster changed pixels into regions. Default 24. */
29
+ cellSize?: number;
30
+ /** Fraction of a cell that must differ before the cell counts. Default 0.05. */
31
+ cellRatio?: number;
32
+ }
33
+ export interface DiffRegion {
34
+ x: number;
35
+ y: number;
36
+ width: number;
37
+ height: number;
38
+ pixels: number;
39
+ }
40
+ export interface DiffResult {
41
+ width: number;
42
+ height: number;
43
+ totalPixels: number;
44
+ diffPixels: number;
45
+ /** Percentage of changed pixels (0–100). */
46
+ percent: number;
47
+ /** True when the two screenshots have different dimensions. */
48
+ dimensionsChanged: boolean;
49
+ /** Changed areas, largest first (top 5). */
50
+ regions: DiffRegion[];
51
+ /** Average color of changed pixels, before → after. */
52
+ beforeAvg?: Rgb;
53
+ afterAvg?: Rgb;
54
+ /** Annotated image: grayscale baseline, changed pixels in red. */
55
+ diffImage: RgbaImage;
56
+ }
57
+ /**
58
+ * Compare two screenshots. When dimensions differ, the overlapping
59
+ * region is compared and the remainder counts as changed.
60
+ */
61
+ export declare function diffImages(before: RgbaImage, after: RgbaImage, options?: DiffOptions): DiffResult;