@continuum8032/playwright-ai-tools 0.2.0-oidc-bootstrap.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.
@@ -0,0 +1,544 @@
1
+ import {
2
+ classifyFailure,
3
+ failureSignature,
4
+ generatePatchSuggestions
5
+ } from "./chunk-3VDDPPGG.js";
6
+
7
+ // src/benchmark.ts
8
+ function roundMetric(value) {
9
+ return Number(value.toFixed(4));
10
+ }
11
+ function benchmarkFailures(fixtures) {
12
+ const seenSignatures = /* @__PURE__ */ new Set();
13
+ const failures = fixtures.map((fixture) => {
14
+ const analysis = classifyFailure(fixture);
15
+ const suggestions = generatePatchSuggestions(fixture, analysis);
16
+ const signature = failureSignature(fixture);
17
+ const cacheHit = seenSignatures.has(signature);
18
+ seenSignatures.add(signature);
19
+ const expectedCategory = fixture.expectedCategory || "unknown";
20
+ return {
21
+ title: fixture.title,
22
+ file: fixture.file,
23
+ expectedCategory,
24
+ actualCategory: analysis.category,
25
+ correct: expectedCategory === analysis.category,
26
+ signature,
27
+ cacheHit,
28
+ analysis,
29
+ suggestions
30
+ };
31
+ });
32
+ const total = failures.length;
33
+ const correct = failures.filter((failure) => failure.correct).length;
34
+ const unknown = failures.filter((failure) => failure.actualCategory === "unknown").length;
35
+ const cacheHits = failures.filter((failure) => failure.cacheHit).length;
36
+ const blocked = failures.reduce(
37
+ (count, failure) => count + failure.suggestions.filter((suggestion) => suggestion.policyDecision === "blocked").length,
38
+ 0
39
+ );
40
+ const review = failures.reduce(
41
+ (count, failure) => count + failure.suggestions.filter((suggestion) => suggestion.policyDecision === "review").length,
42
+ 0
43
+ );
44
+ return {
45
+ metrics: {
46
+ total,
47
+ correct,
48
+ accuracy: total ? roundMetric(correct / total) : 0,
49
+ unknown,
50
+ unknownRate: total ? roundMetric(unknown / total) : 0,
51
+ cacheHits,
52
+ cacheHitRate: total ? roundMetric(cacheHits / total) : 0,
53
+ suggestions: { blocked, review }
54
+ },
55
+ failures
56
+ };
57
+ }
58
+
59
+ // src/generation.ts
60
+ import { mkdir, writeFile } from "fs/promises";
61
+ import path from "path";
62
+ import { chromium } from "@playwright/test";
63
+ var DESTRUCTIVE_PATTERNS = [
64
+ /\bdelete\b/i,
65
+ /\bremove\b/i,
66
+ /\bdestroy\b/i,
67
+ /\bpay\b/i,
68
+ /\bpayment\b/i,
69
+ /\bpurchase\b/i,
70
+ /\bsubmit\s+order\b/i,
71
+ /\bplace\s+order\b/i,
72
+ /\brefund\b/i,
73
+ /\bcancel\s+subscription\b/i
74
+ ];
75
+ function safeSegment(value, fallback) {
76
+ const normalized = (value || fallback).trim().replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 100);
77
+ return normalized || fallback;
78
+ }
79
+ function isLocalUrl(rawUrl) {
80
+ try {
81
+ const url = new URL(rawUrl);
82
+ return ["localhost", "127.0.0.1", "::1"].includes(url.hostname);
83
+ } catch {
84
+ return false;
85
+ }
86
+ }
87
+ function destructiveText(manualCase) {
88
+ return [
89
+ manualCase.title,
90
+ manualCase.description,
91
+ manualCase.preconditions,
92
+ ...manualCase.steps.flatMap((step) => [step.action, step.expected])
93
+ ].filter(Boolean).join("\n");
94
+ }
95
+ function isDestructiveManualCase(manualCase) {
96
+ const text = destructiveText(manualCase);
97
+ return DESTRUCTIVE_PATTERNS.some((pattern) => pattern.test(text));
98
+ }
99
+ function assertBrowserGenerationAllowed(request) {
100
+ if (!isLocalUrl(request.url) && !request.confirmBrowserRun) {
101
+ throw new Error(`Refusing to open external URL ${request.url}. Pass --confirm-browser-run to allow browser exploration.`);
102
+ }
103
+ if (isDestructiveManualCase(request.manualCase) && !request.allowDestructiveActions) {
104
+ throw new Error("Manual test case appears to contain destructive actions. Pass --allow-destructive-actions to override.");
105
+ }
106
+ }
107
+ function truncate(value, maxLength) {
108
+ return value.length > maxLength ? `${value.slice(0, maxLength)}...` : value;
109
+ }
110
+ function defaultSource(manualCase) {
111
+ return manualCase.source?.provider || manualCase.source?.project || "manual";
112
+ }
113
+ var TemplateTestGenerationProvider = class {
114
+ async generateTest(request) {
115
+ const title = request.manualCase.title.replace(/'/g, "\\'");
116
+ const steps = request.manualCase.steps.map((step) => ` // Step ${step.index ?? ""}: ${step.action}${step.expected ? ` -> ${step.expected}` : ""}`.trimEnd()).join("\n");
117
+ return {
118
+ code: [
119
+ "import { test, expect } from '@playwright/test';",
120
+ "",
121
+ `test('${title}', async ({ page }) => {`,
122
+ ` await page.goto('${request.url}');`,
123
+ steps || " // No manual steps were supplied.",
124
+ " await expect(page).toHaveURL(/./);",
125
+ "});"
126
+ ].join("\n"),
127
+ locators: [],
128
+ coverage: request.manualCase.steps.map((step) => ({
129
+ stepIndex: step.index,
130
+ status: "partial",
131
+ notes: "Template provider preserved the manual step as a draft comment."
132
+ })),
133
+ warnings: ["Template provider generated a draft without AI locator reasoning. Review before committing."]
134
+ };
135
+ }
136
+ };
137
+ async function generatePlaywrightTestDraft(request) {
138
+ assertBrowserGenerationAllowed(request);
139
+ const outputDir = request.outputDir || "generated-tests";
140
+ const source = safeSegment(request.sourceName || defaultSource(request.manualCase), "manual");
141
+ const caseKey = safeSegment(request.manualCase.key || request.manualCase.id, "case");
142
+ const caseDir = path.join(outputDir, source);
143
+ const filePath = path.join(caseDir, `${caseKey}.spec.ts`);
144
+ const reportPath = path.join(caseDir, `${caseKey}.generation-report.json`);
145
+ const screenshotPath = path.join(caseDir, `${caseKey}.png`);
146
+ await mkdir(caseDir, { recursive: true });
147
+ const browser = await chromium.launch({ headless: true });
148
+ try {
149
+ const context = await browser.newContext(request.storageState ? { storageState: request.storageState } : {});
150
+ try {
151
+ const page = await context.newPage();
152
+ await page.goto(request.url, { waitUntil: "domcontentloaded" });
153
+ const screenshot = await page.screenshot({ fullPage: true });
154
+ const [title, html, bodyText] = await Promise.all([
155
+ page.title(),
156
+ page.content(),
157
+ page.locator("body").innerText({ timeout: 2e3 }).catch(() => "")
158
+ ]);
159
+ await writeFile(screenshotPath, screenshot);
160
+ const browserEvidence = {
161
+ requestedUrl: request.url,
162
+ currentUrl: page.url(),
163
+ title,
164
+ domSummary: truncate(bodyText, 8e3),
165
+ htmlSnippet: truncate(html, 12e3),
166
+ screenshotBase64: screenshot.toString("base64")
167
+ };
168
+ const generated = await request.provider.generateTest({
169
+ manualCase: request.manualCase,
170
+ url: request.url,
171
+ browser: browserEvidence,
172
+ outputLanguage: request.outputLanguage
173
+ });
174
+ const draft = {
175
+ manualCase: request.manualCase,
176
+ filePath,
177
+ reportPath,
178
+ screenshotPath,
179
+ code: generated.code,
180
+ locators: generated.locators || [],
181
+ coverage: generated.coverage || [],
182
+ warnings: generated.warnings || [],
183
+ browser: {
184
+ requestedUrl: browserEvidence.requestedUrl,
185
+ currentUrl: browserEvidence.currentUrl,
186
+ title: browserEvidence.title,
187
+ domSummary: browserEvidence.domSummary,
188
+ htmlSnippet: browserEvidence.htmlSnippet
189
+ },
190
+ needsHumanReview: true
191
+ };
192
+ const report = {
193
+ manualCase: request.manualCase,
194
+ browser: draft.browser,
195
+ locators: draft.locators,
196
+ coverage: draft.coverage,
197
+ warnings: draft.warnings,
198
+ needsHumanReview: draft.needsHumanReview,
199
+ artifacts: { filePath, reportPath, screenshotPath }
200
+ };
201
+ await writeFile(filePath, `${generated.code.trimEnd()}
202
+ `, "utf8");
203
+ await writeFile(reportPath, `${JSON.stringify(report, null, 2)}
204
+ `, "utf8");
205
+ return draft;
206
+ } finally {
207
+ await context.close();
208
+ }
209
+ } finally {
210
+ await browser.close();
211
+ }
212
+ }
213
+
214
+ // src/normalize.ts
215
+ function walkSuites(suites, visitor) {
216
+ for (const suite of suites || []) {
217
+ for (const spec of suite.specs || []) {
218
+ visitor(spec, suite);
219
+ }
220
+ walkSuites(suite.suites || [], visitor);
221
+ }
222
+ }
223
+ function resultError(result) {
224
+ return result?.error?.message || result?.error?.stack || result?.errors?.[0]?.message || result?.errors?.[0]?.stack || "Unknown error";
225
+ }
226
+ function normalizeSteps(result) {
227
+ const collected = [];
228
+ const visit = (step, insideUserStep) => {
229
+ if (!step) return;
230
+ const category = step.category;
231
+ const isUserStep = category === "test.step";
232
+ const isExpect = category === "expect";
233
+ if ((isUserStep || isExpect) && !insideUserStep && step.title) {
234
+ collected.push(String(step.title));
235
+ }
236
+ for (const child of step.steps || []) {
237
+ visit(child, insideUserStep || isUserStep);
238
+ }
239
+ };
240
+ for (const step of result?.steps || []) {
241
+ visit(step, false);
242
+ }
243
+ return collected;
244
+ }
245
+ function attachmentType(contentType, name) {
246
+ if (contentType?.startsWith("image/")) return "screenshot";
247
+ if (contentType?.includes("zip")) return "trace";
248
+ if (contentType?.includes("video")) return "video";
249
+ if (name?.toLowerCase().includes("trace")) return "trace";
250
+ return name || "artifact";
251
+ }
252
+ function normalizeArtifacts(result) {
253
+ return (result?.attachments || []).filter((attachment) => attachment?.path).map((attachment) => ({
254
+ type: attachmentType(attachment.contentType, attachment.name),
255
+ path: String(attachment.path),
256
+ contentType: attachment.contentType
257
+ }));
258
+ }
259
+ function normalizePlaywrightJsonReport(report, options = {}) {
260
+ const failures = [];
261
+ const run = options.runId ? { id: options.runId } : void 0;
262
+ walkSuites(report?.suites || [], (spec) => {
263
+ for (const testCase of spec.tests || []) {
264
+ for (const result of testCase.results || []) {
265
+ if (!["failed", "timedOut", "interrupted"].includes(result.status)) {
266
+ continue;
267
+ }
268
+ failures.push({
269
+ title: String(spec.title || testCase.title || "unnamed test"),
270
+ file: String(spec.file || testCase.location?.file || "unknown"),
271
+ line: spec.line || testCase.location?.line,
272
+ project: testCase.projectName,
273
+ status: result.status,
274
+ retry: result.retry || 0,
275
+ durationMs: result.duration || 0,
276
+ error: resultError(result),
277
+ steps: normalizeSteps(result),
278
+ artifacts: normalizeArtifacts(result),
279
+ annotations: (testCase.annotations || []).map((annotation) => ({
280
+ type: String(annotation.type),
281
+ description: annotation.description
282
+ })),
283
+ run
284
+ });
285
+ }
286
+ }
287
+ });
288
+ return failures;
289
+ }
290
+
291
+ // src/providers/openai.ts
292
+ var analysisSchema = {
293
+ type: "object",
294
+ properties: {
295
+ side: { type: "string", enum: ["app_bug", "test_bug", "flaky_test", "env_issue", "unknown"] },
296
+ rootCause: { type: "string" },
297
+ evidence: { type: "array", items: { type: "string" } },
298
+ reproductionSteps: { type: "array", items: { type: "string" } },
299
+ suspects: {
300
+ type: "object",
301
+ properties: {
302
+ selectors: { type: "array", items: { type: "string" } },
303
+ pages: { type: "array", items: { type: "string" } },
304
+ endpoints: { type: "array", items: { type: "string" } }
305
+ },
306
+ required: ["selectors", "pages", "endpoints"],
307
+ additionalProperties: false
308
+ },
309
+ fixSuggestions: { type: "array", items: { type: "string" } },
310
+ confidence: { type: "number", minimum: 0, maximum: 100 },
311
+ category: {
312
+ type: "string",
313
+ enum: ["selector", "timing", "runtime", "test_data", "visual", "interaction", "environment", "unknown"]
314
+ },
315
+ risk: { type: "string", enum: ["low", "medium", "high"] },
316
+ evidenceRefs: { type: "array", items: { type: "string" } },
317
+ confidenceReasons: { type: "array", items: { type: "string" } },
318
+ needsHumanReview: { type: "boolean" }
319
+ },
320
+ required: [
321
+ "side",
322
+ "rootCause",
323
+ "evidence",
324
+ "reproductionSteps",
325
+ "suspects",
326
+ "fixSuggestions",
327
+ "confidence",
328
+ "category",
329
+ "risk",
330
+ "evidenceRefs",
331
+ "confidenceReasons",
332
+ "needsHumanReview"
333
+ ],
334
+ additionalProperties: false
335
+ };
336
+ var generationSchema = {
337
+ type: "object",
338
+ properties: {
339
+ code: { type: "string" },
340
+ locators: {
341
+ type: "array",
342
+ items: {
343
+ type: "object",
344
+ properties: {
345
+ stepIndex: { type: "number" },
346
+ selector: { type: "string" },
347
+ reason: { type: "string" }
348
+ },
349
+ required: ["selector", "reason"],
350
+ additionalProperties: false
351
+ }
352
+ },
353
+ coverage: {
354
+ type: "array",
355
+ items: {
356
+ type: "object",
357
+ properties: {
358
+ stepIndex: { type: "number" },
359
+ status: { type: "string", enum: ["covered", "partial", "missing"] },
360
+ notes: { type: "string" }
361
+ },
362
+ required: ["status", "notes"],
363
+ additionalProperties: false
364
+ }
365
+ },
366
+ warnings: { type: "array", items: { type: "string" } }
367
+ },
368
+ required: ["code", "locators", "coverage", "warnings"],
369
+ additionalProperties: false
370
+ };
371
+ function normalizeAnalysis(data, usage) {
372
+ return {
373
+ side: data.side || "unknown",
374
+ rootCause: data.rootCause || data.root_cause || "No root cause returned",
375
+ evidence: data.evidence || [],
376
+ reproductionSteps: data.reproductionSteps || data.reproduction_steps || [],
377
+ suspects: data.suspects || { selectors: [], pages: [], endpoints: [] },
378
+ fixSuggestions: data.fixSuggestions || data.fix || [],
379
+ confidence: Number(data.confidence ?? 0),
380
+ category: data.category || "unknown",
381
+ risk: data.risk || "high",
382
+ evidenceRefs: data.evidenceRefs || [],
383
+ confidenceReasons: data.confidenceReasons || [],
384
+ needsHumanReview: data.needsHumanReview ?? true,
385
+ source: "ai",
386
+ tokenUsage: usage ? {
387
+ inputTokens: usage.input_tokens || usage.inputTokens || 0,
388
+ outputTokens: usage.output_tokens || usage.outputTokens || 0,
389
+ totalTokens: usage.total_tokens || usage.totalTokens || 0
390
+ } : void 0
391
+ };
392
+ }
393
+ function normalizeGeneratedTest(data) {
394
+ return {
395
+ code: String(data.code || ""),
396
+ locators: Array.isArray(data.locators) ? data.locators : [],
397
+ coverage: Array.isArray(data.coverage) ? data.coverage : [],
398
+ warnings: Array.isArray(data.warnings) ? data.warnings : []
399
+ };
400
+ }
401
+ function outputText(response) {
402
+ if (response.output_text) return response.output_text;
403
+ for (const item of response.output || []) {
404
+ for (const content of item.content || []) {
405
+ if (content.text) return content.text;
406
+ }
407
+ }
408
+ return "";
409
+ }
410
+ function compactFailure(failure) {
411
+ return {
412
+ title: failure.title,
413
+ file: failure.file,
414
+ line: failure.line,
415
+ project: failure.project,
416
+ status: failure.status,
417
+ retry: failure.retry,
418
+ durationMs: failure.durationMs,
419
+ error: failure.error,
420
+ steps: failure.steps || [],
421
+ artifacts: failure.artifacts || [],
422
+ annotations: failure.annotations || [],
423
+ browserContext: failure.browserContext,
424
+ page: failure.page,
425
+ consoleErrors: failure.consoleErrors || [],
426
+ networkFailures: failure.networkFailures || [],
427
+ lastActions: failure.lastActions || [],
428
+ artifactRefs: failure.artifactRefs || failure.artifacts || []
429
+ };
430
+ }
431
+ var OpenAIResponsesProvider = class {
432
+ constructor(options = {}) {
433
+ this.apiKey = options.apiKey ?? process.env.OPENAI_API_KEY ?? "";
434
+ this.model = options.model ?? process.env.PW_AI_MODEL ?? process.env.AI_MODEL ?? "gpt-4.1-mini";
435
+ this.baseUrl = options.baseUrl ?? "https://api.openai.com/v1/responses";
436
+ this.transport = options.transport ?? ((url, init) => fetch(url, init));
437
+ }
438
+ async analyzeFailure(request) {
439
+ if (!this.apiKey) {
440
+ throw new Error("OPENAI_API_KEY is required for OpenAIResponsesProvider");
441
+ }
442
+ const payload = {
443
+ model: this.model,
444
+ instructions: `You are a generic Playwright failure analyzer. Return concise ${request.outputLanguage} JSON only. Use only the supplied test failure, trace evidence, steps, artifacts, and similar analyses. Every conclusion must point to supplied evidenceRefs.`,
445
+ input: JSON.stringify({
446
+ failure: compactFailure(request.failure),
447
+ similarAnalyses: request.similarAnalyses || []
448
+ }),
449
+ text: {
450
+ format: {
451
+ type: "json_schema",
452
+ name: "playwright_failure_analysis",
453
+ strict: true,
454
+ schema: analysisSchema
455
+ }
456
+ }
457
+ };
458
+ const response = await this.transport(this.baseUrl, {
459
+ method: "POST",
460
+ headers: {
461
+ authorization: `Bearer ${this.apiKey}`,
462
+ "content-type": "application/json"
463
+ },
464
+ body: JSON.stringify(payload)
465
+ });
466
+ if (!response.ok) {
467
+ throw new Error(`OpenAI Responses request failed with status ${response.status}`);
468
+ }
469
+ const body = await response.json();
470
+ const rawText = outputText(body);
471
+ if (!rawText) {
472
+ throw new Error("OpenAI Responses payload did not contain output text");
473
+ }
474
+ return normalizeAnalysis(JSON.parse(rawText), body.usage);
475
+ }
476
+ async generateTest(request) {
477
+ if (!this.apiKey) {
478
+ throw new Error("OPENAI_API_KEY is required for OpenAIResponsesProvider");
479
+ }
480
+ const browser = {
481
+ requestedUrl: request.browser.requestedUrl,
482
+ currentUrl: request.browser.currentUrl,
483
+ title: request.browser.title,
484
+ domSummary: request.browser.domSummary,
485
+ htmlSnippet: request.browser.htmlSnippet
486
+ };
487
+ const content = [
488
+ {
489
+ type: "input_text",
490
+ text: JSON.stringify({
491
+ manualCase: request.manualCase,
492
+ url: request.url,
493
+ browser
494
+ })
495
+ }
496
+ ];
497
+ if (request.browser.screenshotBase64) {
498
+ content.push({
499
+ type: "input_image",
500
+ image_url: `data:image/png;base64,${request.browser.screenshotBase64}`
501
+ });
502
+ }
503
+ const payload = {
504
+ model: this.model,
505
+ instructions: `You generate review-only Playwright tests from manual test cases. Return concise ${request.outputLanguage || "ru"} JSON only. Use only the supplied manual steps, browser DOM evidence, URL, and screenshot. Prefer user-facing locators and data-testid selectors when the evidence supports them. Do not weaken assertions, do not invent credentials, and include warnings for any uncertain step.`,
506
+ input: [{ role: "user", content }],
507
+ text: {
508
+ format: {
509
+ type: "json_schema",
510
+ name: "playwright_test_generation",
511
+ strict: true,
512
+ schema: generationSchema
513
+ }
514
+ }
515
+ };
516
+ const response = await this.transport(this.baseUrl, {
517
+ method: "POST",
518
+ headers: {
519
+ authorization: `Bearer ${this.apiKey}`,
520
+ "content-type": "application/json"
521
+ },
522
+ body: JSON.stringify(payload)
523
+ });
524
+ if (!response.ok) {
525
+ throw new Error(`OpenAI Responses request failed with status ${response.status}`);
526
+ }
527
+ const body = await response.json();
528
+ const rawText = outputText(body);
529
+ if (!rawText) {
530
+ throw new Error("OpenAI Responses payload did not contain output text");
531
+ }
532
+ return normalizeGeneratedTest(JSON.parse(rawText));
533
+ }
534
+ };
535
+
536
+ export {
537
+ benchmarkFailures,
538
+ isDestructiveManualCase,
539
+ assertBrowserGenerationAllowed,
540
+ TemplateTestGenerationProvider,
541
+ generatePlaywrightTestDraft,
542
+ normalizePlaywrightJsonReport,
543
+ OpenAIResponsesProvider
544
+ };