@atezca/core 1.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.
- package/README.md +47 -0
- package/dist/cli.d.mts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +1566 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.mjs +1539 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/index.d.mts +204 -0
- package/dist/index.d.ts +204 -0
- package/dist/index.js +101 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +91 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +59 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1566 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// ../../node_modules/.pnpm/tsup@8.5.1_postcss@8.5.6_typescript@5.9.3/node_modules/tsup/assets/cjs_shims.js
|
|
27
|
+
var getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
28
|
+
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
29
|
+
|
|
30
|
+
// src/cli.ts
|
|
31
|
+
var fs4 = __toESM(require("fs"));
|
|
32
|
+
var path3 = __toESM(require("path"));
|
|
33
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
34
|
+
|
|
35
|
+
// src/index.ts
|
|
36
|
+
var AtezcaState = class {
|
|
37
|
+
config = null;
|
|
38
|
+
commands = [];
|
|
39
|
+
setConfig(config) {
|
|
40
|
+
this.config = config;
|
|
41
|
+
}
|
|
42
|
+
getConfig() {
|
|
43
|
+
return this.config;
|
|
44
|
+
}
|
|
45
|
+
addCommand(command) {
|
|
46
|
+
this.commands.push(command);
|
|
47
|
+
}
|
|
48
|
+
getCommands() {
|
|
49
|
+
return [...this.commands];
|
|
50
|
+
}
|
|
51
|
+
clear() {
|
|
52
|
+
this.config = null;
|
|
53
|
+
this.commands = [];
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
var ATEZCA_STATE_KEY = /* @__PURE__ */ Symbol.for("__ATEZCA_STATE__");
|
|
57
|
+
if (!globalThis[ATEZCA_STATE_KEY]) {
|
|
58
|
+
globalThis[ATEZCA_STATE_KEY] = new AtezcaState();
|
|
59
|
+
}
|
|
60
|
+
var state = globalThis[ATEZCA_STATE_KEY];
|
|
61
|
+
function getState() {
|
|
62
|
+
return {
|
|
63
|
+
config: state.getConfig(),
|
|
64
|
+
commands: state.getCommands()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/runner/test-runner.ts
|
|
69
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
70
|
+
|
|
71
|
+
// src/config/config-loader.ts
|
|
72
|
+
var fs = __toESM(require("fs"));
|
|
73
|
+
var path = __toESM(require("path"));
|
|
74
|
+
var import_dotenv = require("dotenv");
|
|
75
|
+
var import_zod = require("zod");
|
|
76
|
+
var ConfigSchema = import_zod.z.object({
|
|
77
|
+
aiProvider: import_zod.z.enum(["claude", "gemini"]).default("claude"),
|
|
78
|
+
anthropicApiKey: import_zod.z.string().optional(),
|
|
79
|
+
googleApiKey: import_zod.z.string().optional(),
|
|
80
|
+
cache: import_zod.z.object({
|
|
81
|
+
enabled: import_zod.z.boolean().default(true),
|
|
82
|
+
expiryDays: import_zod.z.number().positive().default(30),
|
|
83
|
+
filePath: import_zod.z.string().default(".atezca-cache.json")
|
|
84
|
+
}),
|
|
85
|
+
browser: import_zod.z.object({
|
|
86
|
+
type: import_zod.z.enum(["chromium", "firefox", "webkit"]).default("chromium"),
|
|
87
|
+
headless: import_zod.z.boolean().default(true)
|
|
88
|
+
}),
|
|
89
|
+
execution: import_zod.z.object({
|
|
90
|
+
timeout: import_zod.z.number().positive().default(3e4),
|
|
91
|
+
retries: import_zod.z.number().nonnegative().default(3),
|
|
92
|
+
outputDir: import_zod.z.string().default("generated-tests")
|
|
93
|
+
})
|
|
94
|
+
}).refine(
|
|
95
|
+
(data) => {
|
|
96
|
+
if (data.aiProvider === "claude" && !data.anthropicApiKey) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
if (data.aiProvider === "gemini" && !data.googleApiKey) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
message: "API key required for selected AI provider"
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
var ConfigLoader = class {
|
|
109
|
+
config = null;
|
|
110
|
+
constructor() {
|
|
111
|
+
this.load();
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Load configuration
|
|
115
|
+
*/
|
|
116
|
+
load() {
|
|
117
|
+
(0, import_dotenv.config)();
|
|
118
|
+
const configFile = this.findConfigFile();
|
|
119
|
+
let fileConfig = {};
|
|
120
|
+
if (configFile) {
|
|
121
|
+
try {
|
|
122
|
+
const content = fs.readFileSync(configFile, "utf-8");
|
|
123
|
+
fileConfig = JSON.parse(content);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.warn(`Warning: Failed to parse ${configFile}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const rawConfig = {
|
|
129
|
+
aiProvider: process.env.ATEZCA_AI_PROVIDER || fileConfig.aiProvider || "claude",
|
|
130
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY || fileConfig.anthropicApiKey || "",
|
|
131
|
+
googleApiKey: process.env.GOOGLE_API_KEY || fileConfig.googleApiKey || "",
|
|
132
|
+
cache: {
|
|
133
|
+
enabled: this.parseBoolean(process.env.ATEZCA_CACHE_ENABLED) ?? fileConfig.cacheEnabled ?? true,
|
|
134
|
+
expiryDays: parseInt(process.env.ATEZCA_CACHE_EXPIRY_DAYS ?? "") || fileConfig.cacheExpiryDays || 30,
|
|
135
|
+
filePath: process.env.ATEZCA_CACHE_FILE || fileConfig.cacheFile || ".atezca-cache.json"
|
|
136
|
+
},
|
|
137
|
+
browser: {
|
|
138
|
+
type: process.env.ATEZCA_BROWSER || fileConfig.browser || "chromium",
|
|
139
|
+
headless: this.parseBoolean(process.env.ATEZCA_HEADLESS) ?? fileConfig.headless ?? true
|
|
140
|
+
},
|
|
141
|
+
execution: {
|
|
142
|
+
timeout: parseInt(process.env.ATEZCA_TIMEOUT ?? "") || fileConfig.timeout || 3e4,
|
|
143
|
+
retries: parseInt(process.env.ATEZCA_RETRIES ?? "") || fileConfig.retries || 3,
|
|
144
|
+
outputDir: process.env.ATEZCA_OUTPUT_DIR || fileConfig.outputDir || "generated-tests"
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
try {
|
|
148
|
+
this.config = ConfigSchema.parse(rawConfig);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
if (error instanceof import_zod.z.ZodError) {
|
|
151
|
+
const messages = error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("\n");
|
|
152
|
+
throw new Error(`Configuration validation failed:
|
|
153
|
+
${messages}`);
|
|
154
|
+
}
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Find config file in current or parent directories
|
|
160
|
+
*/
|
|
161
|
+
findConfigFile() {
|
|
162
|
+
const filenames = [".atezcarc", ".atezcarc.json"];
|
|
163
|
+
let dir = process.cwd();
|
|
164
|
+
while (true) {
|
|
165
|
+
for (const filename of filenames) {
|
|
166
|
+
const filepath = path.join(dir, filename);
|
|
167
|
+
if (fs.existsSync(filepath)) {
|
|
168
|
+
return filepath;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const parent = path.dirname(dir);
|
|
172
|
+
if (parent === dir) break;
|
|
173
|
+
dir = parent;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Parse boolean from string
|
|
179
|
+
*/
|
|
180
|
+
parseBoolean(value) {
|
|
181
|
+
if (value === void 0) return void 0;
|
|
182
|
+
return value.toLowerCase() === "true" || value === "1";
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Get configuration
|
|
186
|
+
*/
|
|
187
|
+
getConfig() {
|
|
188
|
+
if (!this.config) {
|
|
189
|
+
throw new Error("Configuration not loaded");
|
|
190
|
+
}
|
|
191
|
+
return this.config;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Create default config file
|
|
195
|
+
*/
|
|
196
|
+
static createDefaultConfig(filepath = ".atezcarc") {
|
|
197
|
+
const defaultConfig = {
|
|
198
|
+
aiProvider: "claude",
|
|
199
|
+
anthropicApiKey: process.env.ANTHROPIC_API_KEY || "",
|
|
200
|
+
googleApiKey: process.env.GOOGLE_API_KEY || "",
|
|
201
|
+
cacheEnabled: true,
|
|
202
|
+
cacheExpiryDays: 30,
|
|
203
|
+
cacheFile: ".atezca-cache.json",
|
|
204
|
+
browser: "chromium",
|
|
205
|
+
headless: true,
|
|
206
|
+
timeout: 3e4,
|
|
207
|
+
retries: 3,
|
|
208
|
+
outputDir: "generated-tests"
|
|
209
|
+
};
|
|
210
|
+
fs.writeFileSync(filepath, JSON.stringify(defaultConfig, null, 2), "utf-8");
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
var globalConfig = null;
|
|
214
|
+
function getConfig() {
|
|
215
|
+
if (!globalConfig) {
|
|
216
|
+
globalConfig = new ConfigLoader();
|
|
217
|
+
}
|
|
218
|
+
return globalConfig.getConfig();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/interpreter/interpreter.ts
|
|
222
|
+
var import_zod2 = require("zod");
|
|
223
|
+
|
|
224
|
+
// src/interpreter/claude-client.ts
|
|
225
|
+
var import_sdk = __toESM(require("@anthropic-ai/sdk"));
|
|
226
|
+
var ClaudeClient = class {
|
|
227
|
+
client;
|
|
228
|
+
model;
|
|
229
|
+
constructor(apiKey, model = "claude-3-5-sonnet-20241022") {
|
|
230
|
+
if (!apiKey || !apiKey.startsWith("sk-ant-")) {
|
|
231
|
+
throw new Error('Invalid Anthropic API key. Must start with "sk-ant-"');
|
|
232
|
+
}
|
|
233
|
+
this.client = new import_sdk.default({
|
|
234
|
+
apiKey
|
|
235
|
+
});
|
|
236
|
+
this.model = model;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Send a message to Claude and get response
|
|
240
|
+
*/
|
|
241
|
+
async sendMessage(systemPrompt, userPrompt) {
|
|
242
|
+
console.log(`\u{1F916} Using model: ${this.model}`);
|
|
243
|
+
try {
|
|
244
|
+
const message = await this.client.messages.create({
|
|
245
|
+
model: this.model,
|
|
246
|
+
max_tokens: 1024,
|
|
247
|
+
temperature: 0,
|
|
248
|
+
// Deterministic responses
|
|
249
|
+
system: systemPrompt,
|
|
250
|
+
messages: [
|
|
251
|
+
{
|
|
252
|
+
role: "user",
|
|
253
|
+
content: userPrompt
|
|
254
|
+
}
|
|
255
|
+
]
|
|
256
|
+
});
|
|
257
|
+
const content = message.content[0];
|
|
258
|
+
if (content.type !== "text") {
|
|
259
|
+
throw new Error("Expected text response from Claude");
|
|
260
|
+
}
|
|
261
|
+
return content.text;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
if (error instanceof import_sdk.default.APIError) {
|
|
264
|
+
throw new Error(`Claude API error: ${error.message}`);
|
|
265
|
+
}
|
|
266
|
+
throw error;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Test API key validity
|
|
271
|
+
*/
|
|
272
|
+
async testConnection() {
|
|
273
|
+
try {
|
|
274
|
+
await this.sendMessage("You are a test assistant.", 'Respond with "OK"');
|
|
275
|
+
return true;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// src/interpreter/gemini-client.ts
|
|
283
|
+
var import_generative_ai = require("@google/generative-ai");
|
|
284
|
+
var GeminiClient = class {
|
|
285
|
+
genAI;
|
|
286
|
+
model;
|
|
287
|
+
constructor(apiKey, model = "gemini-3-pro-preview") {
|
|
288
|
+
if (!apiKey || !apiKey.startsWith("AIza")) {
|
|
289
|
+
throw new Error('Invalid Google API key. Must start with "AIza"');
|
|
290
|
+
}
|
|
291
|
+
this.genAI = new import_generative_ai.GoogleGenerativeAI(apiKey);
|
|
292
|
+
this.model = model;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Send a message to Gemini and get response
|
|
296
|
+
*/
|
|
297
|
+
async sendMessage(systemPrompt, userPrompt) {
|
|
298
|
+
console.log(`\u{1F916} Using model: ${this.model}`);
|
|
299
|
+
try {
|
|
300
|
+
const model = this.genAI.getGenerativeModel({
|
|
301
|
+
model: this.model,
|
|
302
|
+
generationConfig: {
|
|
303
|
+
temperature: 0,
|
|
304
|
+
// Deterministic responses
|
|
305
|
+
maxOutputTokens: 1024
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
const fullPrompt = `${systemPrompt}
|
|
309
|
+
|
|
310
|
+
User Request:
|
|
311
|
+
${userPrompt}`;
|
|
312
|
+
const result = await model.generateContent(fullPrompt);
|
|
313
|
+
const response = await result.response;
|
|
314
|
+
return response.text();
|
|
315
|
+
} catch (error) {
|
|
316
|
+
if (error instanceof Error) {
|
|
317
|
+
throw new Error(`Gemini API error: ${error.message}`);
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Test API key validity
|
|
324
|
+
*/
|
|
325
|
+
async testConnection() {
|
|
326
|
+
try {
|
|
327
|
+
await this.sendMessage("You are a test assistant.", 'Respond with "OK"');
|
|
328
|
+
return true;
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// src/interpreter/prompt.ts
|
|
336
|
+
function getSystemPrompt() {
|
|
337
|
+
return `You are an expert E2E test automation assistant. Your role is to interpret natural language test commands and convert them into structured Playwright actions.
|
|
338
|
+
|
|
339
|
+
CRITICAL RULES:
|
|
340
|
+
1. Always respond with valid JSON only - no markdown, no code blocks, no explanations
|
|
341
|
+
2. Use robust selectors in order of preference: role > aria-label > data-testid > text content > CSS
|
|
342
|
+
3. Be specific and deterministic - avoid ambiguous selectors
|
|
343
|
+
4. For interact actions with buttons/links, prefer getByRole over other selectors
|
|
344
|
+
5. For form inputs, use getByLabel or getByPlaceholder
|
|
345
|
+
6. Always include timeout values for wait actions
|
|
346
|
+
|
|
347
|
+
OUTPUT FORMAT (JSON only):
|
|
348
|
+
{
|
|
349
|
+
"actions": [
|
|
350
|
+
{
|
|
351
|
+
"action": "click" | "type" | "navigate" | "wait" | "expect" | "select" | "hover" | "press",
|
|
352
|
+
"selector": "button:has-text('Login')" or "role=button[name='Login']",
|
|
353
|
+
"value": "text to type or URL to navigate",
|
|
354
|
+
"condition": "visible|hidden|attached|detached",
|
|
355
|
+
"assertionType": "visible|hidden|text|value|enabled|disabled",
|
|
356
|
+
"expected": "expected value for assertions",
|
|
357
|
+
"timeout": 30000
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
EXAMPLES:
|
|
363
|
+
|
|
364
|
+
Input: { actionType: "interact", description: "click button 'Login'" }
|
|
365
|
+
Output: {"actions":[{"action":"click","selector":"role=button[name='Login']"}]}
|
|
366
|
+
|
|
367
|
+
Input: { actionType: "interact", description: "type 'john@example.com' in email field" }
|
|
368
|
+
Output: {"actions":[{"action":"type","selector":"role=textbox[name='Email' i]","value":"john@example.com"}]}
|
|
369
|
+
|
|
370
|
+
Input: { actionType: "navigate", description: "go to login page" }
|
|
371
|
+
Output: {"actions":[{"action":"navigate","value":"/login"}]}
|
|
372
|
+
|
|
373
|
+
Input: { actionType: "wait", description: "until submit button is visible" }
|
|
374
|
+
Output: {"actions":[{"action":"wait","selector":"role=button[name='Submit']","condition":"visible","timeout":30000}]}
|
|
375
|
+
|
|
376
|
+
Input: { actionType: "expect", description: "show success message" }
|
|
377
|
+
Output: {"actions":[{"action":"expect","selector":"text=/success|successfully/i","assertionType":"visible"}]}
|
|
378
|
+
|
|
379
|
+
Input: { actionType: "interact", description: "select 'Premium' from plan dropdown" }
|
|
380
|
+
Output: {"actions":[{"action":"select","selector":"role=combobox[name='Plan']","value":"Premium"}]}
|
|
381
|
+
|
|
382
|
+
IMPORTANT:
|
|
383
|
+
- For "expect" actions, use appropriate assertionType (visible, text, value, etc.)
|
|
384
|
+
- For "wait" actions, always include timeout
|
|
385
|
+
- For text matching, use case-insensitive regex when appropriate: /text/i
|
|
386
|
+
- Respond ONLY with JSON, no other text`;
|
|
387
|
+
}
|
|
388
|
+
function getUserPrompt(command) {
|
|
389
|
+
return JSON.stringify({
|
|
390
|
+
actionType: command.actionType,
|
|
391
|
+
description: command.description
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function getUserPromptWithContext(command, context) {
|
|
395
|
+
const elementsInfo = context.interactiveElements.length > 0 ? `
|
|
396
|
+
|
|
397
|
+
Available interactive elements on the page:
|
|
398
|
+
${context.interactiveElements.map((el) => `- ${el.role}: "${el.name}" (${el.tag})`).join("\n")}` : "";
|
|
399
|
+
const accessibilityInfo = context.accessibilityTree && context.accessibilityTree !== "Unable to capture" ? `
|
|
400
|
+
|
|
401
|
+
Page structure:
|
|
402
|
+
${context.accessibilityTree}` : "";
|
|
403
|
+
return `CURRENT PAGE CONTEXT:
|
|
404
|
+
- URL: ${context.url}
|
|
405
|
+
- Title: ${context.title}${elementsInfo}${accessibilityInfo}
|
|
406
|
+
|
|
407
|
+
USER COMMAND:
|
|
408
|
+
${JSON.stringify({
|
|
409
|
+
actionType: command.actionType,
|
|
410
|
+
description: command.description
|
|
411
|
+
})}
|
|
412
|
+
|
|
413
|
+
Based on the current page context above, generate the appropriate Playwright actions to fulfill the user's command. Use the available elements to create accurate selectors.`;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/interpreter/interpreter.ts
|
|
417
|
+
var PlaywrightActionSchema = import_zod2.z.object({
|
|
418
|
+
action: import_zod2.z.enum(["click", "type", "navigate", "wait", "expect", "select", "hover", "press"]),
|
|
419
|
+
selector: import_zod2.z.string().optional(),
|
|
420
|
+
value: import_zod2.z.string().optional(),
|
|
421
|
+
condition: import_zod2.z.string().optional(),
|
|
422
|
+
assertionType: import_zod2.z.enum(["visible", "hidden", "text", "value", "enabled", "disabled"]).optional(),
|
|
423
|
+
expected: import_zod2.z.string().optional(),
|
|
424
|
+
timeout: import_zod2.z.number().optional()
|
|
425
|
+
});
|
|
426
|
+
var ClaudeResponseSchema = import_zod2.z.object({
|
|
427
|
+
actions: import_zod2.z.array(PlaywrightActionSchema)
|
|
428
|
+
});
|
|
429
|
+
var Interpreter = class {
|
|
430
|
+
client;
|
|
431
|
+
provider;
|
|
432
|
+
constructor(provider, apiKey) {
|
|
433
|
+
this.provider = provider;
|
|
434
|
+
switch (provider) {
|
|
435
|
+
case "claude":
|
|
436
|
+
this.client = new ClaudeClient(apiKey);
|
|
437
|
+
break;
|
|
438
|
+
case "gemini":
|
|
439
|
+
this.client = new GeminiClient(apiKey);
|
|
440
|
+
break;
|
|
441
|
+
default:
|
|
442
|
+
AI;
|
|
443
|
+
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Interpret a test command into Playwright actions
|
|
448
|
+
*/
|
|
449
|
+
async interpret(command, pageContext) {
|
|
450
|
+
const waitMatch = command.description.match(/^wait\s+(\d+)ms$/i);
|
|
451
|
+
if (waitMatch && command.actionType === "wait") {
|
|
452
|
+
const milliseconds = parseInt(waitMatch[1], 10);
|
|
453
|
+
return [{
|
|
454
|
+
action: "wait",
|
|
455
|
+
timeout: milliseconds,
|
|
456
|
+
selector: "body",
|
|
457
|
+
// Dummy selector, will just wait
|
|
458
|
+
condition: "attached"
|
|
459
|
+
}];
|
|
460
|
+
}
|
|
461
|
+
const systemPrompt = getSystemPrompt();
|
|
462
|
+
const userPrompt = pageContext ? getUserPromptWithContext(command, pageContext) : getUserPrompt(command);
|
|
463
|
+
const responseText = await this.client.sendMessage(systemPrompt, userPrompt);
|
|
464
|
+
let response;
|
|
465
|
+
try {
|
|
466
|
+
const cleanedResponse = responseText.replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
|
|
467
|
+
response = JSON.parse(cleanedResponse);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
throw new Error(`Failed to parse ${this.provider} response: ${responseText}`);
|
|
470
|
+
}
|
|
471
|
+
const validated = ClaudeResponseSchema.parse(response);
|
|
472
|
+
return validated.actions;
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* Test if the interpreter is working
|
|
476
|
+
*/
|
|
477
|
+
async test() {
|
|
478
|
+
return this.client.testConnection();
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
function createInterpretationResult(command, actions, cached = false) {
|
|
482
|
+
return {
|
|
483
|
+
command,
|
|
484
|
+
actions,
|
|
485
|
+
cached,
|
|
486
|
+
timestamp: Date.now()
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/cache/cache-manager.ts
|
|
491
|
+
var fs2 = __toESM(require("fs"));
|
|
492
|
+
var crypto = __toESM(require("crypto"));
|
|
493
|
+
var CacheManager = class {
|
|
494
|
+
cacheFilePath;
|
|
495
|
+
expiryDays;
|
|
496
|
+
cache;
|
|
497
|
+
constructor(cacheFilePath = ".atezca-cache.json", expiryDays = 30) {
|
|
498
|
+
this.cacheFilePath = cacheFilePath;
|
|
499
|
+
this.expiryDays = expiryDays;
|
|
500
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
501
|
+
this.load();
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Generate cache key from command
|
|
505
|
+
*/
|
|
506
|
+
generateKey(command) {
|
|
507
|
+
const data = `${command.actionType}:${command.description}`;
|
|
508
|
+
return crypto.createHash("sha256").update(data).digest("hex");
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Check if cache entry is expired
|
|
512
|
+
*/
|
|
513
|
+
isExpired(entry) {
|
|
514
|
+
return Date.now() > entry.expiresAt;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Load cache from file
|
|
518
|
+
*/
|
|
519
|
+
load() {
|
|
520
|
+
try {
|
|
521
|
+
if (fs2.existsSync(this.cacheFilePath)) {
|
|
522
|
+
const data = fs2.readFileSync(this.cacheFilePath, "utf-8");
|
|
523
|
+
const entries = JSON.parse(data);
|
|
524
|
+
for (const entry of entries) {
|
|
525
|
+
if (!this.isExpired(entry)) {
|
|
526
|
+
this.cache.set(entry.key, entry);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.warn("Failed to load cache, starting with empty cache");
|
|
532
|
+
this.cache.clear();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Save cache to file
|
|
537
|
+
*/
|
|
538
|
+
save() {
|
|
539
|
+
try {
|
|
540
|
+
const entries = Array.from(this.cache.values());
|
|
541
|
+
const data = JSON.stringify(entries, null, 2);
|
|
542
|
+
fs2.writeFileSync(this.cacheFilePath, data, "utf-8");
|
|
543
|
+
} catch (error) {
|
|
544
|
+
console.warn("Failed to save cache:", error);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Get cached actions for a command
|
|
549
|
+
*/
|
|
550
|
+
get(command) {
|
|
551
|
+
const key = this.generateKey(command);
|
|
552
|
+
const entry = this.cache.get(key);
|
|
553
|
+
if (!entry) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
if (this.isExpired(entry)) {
|
|
557
|
+
this.cache.delete(key);
|
|
558
|
+
this.save();
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
return entry.actions;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Store actions in cache
|
|
565
|
+
*/
|
|
566
|
+
set(command, actions) {
|
|
567
|
+
const key = this.generateKey(command);
|
|
568
|
+
const now = Date.now();
|
|
569
|
+
const expiryMs = this.expiryDays * 24 * 60 * 60 * 1e3;
|
|
570
|
+
const entry = {
|
|
571
|
+
key,
|
|
572
|
+
actions,
|
|
573
|
+
cachedAt: now,
|
|
574
|
+
expiresAt: now + expiryMs
|
|
575
|
+
};
|
|
576
|
+
this.cache.set(key, entry);
|
|
577
|
+
this.save();
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Check if command is cached
|
|
581
|
+
*/
|
|
582
|
+
has(command) {
|
|
583
|
+
return this.get(command) !== null;
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Clear all cache
|
|
587
|
+
*/
|
|
588
|
+
clear() {
|
|
589
|
+
this.cache.clear();
|
|
590
|
+
if (fs2.existsSync(this.cacheFilePath)) {
|
|
591
|
+
fs2.unlinkSync(this.cacheFilePath);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Clear expired entries
|
|
596
|
+
*/
|
|
597
|
+
clearExpired() {
|
|
598
|
+
const keysToDelete = [];
|
|
599
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
600
|
+
if (this.isExpired(entry)) {
|
|
601
|
+
keysToDelete.push(key);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
for (const key of keysToDelete) {
|
|
605
|
+
this.cache.delete(key);
|
|
606
|
+
}
|
|
607
|
+
if (keysToDelete.length > 0) {
|
|
608
|
+
this.save();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Get cache statistics
|
|
613
|
+
*/
|
|
614
|
+
getStats() {
|
|
615
|
+
const entries = Array.from(this.cache.values());
|
|
616
|
+
if (entries.length === 0) {
|
|
617
|
+
return { size: 0, oldestEntry: null, newestEntry: null };
|
|
618
|
+
}
|
|
619
|
+
const timestamps = entries.map((e) => e.cachedAt);
|
|
620
|
+
return {
|
|
621
|
+
size: entries.length,
|
|
622
|
+
oldestEntry: Math.min(...timestamps),
|
|
623
|
+
newestEntry: Math.max(...timestamps)
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// src/executor/playwright-executor.ts
|
|
629
|
+
var import_playwright = require("playwright");
|
|
630
|
+
var PlaywrightExecutor = class {
|
|
631
|
+
browser = null;
|
|
632
|
+
context = null;
|
|
633
|
+
page = null;
|
|
634
|
+
config;
|
|
635
|
+
actionLog = [];
|
|
636
|
+
constructor(config) {
|
|
637
|
+
this.config = config;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Initialize browser and page
|
|
641
|
+
*/
|
|
642
|
+
async initialize() {
|
|
643
|
+
const browserType = this.config.browser ?? "chromium";
|
|
644
|
+
const headless = this.config.headless ?? true;
|
|
645
|
+
switch (browserType) {
|
|
646
|
+
case "chromium":
|
|
647
|
+
this.browser = await import_playwright.chromium.launch({ headless });
|
|
648
|
+
break;
|
|
649
|
+
case "firefox":
|
|
650
|
+
this.browser = await import_playwright.firefox.launch({ headless });
|
|
651
|
+
break;
|
|
652
|
+
case "webkit":
|
|
653
|
+
this.browser = await import_playwright.webkit.launch({ headless });
|
|
654
|
+
break;
|
|
655
|
+
default:
|
|
656
|
+
throw new Error(`Unsupported browser: ${browserType}`);
|
|
657
|
+
}
|
|
658
|
+
this.context = await this.browser.newContext({
|
|
659
|
+
ignoreHTTPSErrors: this.config.disableSSL ?? false
|
|
660
|
+
});
|
|
661
|
+
this.page = await this.context.newPage();
|
|
662
|
+
this.page.setDefaultTimeout(this.config.timeout ?? 3e4);
|
|
663
|
+
await this.page.goto(this.config.url);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Execute a single action with retry logic
|
|
667
|
+
*/
|
|
668
|
+
async executeAction(action) {
|
|
669
|
+
if (!this.page) {
|
|
670
|
+
throw new Error("Executor not initialized. Call initialize() first.");
|
|
671
|
+
}
|
|
672
|
+
const startTime = Date.now();
|
|
673
|
+
const retries = this.config.retries ?? 3;
|
|
674
|
+
let lastError = null;
|
|
675
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
676
|
+
try {
|
|
677
|
+
await this.performAction(action);
|
|
678
|
+
const duration2 = Date.now() - startTime;
|
|
679
|
+
this.actionLog.push({ action, result: "success", duration: duration2 });
|
|
680
|
+
return;
|
|
681
|
+
} catch (error) {
|
|
682
|
+
lastError = error;
|
|
683
|
+
if (attempt < retries - 1) {
|
|
684
|
+
await this.page.waitForTimeout(Math.pow(2, attempt) * 1e3);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
const duration = Date.now() - startTime;
|
|
689
|
+
this.actionLog.push({
|
|
690
|
+
action,
|
|
691
|
+
result: `failed: ${lastError?.message}`,
|
|
692
|
+
duration
|
|
693
|
+
});
|
|
694
|
+
throw lastError;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Perform the actual action
|
|
698
|
+
*/
|
|
699
|
+
async performAction(action) {
|
|
700
|
+
if (!this.page) {
|
|
701
|
+
throw new Error("Page not initialized");
|
|
702
|
+
}
|
|
703
|
+
const timeout = action.timeout ?? this.config.timeout ?? 3e4;
|
|
704
|
+
switch (action.action) {
|
|
705
|
+
case "click":
|
|
706
|
+
if (!action.selector) throw new Error("Click action requires selector");
|
|
707
|
+
await this.page.locator(action.selector).click({ timeout });
|
|
708
|
+
break;
|
|
709
|
+
case "type":
|
|
710
|
+
if (!action.selector) throw new Error("Type action requires selector");
|
|
711
|
+
if (!action.value) throw new Error("Type action requires value");
|
|
712
|
+
await this.page.locator(action.selector).fill(action.value, { timeout });
|
|
713
|
+
break;
|
|
714
|
+
case "navigate":
|
|
715
|
+
if (!action.value) throw new Error("Navigate action requires value");
|
|
716
|
+
const url = action.value.startsWith("http") ? action.value : new URL(action.value, this.config.url).toString();
|
|
717
|
+
await this.page.goto(url, { timeout });
|
|
718
|
+
break;
|
|
719
|
+
case "wait":
|
|
720
|
+
if (action.condition === "attached" && action.selector === "body") {
|
|
721
|
+
await this.page.waitForTimeout(timeout);
|
|
722
|
+
} else {
|
|
723
|
+
if (!action.selector) throw new Error("Wait action requires selector");
|
|
724
|
+
const state2 = action.condition ?? "visible";
|
|
725
|
+
await this.page.locator(action.selector).waitFor({
|
|
726
|
+
state: state2,
|
|
727
|
+
timeout
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
break;
|
|
731
|
+
case "expect":
|
|
732
|
+
if (!action.selector) throw new Error("Expect action requires selector");
|
|
733
|
+
await this.performAssertion(action, timeout);
|
|
734
|
+
break;
|
|
735
|
+
case "select":
|
|
736
|
+
if (!action.selector) throw new Error("Select action requires selector");
|
|
737
|
+
if (!action.value) throw new Error("Select action requires value");
|
|
738
|
+
await this.page.locator(action.selector).selectOption(action.value, { timeout });
|
|
739
|
+
break;
|
|
740
|
+
case "hover":
|
|
741
|
+
if (!action.selector) throw new Error("Hover action requires selector");
|
|
742
|
+
await this.page.locator(action.selector).hover({ timeout });
|
|
743
|
+
break;
|
|
744
|
+
case "press":
|
|
745
|
+
if (!action.value) throw new Error("Press action requires value (key name)");
|
|
746
|
+
await this.page.keyboard.press(action.value);
|
|
747
|
+
break;
|
|
748
|
+
default:
|
|
749
|
+
throw new Error(`Unsupported action: ${action.action}`);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Perform assertion
|
|
754
|
+
*/
|
|
755
|
+
async performAssertion(action, timeout) {
|
|
756
|
+
if (!this.page || !action.selector) return;
|
|
757
|
+
const locator = this.page.locator(action.selector);
|
|
758
|
+
switch (action.assertionType) {
|
|
759
|
+
case "visible":
|
|
760
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
761
|
+
break;
|
|
762
|
+
case "hidden":
|
|
763
|
+
await locator.waitFor({ state: "hidden", timeout });
|
|
764
|
+
break;
|
|
765
|
+
case "text":
|
|
766
|
+
if (!action.expected) throw new Error("Text assertion requires expected value");
|
|
767
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
768
|
+
const text = await locator.textContent();
|
|
769
|
+
if (!text?.includes(action.expected)) {
|
|
770
|
+
throw new Error(`Expected text "${action.expected}" but got "${text}"`);
|
|
771
|
+
}
|
|
772
|
+
break;
|
|
773
|
+
case "value":
|
|
774
|
+
if (!action.expected) throw new Error("Value assertion requires expected value");
|
|
775
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
776
|
+
const value = await locator.inputValue();
|
|
777
|
+
if (value !== action.expected) {
|
|
778
|
+
throw new Error(`Expected value "${action.expected}" but got "${value}"`);
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
case "enabled":
|
|
782
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
783
|
+
if (await locator.isDisabled()) {
|
|
784
|
+
throw new Error("Expected element to be enabled but it is disabled");
|
|
785
|
+
}
|
|
786
|
+
break;
|
|
787
|
+
case "disabled":
|
|
788
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
789
|
+
if (await locator.isEnabled()) {
|
|
790
|
+
throw new Error("Expected element to be disabled but it is enabled");
|
|
791
|
+
}
|
|
792
|
+
break;
|
|
793
|
+
default:
|
|
794
|
+
await locator.waitFor({ state: "visible", timeout });
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
/**
|
|
798
|
+
* Execute multiple actions in sequence
|
|
799
|
+
*/
|
|
800
|
+
async executeActions(actions) {
|
|
801
|
+
for (const action of actions) {
|
|
802
|
+
await this.executeAction(action);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Get action execution log
|
|
807
|
+
*/
|
|
808
|
+
getActionLog() {
|
|
809
|
+
return [...this.actionLog];
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Take screenshot
|
|
813
|
+
*/
|
|
814
|
+
async screenshot(path4) {
|
|
815
|
+
if (!this.page) {
|
|
816
|
+
throw new Error("Page not initialized");
|
|
817
|
+
}
|
|
818
|
+
await this.page.screenshot({ path: path4 });
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Close browser
|
|
822
|
+
*/
|
|
823
|
+
async close() {
|
|
824
|
+
if (this.context) {
|
|
825
|
+
await this.context.close();
|
|
826
|
+
}
|
|
827
|
+
if (this.browser) {
|
|
828
|
+
await this.browser.close();
|
|
829
|
+
}
|
|
830
|
+
this.browser = null;
|
|
831
|
+
this.context = null;
|
|
832
|
+
this.page = null;
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Get current page (for advanced usage)
|
|
836
|
+
*/
|
|
837
|
+
getPage() {
|
|
838
|
+
return this.page;
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* Capture current page context for AI interpretation
|
|
842
|
+
*/
|
|
843
|
+
async getPageContext() {
|
|
844
|
+
if (!this.page) {
|
|
845
|
+
throw new Error("Page not initialized");
|
|
846
|
+
}
|
|
847
|
+
try {
|
|
848
|
+
const url = this.page.url();
|
|
849
|
+
const title = await this.page.title();
|
|
850
|
+
const snapshot = await this.page.accessibility.snapshot();
|
|
851
|
+
const accessibilityTree = this.simplifyAccessibilityTree(snapshot);
|
|
852
|
+
const interactiveElements = await this.page.evaluate(() => {
|
|
853
|
+
const elements = [];
|
|
854
|
+
document.querySelectorAll('button, [role="button"], input[type="button"], input[type="submit"]').forEach((el) => {
|
|
855
|
+
const text = el.textContent?.trim() || el.value || "";
|
|
856
|
+
if (text) {
|
|
857
|
+
elements.push({
|
|
858
|
+
role: "button",
|
|
859
|
+
name: text,
|
|
860
|
+
tag: el.tagName.toLowerCase()
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
document.querySelectorAll("a[href]").forEach((el) => {
|
|
865
|
+
const text = el.textContent?.trim() || "";
|
|
866
|
+
if (text) {
|
|
867
|
+
elements.push({
|
|
868
|
+
role: "link",
|
|
869
|
+
name: text,
|
|
870
|
+
tag: "a"
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
document.querySelectorAll('input:not([type="hidden"]), textarea, select').forEach((el) => {
|
|
875
|
+
const input = el;
|
|
876
|
+
const label = document.querySelector(`label[for="${input.id}"]`)?.textContent?.trim() || input.placeholder || input.name || input.id;
|
|
877
|
+
if (label) {
|
|
878
|
+
elements.push({
|
|
879
|
+
role: "input",
|
|
880
|
+
name: label,
|
|
881
|
+
tag: input.tagName.toLowerCase()
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
});
|
|
885
|
+
return elements.slice(0, 50);
|
|
886
|
+
});
|
|
887
|
+
return {
|
|
888
|
+
url,
|
|
889
|
+
title,
|
|
890
|
+
accessibilityTree,
|
|
891
|
+
interactiveElements
|
|
892
|
+
};
|
|
893
|
+
} catch (error) {
|
|
894
|
+
return {
|
|
895
|
+
url: this.page.url(),
|
|
896
|
+
title: await this.page.title().catch(() => "Unknown"),
|
|
897
|
+
accessibilityTree: "Unable to capture",
|
|
898
|
+
interactiveElements: []
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Simplify accessibility tree for AI consumption
|
|
904
|
+
*/
|
|
905
|
+
simplifyAccessibilityTree(node, depth = 0, maxDepth = 3) {
|
|
906
|
+
if (!node || depth > maxDepth) return "";
|
|
907
|
+
let result = "";
|
|
908
|
+
const indent = " ".repeat(depth);
|
|
909
|
+
if (node.role) {
|
|
910
|
+
const name = node.name ? ` "${node.name}"` : "";
|
|
911
|
+
result += `${indent}- ${node.role}${name}
|
|
912
|
+
`;
|
|
913
|
+
}
|
|
914
|
+
if (node.children && depth < maxDepth) {
|
|
915
|
+
for (const child of node.children.slice(0, 10)) {
|
|
916
|
+
result += this.simplifyAccessibilityTree(child, depth + 1, maxDepth);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
return result;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
// src/generator/code-generator.ts
|
|
924
|
+
var fs3 = __toESM(require("fs"));
|
|
925
|
+
var path2 = __toESM(require("path"));
|
|
926
|
+
var CodeGenerator = class {
|
|
927
|
+
config;
|
|
928
|
+
constructor(config) {
|
|
929
|
+
this.config = config;
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Generate Playwright test code from actions
|
|
933
|
+
*/
|
|
934
|
+
generateCode(interpretations) {
|
|
935
|
+
const lines = [];
|
|
936
|
+
lines.push("import { test, expect } from '@playwright/test';");
|
|
937
|
+
lines.push("");
|
|
938
|
+
if (this.config.disableSSL) {
|
|
939
|
+
lines.push("// Note: SSL certificate validation is disabled (ignoreHTTPSErrors: true)");
|
|
940
|
+
lines.push("// This is configured in playwright.config.ts");
|
|
941
|
+
lines.push("");
|
|
942
|
+
}
|
|
943
|
+
lines.push("test.describe('Generated E2E Test', () => {");
|
|
944
|
+
lines.push(" test.beforeEach(async ({ page }) => {");
|
|
945
|
+
lines.push(` // Navigate to base URL`);
|
|
946
|
+
lines.push(` await page.goto('${this.config.url}');`);
|
|
947
|
+
lines.push(" });");
|
|
948
|
+
lines.push("");
|
|
949
|
+
lines.push(" test('should execute test actions', async ({ page }) => {");
|
|
950
|
+
for (const interpretation of interpretations) {
|
|
951
|
+
const { command, actions } = interpretation;
|
|
952
|
+
lines.push("");
|
|
953
|
+
lines.push(` // ${command.actionType}: ${command.description}`);
|
|
954
|
+
for (const action of actions) {
|
|
955
|
+
const code = this.generateActionCode(action);
|
|
956
|
+
lines.push(` ${code}`);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
lines.push(" });");
|
|
960
|
+
lines.push("});");
|
|
961
|
+
lines.push("");
|
|
962
|
+
return lines.join("\n");
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Generate code for a single action
|
|
966
|
+
*/
|
|
967
|
+
generateActionCode(action) {
|
|
968
|
+
const timeout = action.timeout ?? this.config.timeout ?? 3e4;
|
|
969
|
+
switch (action.action) {
|
|
970
|
+
case "click":
|
|
971
|
+
return `await page.locator('${this.escapeString(action.selector)}').click({ timeout: ${timeout} });`;
|
|
972
|
+
case "type":
|
|
973
|
+
return `await page.locator('${this.escapeString(action.selector)}').fill('${this.escapeString(action.value)}', { timeout: ${timeout} });`;
|
|
974
|
+
case "navigate": {
|
|
975
|
+
const url = action.value.startsWith("http") ? action.value : `\${baseUrl}${action.value}`;
|
|
976
|
+
return `await page.goto('${this.escapeString(url)}', { timeout: ${timeout} });`;
|
|
977
|
+
}
|
|
978
|
+
case "wait": {
|
|
979
|
+
if (action.condition === "attached" && action.selector === "body") {
|
|
980
|
+
return `await page.waitForTimeout(${timeout});`;
|
|
981
|
+
}
|
|
982
|
+
const state2 = action.condition ?? "visible";
|
|
983
|
+
return `await page.locator('${this.escapeString(action.selector)}').waitFor({ state: '${state2}', timeout: ${timeout} });`;
|
|
984
|
+
}
|
|
985
|
+
case "expect":
|
|
986
|
+
return this.generateAssertionCode(action, timeout);
|
|
987
|
+
case "select":
|
|
988
|
+
return `await page.locator('${this.escapeString(action.selector)}').selectOption('${this.escapeString(action.value)}', { timeout: ${timeout} });`;
|
|
989
|
+
case "hover":
|
|
990
|
+
return `await page.locator('${this.escapeString(action.selector)}').hover({ timeout: ${timeout} });`;
|
|
991
|
+
case "press":
|
|
992
|
+
return `await page.keyboard.press('${this.escapeString(action.value)}');`;
|
|
993
|
+
default:
|
|
994
|
+
return `// Unsupported action: ${action.action}`;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Generate assertion code
|
|
999
|
+
*/
|
|
1000
|
+
generateAssertionCode(action, timeout) {
|
|
1001
|
+
const locator = `page.locator('${this.escapeString(action.selector)}')`;
|
|
1002
|
+
switch (action.assertionType) {
|
|
1003
|
+
case "visible":
|
|
1004
|
+
return `await expect(${locator}).toBeVisible({ timeout: ${timeout} });`;
|
|
1005
|
+
case "hidden":
|
|
1006
|
+
return `await expect(${locator}).toBeHidden({ timeout: ${timeout} });`;
|
|
1007
|
+
case "text":
|
|
1008
|
+
return `await expect(${locator}).toContainText('${this.escapeString(action.expected)}', { timeout: ${timeout} });`;
|
|
1009
|
+
case "value":
|
|
1010
|
+
return `await expect(${locator}).toHaveValue('${this.escapeString(action.expected)}', { timeout: ${timeout} });`;
|
|
1011
|
+
case "enabled":
|
|
1012
|
+
return `await expect(${locator}).toBeEnabled({ timeout: ${timeout} });`;
|
|
1013
|
+
case "disabled":
|
|
1014
|
+
return `await expect(${locator}).toBeDisabled({ timeout: ${timeout} });`;
|
|
1015
|
+
default:
|
|
1016
|
+
return `await expect(${locator}).toBeVisible({ timeout: ${timeout} });`;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Escape string for code generation
|
|
1021
|
+
*/
|
|
1022
|
+
escapeString(str) {
|
|
1023
|
+
return str.replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r");
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Generate and save test file
|
|
1027
|
+
*/
|
|
1028
|
+
async saveTestFile(interpretations, filename) {
|
|
1029
|
+
const code = this.generateCode(interpretations);
|
|
1030
|
+
const outputDir = this.config.outputDir ?? "generated-tests";
|
|
1031
|
+
if (!fs3.existsSync(outputDir)) {
|
|
1032
|
+
fs3.mkdirSync(outputDir, { recursive: true });
|
|
1033
|
+
}
|
|
1034
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
1035
|
+
const finalFilename = filename ?? `test-${timestamp}.spec.ts`;
|
|
1036
|
+
const filePath = path2.join(outputDir, finalFilename);
|
|
1037
|
+
fs3.writeFileSync(filePath, code, "utf-8");
|
|
1038
|
+
return filePath;
|
|
1039
|
+
}
|
|
1040
|
+
/**
|
|
1041
|
+
* Generate Playwright config file
|
|
1042
|
+
*/
|
|
1043
|
+
generatePlaywrightConfig() {
|
|
1044
|
+
const useConfig = [
|
|
1045
|
+
` baseURL: '${this.config.url}',`,
|
|
1046
|
+
` trace: 'on-first-retry',`,
|
|
1047
|
+
` screenshot: 'only-on-failure',`
|
|
1048
|
+
];
|
|
1049
|
+
if (this.config.disableSSL) {
|
|
1050
|
+
useConfig.push(` ignoreHTTPSErrors: true, // SSL certificate errors disabled`);
|
|
1051
|
+
}
|
|
1052
|
+
return `import { defineConfig, devices } from '@playwright/test';
|
|
1053
|
+
|
|
1054
|
+
export default defineConfig({
|
|
1055
|
+
testDir: './generated-tests',
|
|
1056
|
+
fullyParallel: true,
|
|
1057
|
+
forbidOnly: !!process.env.CI,
|
|
1058
|
+
retries: process.env.CI ? 2 : 0,
|
|
1059
|
+
workers: process.env.CI ? 1 : undefined,
|
|
1060
|
+
reporter: 'html',
|
|
1061
|
+
use: {
|
|
1062
|
+
${useConfig.join("\n")}
|
|
1063
|
+
},
|
|
1064
|
+
projects: [
|
|
1065
|
+
{
|
|
1066
|
+
name: 'chromium',
|
|
1067
|
+
use: { ...devices['Desktop Chrome'] },
|
|
1068
|
+
},
|
|
1069
|
+
],
|
|
1070
|
+
});
|
|
1071
|
+
`;
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Save Playwright config file
|
|
1075
|
+
*/
|
|
1076
|
+
async savePlaywrightConfig() {
|
|
1077
|
+
const config = this.generatePlaywrightConfig();
|
|
1078
|
+
const filePath = "playwright.config.ts";
|
|
1079
|
+
if (!fs3.existsSync(filePath)) {
|
|
1080
|
+
fs3.writeFileSync(filePath, config, "utf-8");
|
|
1081
|
+
}
|
|
1082
|
+
return filePath;
|
|
1083
|
+
}
|
|
1084
|
+
};
|
|
1085
|
+
|
|
1086
|
+
// src/utils/browser-installer.ts
|
|
1087
|
+
var import_child_process = require("child_process");
|
|
1088
|
+
var import_chalk = __toESM(require("chalk"));
|
|
1089
|
+
function isMissingBrowserError(error) {
|
|
1090
|
+
const message = error.message;
|
|
1091
|
+
return message.includes("Executable doesn't exist at") || message.includes("Looks like Playwright") || message.includes("npx playwright install");
|
|
1092
|
+
}
|
|
1093
|
+
function extractBrowserFromError(error) {
|
|
1094
|
+
const message = error.message;
|
|
1095
|
+
if (message.includes("chromium")) return "chromium";
|
|
1096
|
+
if (message.includes("firefox")) return "firefox";
|
|
1097
|
+
if (message.includes("webkit")) return "webkit";
|
|
1098
|
+
return null;
|
|
1099
|
+
}
|
|
1100
|
+
async function installPlaywrightBrowser(browser) {
|
|
1101
|
+
try {
|
|
1102
|
+
console.log(import_chalk.default.yellow(`
|
|
1103
|
+
\u26A0\uFE0F Browser ${browser} not found. Installing automatically...
|
|
1104
|
+
`));
|
|
1105
|
+
(0, import_child_process.execSync)(`npx playwright install ${browser}`, {
|
|
1106
|
+
stdio: "inherit",
|
|
1107
|
+
cwd: process.cwd()
|
|
1108
|
+
});
|
|
1109
|
+
console.log(import_chalk.default.green(`
|
|
1110
|
+
\u2713 Browser ${browser} installed successfully!
|
|
1111
|
+
`));
|
|
1112
|
+
return true;
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
console.error(import_chalk.default.red(`
|
|
1115
|
+
\u2717 Failed to install browser: ${error.message}
|
|
1116
|
+
`));
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
async function handleBrowserInstallation(error, browserType) {
|
|
1121
|
+
if (!isMissingBrowserError(error)) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
const browser = extractBrowserFromError(error) || browserType;
|
|
1125
|
+
return await installPlaywrightBrowser(browser);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// src/runner/test-runner.ts
|
|
1129
|
+
var TestRunner = class {
|
|
1130
|
+
config;
|
|
1131
|
+
interpreter;
|
|
1132
|
+
cache;
|
|
1133
|
+
executor = null;
|
|
1134
|
+
generator;
|
|
1135
|
+
interpretations = [];
|
|
1136
|
+
constructor(config) {
|
|
1137
|
+
this.config = config;
|
|
1138
|
+
const globalConfig2 = getConfig();
|
|
1139
|
+
const aiProvider = config.aiProvider || globalConfig2.aiProvider;
|
|
1140
|
+
let apiKey = config.apiKey;
|
|
1141
|
+
if (!apiKey) {
|
|
1142
|
+
apiKey = aiProvider === "claude" ? globalConfig2.anthropicApiKey : globalConfig2.googleApiKey;
|
|
1143
|
+
}
|
|
1144
|
+
if (!apiKey) {
|
|
1145
|
+
throw new Error(`API key not found for provider: ${aiProvider}`);
|
|
1146
|
+
}
|
|
1147
|
+
this.interpreter = new Interpreter(aiProvider, apiKey);
|
|
1148
|
+
this.cache = new CacheManager(
|
|
1149
|
+
globalConfig2.cache.filePath,
|
|
1150
|
+
globalConfig2.cache.expiryDays
|
|
1151
|
+
);
|
|
1152
|
+
this.generator = new CodeGenerator(config);
|
|
1153
|
+
}
|
|
1154
|
+
/**
|
|
1155
|
+
* Process a single command (interpret and cache) with retry logic
|
|
1156
|
+
*/
|
|
1157
|
+
async processCommand(command) {
|
|
1158
|
+
const cacheEnabled = this.config.cacheEnabled ?? true;
|
|
1159
|
+
const aiProvider = this.config.aiProvider || getConfig().aiProvider;
|
|
1160
|
+
if (cacheEnabled) {
|
|
1161
|
+
const cached = this.cache.get(command);
|
|
1162
|
+
if (cached) {
|
|
1163
|
+
console.log(import_chalk2.default.gray(` \u2713 Using cached interpretation`));
|
|
1164
|
+
return createInterpretationResult(command, cached, true);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
const displayProvider = aiProvider.toUpperCase();
|
|
1168
|
+
const maxInterpretationRetries = 3;
|
|
1169
|
+
let lastError = null;
|
|
1170
|
+
for (let attempt = 0; attempt < maxInterpretationRetries; attempt++) {
|
|
1171
|
+
try {
|
|
1172
|
+
if (attempt === 0) {
|
|
1173
|
+
console.log(import_chalk2.default.blue(` \u2192 Interpreting with ${displayProvider}...`));
|
|
1174
|
+
} else {
|
|
1175
|
+
console.log(import_chalk2.default.yellow(` \u27F3 Interpretation retry ${attempt}/${maxInterpretationRetries - 1}...`));
|
|
1176
|
+
}
|
|
1177
|
+
const actions = await this.interpreter.interpret(command);
|
|
1178
|
+
if (cacheEnabled) {
|
|
1179
|
+
this.cache.set(command, actions);
|
|
1180
|
+
}
|
|
1181
|
+
return createInterpretationResult(command, actions, false);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
lastError = error;
|
|
1184
|
+
console.log(import_chalk2.default.yellow(` \u26A0 Interpretation failed: ${lastError.message}`));
|
|
1185
|
+
if (attempt < maxInterpretationRetries - 1) {
|
|
1186
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3 * (attempt + 1)));
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
throw new Error(`Failed to interpret after ${maxInterpretationRetries} attempts: ${lastError?.message}`);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Interpret command with retry logic and page context
|
|
1194
|
+
*/
|
|
1195
|
+
async interpretWithRetry(command, pageContext) {
|
|
1196
|
+
const aiProvider = this.config.aiProvider || getConfig().aiProvider;
|
|
1197
|
+
const displayProvider = aiProvider.toUpperCase();
|
|
1198
|
+
const maxInterpretationRetries = 3;
|
|
1199
|
+
let lastError = null;
|
|
1200
|
+
for (let attempt = 0; attempt < maxInterpretationRetries; attempt++) {
|
|
1201
|
+
try {
|
|
1202
|
+
if (attempt > 0) {
|
|
1203
|
+
console.log(import_chalk2.default.yellow(` \u27F3 AI retry ${attempt}/${maxInterpretationRetries - 1} (${displayProvider})...`));
|
|
1204
|
+
}
|
|
1205
|
+
const actions = await this.interpreter.interpret(command, pageContext);
|
|
1206
|
+
return actions;
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
lastError = error;
|
|
1209
|
+
console.log(import_chalk2.default.yellow(` \u26A0 ${displayProvider} failed: ${lastError.message}`));
|
|
1210
|
+
if (attempt < maxInterpretationRetries - 1) {
|
|
1211
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3 * (attempt + 1)));
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
throw new Error(`${displayProvider} failed after ${maxInterpretationRetries} attempts: ${lastError?.message}`);
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Process all commands
|
|
1219
|
+
*/
|
|
1220
|
+
async processCommands(commands) {
|
|
1221
|
+
console.log(import_chalk2.default.bold("\n\u{1F4DD} Processing commands...\n"));
|
|
1222
|
+
const interpretations = [];
|
|
1223
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1224
|
+
const command = commands[i];
|
|
1225
|
+
console.log(import_chalk2.default.cyan(`[${i + 1}/${commands.length}] ${command.actionType}: ${command.description}`));
|
|
1226
|
+
try {
|
|
1227
|
+
const interpretation = await this.processCommand(command);
|
|
1228
|
+
interpretations.push(interpretation);
|
|
1229
|
+
console.log(import_chalk2.default.green(` \u2713 ${interpretation.actions.length} action(s) generated
|
|
1230
|
+
`));
|
|
1231
|
+
} catch (error) {
|
|
1232
|
+
console.error(import_chalk2.default.red(` \u2717 Failed: ${error.message}
|
|
1233
|
+
`));
|
|
1234
|
+
throw error;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
this.interpretations = interpretations;
|
|
1238
|
+
return interpretations;
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* Execute tests
|
|
1242
|
+
*/
|
|
1243
|
+
async execute(commands) {
|
|
1244
|
+
const startTime = Date.now();
|
|
1245
|
+
let actionsExecuted = 0;
|
|
1246
|
+
let browserInstalled = false;
|
|
1247
|
+
try {
|
|
1248
|
+
console.log(import_chalk2.default.bold("\u{1F680} Launching browser...\n"));
|
|
1249
|
+
this.executor = new PlaywrightExecutor(this.config);
|
|
1250
|
+
try {
|
|
1251
|
+
await this.executor.initialize();
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
const installed = await handleBrowserInstallation(
|
|
1254
|
+
error,
|
|
1255
|
+
this.config.browser || "chromium"
|
|
1256
|
+
);
|
|
1257
|
+
if (installed) {
|
|
1258
|
+
browserInstalled = true;
|
|
1259
|
+
console.log(import_chalk2.default.blue("Retrying browser launch...\n"));
|
|
1260
|
+
await this.executor.initialize();
|
|
1261
|
+
} else {
|
|
1262
|
+
throw error;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
console.log(import_chalk2.default.bold("\u25B6\uFE0F Executing tests...\n"));
|
|
1266
|
+
for (let i = 0; i < commands.length; i++) {
|
|
1267
|
+
const command = commands[i];
|
|
1268
|
+
console.log(import_chalk2.default.cyan(`
|
|
1269
|
+
[${i + 1}/${commands.length}] ${command.actionType}: ${command.description}`));
|
|
1270
|
+
let success = false;
|
|
1271
|
+
let lastError = null;
|
|
1272
|
+
const maxRetries = 5;
|
|
1273
|
+
for (let attempt = 0; attempt < maxRetries && !success; attempt++) {
|
|
1274
|
+
try {
|
|
1275
|
+
const pageContext = await this.executor.getPageContext();
|
|
1276
|
+
if (attempt === 0) {
|
|
1277
|
+
console.log(import_chalk2.default.blue(` \u2192 Interpreting with context...`));
|
|
1278
|
+
console.log(import_chalk2.default.gray(` Page: ${pageContext.url}`));
|
|
1279
|
+
console.log(import_chalk2.default.gray(` Available elements: ${pageContext.interactiveElements.length}`));
|
|
1280
|
+
} else {
|
|
1281
|
+
console.log(import_chalk2.default.yellow(` \u27F3 Retry ${attempt}/${maxRetries - 1} - Re-analyzing page...`));
|
|
1282
|
+
console.log(import_chalk2.default.gray(` Page: ${pageContext.url}`));
|
|
1283
|
+
console.log(import_chalk2.default.gray(` Available elements: ${pageContext.interactiveElements.length}`));
|
|
1284
|
+
}
|
|
1285
|
+
let actions;
|
|
1286
|
+
if (attempt === 0 && this.config.cacheEnabled) {
|
|
1287
|
+
const cached = this.cache.get(command);
|
|
1288
|
+
if (cached) {
|
|
1289
|
+
console.log(import_chalk2.default.gray(` \u2713 Using cached interpretation`));
|
|
1290
|
+
actions = cached;
|
|
1291
|
+
} else {
|
|
1292
|
+
actions = await this.interpretWithRetry(command, pageContext);
|
|
1293
|
+
this.cache.set(command, actions);
|
|
1294
|
+
}
|
|
1295
|
+
} else {
|
|
1296
|
+
actions = await this.interpretWithRetry(command, pageContext);
|
|
1297
|
+
if (this.config.cacheEnabled) {
|
|
1298
|
+
this.cache.set(command, actions);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
console.log(import_chalk2.default.gray(` \u2713 Generated ${actions.length} action(s)`));
|
|
1302
|
+
for (const action of actions) {
|
|
1303
|
+
await this.executor.executeAction(action);
|
|
1304
|
+
actionsExecuted++;
|
|
1305
|
+
console.log(import_chalk2.default.gray(` \u2713 ${action.action}${action.selector ? ` (${action.selector})` : ""}`));
|
|
1306
|
+
}
|
|
1307
|
+
const interpretation = createInterpretationResult(command, actions, false);
|
|
1308
|
+
this.interpretations.push(interpretation);
|
|
1309
|
+
success = true;
|
|
1310
|
+
console.log(import_chalk2.default.green(` \u2713 Success
|
|
1311
|
+
`));
|
|
1312
|
+
} catch (error) {
|
|
1313
|
+
lastError = error;
|
|
1314
|
+
if (attempt < maxRetries - 1) {
|
|
1315
|
+
console.log(import_chalk2.default.yellow(` \u26A0 Failed: ${lastError.message}`));
|
|
1316
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3 * (attempt + 1)));
|
|
1317
|
+
} else {
|
|
1318
|
+
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
console.log(import_chalk2.default.bold("\n\u{1F4C4} Generating Playwright code...\n"));
|
|
1324
|
+
const generatedFile = await this.generator.saveTestFile(this.interpretations);
|
|
1325
|
+
console.log(import_chalk2.default.green(`\u2713 Test file saved: ${generatedFile}`));
|
|
1326
|
+
const duration = Date.now() - startTime;
|
|
1327
|
+
return {
|
|
1328
|
+
success: true,
|
|
1329
|
+
duration,
|
|
1330
|
+
actionsExecuted,
|
|
1331
|
+
generatedFile
|
|
1332
|
+
};
|
|
1333
|
+
} catch (error) {
|
|
1334
|
+
const duration = Date.now() - startTime;
|
|
1335
|
+
return {
|
|
1336
|
+
success: false,
|
|
1337
|
+
error: error.message,
|
|
1338
|
+
duration,
|
|
1339
|
+
actionsExecuted
|
|
1340
|
+
};
|
|
1341
|
+
} finally {
|
|
1342
|
+
if (this.executor) {
|
|
1343
|
+
await this.executor.close();
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Generate code only (no execution)
|
|
1349
|
+
*/
|
|
1350
|
+
async generateOnly(commands) {
|
|
1351
|
+
console.log(import_chalk2.default.bold("\u{1F4DD} Processing commands for code generation...\n"));
|
|
1352
|
+
const interpretations = await this.processCommands(commands);
|
|
1353
|
+
console.log(import_chalk2.default.bold("\n\u{1F4C4} Generating Playwright code...\n"));
|
|
1354
|
+
const generatedFile = await this.generator.saveTestFile(interpretations);
|
|
1355
|
+
console.log(import_chalk2.default.green(`\u2713 Test file saved: ${generatedFile}`));
|
|
1356
|
+
return generatedFile;
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Get interpretations
|
|
1360
|
+
*/
|
|
1361
|
+
getInterpretations() {
|
|
1362
|
+
return this.interpretations;
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
// src/cli.ts
|
|
1367
|
+
function showHelp() {
|
|
1368
|
+
console.log(import_chalk3.default.bold("\n\u26A1 Atezca - AI-Powered E2E Testing\n"));
|
|
1369
|
+
console.log("Usage:");
|
|
1370
|
+
console.log(" az run <file> Execute test file");
|
|
1371
|
+
console.log(" az generate <file> Generate Playwright code without execution");
|
|
1372
|
+
console.log(" az init Create .atezcarc config file");
|
|
1373
|
+
console.log(" az cache clear Clear interpretation cache");
|
|
1374
|
+
console.log(" az cache stats Show cache statistics");
|
|
1375
|
+
console.log(" az help Show this help message");
|
|
1376
|
+
console.log("");
|
|
1377
|
+
console.log("Examples:");
|
|
1378
|
+
console.log(" az run test.spec.js");
|
|
1379
|
+
console.log(" az generate test.spec.js");
|
|
1380
|
+
console.log(" az init");
|
|
1381
|
+
console.log("");
|
|
1382
|
+
}
|
|
1383
|
+
async function runTest(filepath) {
|
|
1384
|
+
try {
|
|
1385
|
+
console.log(import_chalk3.default.blue(`Loading test file: ${filepath}
|
|
1386
|
+
`));
|
|
1387
|
+
if (!fs4.existsSync(filepath)) {
|
|
1388
|
+
console.error(import_chalk3.default.red(`Error: File not found: ${filepath}`));
|
|
1389
|
+
process.exit(1);
|
|
1390
|
+
}
|
|
1391
|
+
const absPath = path3.resolve(filepath);
|
|
1392
|
+
await import(`file://${absPath}`);
|
|
1393
|
+
let state2 = getState();
|
|
1394
|
+
if (!state2.config || state2.commands.length === 0) {
|
|
1395
|
+
try {
|
|
1396
|
+
const currentDir = path3.dirname(new URL(importMetaUrl).pathname);
|
|
1397
|
+
const indexModule = await import(path3.join(currentDir, "index.js"));
|
|
1398
|
+
const altState = indexModule.getState();
|
|
1399
|
+
if (altState.config) {
|
|
1400
|
+
state2 = altState;
|
|
1401
|
+
}
|
|
1402
|
+
} catch (e) {
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
if (!state2.config) {
|
|
1406
|
+
console.error(import_chalk3.default.red("Error: az.setup() was not called in the test file"));
|
|
1407
|
+
process.exit(1);
|
|
1408
|
+
}
|
|
1409
|
+
if (state2.commands.length === 0) {
|
|
1410
|
+
console.error(import_chalk3.default.yellow("Warning: No test commands found"));
|
|
1411
|
+
process.exit(0);
|
|
1412
|
+
}
|
|
1413
|
+
console.log(import_chalk3.default.bold(`Found ${state2.commands.length} test command(s)
|
|
1414
|
+
`));
|
|
1415
|
+
const runner = new TestRunner(state2.config);
|
|
1416
|
+
const result = await runner.execute(state2.commands);
|
|
1417
|
+
console.log(import_chalk3.default.bold("\n" + "=".repeat(50)));
|
|
1418
|
+
if (result.success) {
|
|
1419
|
+
console.log(import_chalk3.default.green.bold("\n\u2713 Test passed!\n"));
|
|
1420
|
+
console.log(import_chalk3.default.gray(`Duration: ${(result.duration / 1e3).toFixed(2)}s`));
|
|
1421
|
+
console.log(import_chalk3.default.gray(`Actions executed: ${result.actionsExecuted}`));
|
|
1422
|
+
if (result.generatedFile) {
|
|
1423
|
+
console.log(import_chalk3.default.gray(`Generated file: ${result.generatedFile}`));
|
|
1424
|
+
}
|
|
1425
|
+
process.exit(0);
|
|
1426
|
+
} else {
|
|
1427
|
+
console.log(import_chalk3.default.red.bold("\n\u2717 Test failed!\n"));
|
|
1428
|
+
console.log(import_chalk3.default.red(`Error: ${result.error}`));
|
|
1429
|
+
console.log(import_chalk3.default.gray(`Duration: ${(result.duration / 1e3).toFixed(2)}s`));
|
|
1430
|
+
console.log(import_chalk3.default.gray(`Actions executed: ${result.actionsExecuted}`));
|
|
1431
|
+
process.exit(1);
|
|
1432
|
+
}
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
console.error(import_chalk3.default.red("\n\u2717 Error:"), error.message);
|
|
1435
|
+
process.exit(1);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
async function generateTest(filepath) {
|
|
1439
|
+
try {
|
|
1440
|
+
console.log(import_chalk3.default.blue(`Loading test file: ${filepath}
|
|
1441
|
+
`));
|
|
1442
|
+
if (!fs4.existsSync(filepath)) {
|
|
1443
|
+
console.error(import_chalk3.default.red(`Error: File not found: ${filepath}`));
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
}
|
|
1446
|
+
const absPath = path3.resolve(filepath);
|
|
1447
|
+
await import(`file://${absPath}`);
|
|
1448
|
+
let state2 = getState();
|
|
1449
|
+
if (!state2.config || state2.commands.length === 0) {
|
|
1450
|
+
try {
|
|
1451
|
+
const currentDir = path3.dirname(new URL(importMetaUrl).pathname);
|
|
1452
|
+
const indexModule = await import(path3.join(currentDir, "index.js"));
|
|
1453
|
+
const altState = indexModule.getState();
|
|
1454
|
+
if (altState.config) {
|
|
1455
|
+
state2 = altState;
|
|
1456
|
+
}
|
|
1457
|
+
} catch (e) {
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
if (!state2.config) {
|
|
1461
|
+
console.error(import_chalk3.default.red("Error: az.setup() was not called in the test file"));
|
|
1462
|
+
process.exit(1);
|
|
1463
|
+
}
|
|
1464
|
+
if (state2.commands.length === 0) {
|
|
1465
|
+
console.error(import_chalk3.default.yellow("Warning: No test commands found"));
|
|
1466
|
+
process.exit(0);
|
|
1467
|
+
}
|
|
1468
|
+
console.log(import_chalk3.default.bold(`Found ${state2.commands.length} test command(s)
|
|
1469
|
+
`));
|
|
1470
|
+
const runner = new TestRunner(state2.config);
|
|
1471
|
+
const generatedFile = await runner.generateOnly(state2.commands);
|
|
1472
|
+
console.log(import_chalk3.default.green.bold("\n\u2713 Code generation complete!\n"));
|
|
1473
|
+
console.log(import_chalk3.default.gray(`Generated file: ${generatedFile}`));
|
|
1474
|
+
console.log(import_chalk3.default.gray("\nRun with: npx playwright test"));
|
|
1475
|
+
process.exit(0);
|
|
1476
|
+
} catch (error) {
|
|
1477
|
+
console.error(import_chalk3.default.red("\n\u2717 Error:"), error.message);
|
|
1478
|
+
process.exit(1);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
function initConfig() {
|
|
1482
|
+
const filepath = ".atezcarc";
|
|
1483
|
+
if (fs4.existsSync(filepath)) {
|
|
1484
|
+
console.log(import_chalk3.default.yellow(`Warning: ${filepath} already exists`));
|
|
1485
|
+
process.exit(0);
|
|
1486
|
+
}
|
|
1487
|
+
ConfigLoader.createDefaultConfig(filepath);
|
|
1488
|
+
console.log(import_chalk3.default.green(`\u2713 Created ${filepath}`));
|
|
1489
|
+
console.log(import_chalk3.default.gray("\nDon't forget to set your ANTHROPIC_API_KEY in .env or .atezcarc"));
|
|
1490
|
+
process.exit(0);
|
|
1491
|
+
}
|
|
1492
|
+
function clearCache() {
|
|
1493
|
+
const cache = new CacheManager();
|
|
1494
|
+
cache.clear();
|
|
1495
|
+
console.log(import_chalk3.default.green("\u2713 Cache cleared"));
|
|
1496
|
+
process.exit(0);
|
|
1497
|
+
}
|
|
1498
|
+
function showCacheStats() {
|
|
1499
|
+
const cache = new CacheManager();
|
|
1500
|
+
const stats = cache.getStats();
|
|
1501
|
+
console.log(import_chalk3.default.bold("\nCache Statistics:\n"));
|
|
1502
|
+
console.log(import_chalk3.default.gray(`Entries: ${stats.size}`));
|
|
1503
|
+
if (stats.oldestEntry) {
|
|
1504
|
+
const oldest = new Date(stats.oldestEntry);
|
|
1505
|
+
console.log(import_chalk3.default.gray(`Oldest entry: ${oldest.toLocaleString()}`));
|
|
1506
|
+
}
|
|
1507
|
+
if (stats.newestEntry) {
|
|
1508
|
+
const newest = new Date(stats.newestEntry);
|
|
1509
|
+
console.log(import_chalk3.default.gray(`Newest entry: ${newest.toLocaleString()}`));
|
|
1510
|
+
}
|
|
1511
|
+
if (stats.size === 0) {
|
|
1512
|
+
console.log(import_chalk3.default.yellow("\nCache is empty"));
|
|
1513
|
+
}
|
|
1514
|
+
console.log("");
|
|
1515
|
+
process.exit(0);
|
|
1516
|
+
}
|
|
1517
|
+
async function main() {
|
|
1518
|
+
const args = process.argv.slice(2);
|
|
1519
|
+
if (args.length === 0 || args[0] === "help" || args[0] === "--help" || args[0] === "-h") {
|
|
1520
|
+
showHelp();
|
|
1521
|
+
process.exit(0);
|
|
1522
|
+
}
|
|
1523
|
+
const command = args[0];
|
|
1524
|
+
const subcommand = args[1];
|
|
1525
|
+
switch (command) {
|
|
1526
|
+
case "run":
|
|
1527
|
+
if (!subcommand) {
|
|
1528
|
+
console.error(import_chalk3.default.red("Error: Please specify a test file"));
|
|
1529
|
+
console.log("Usage: az run <file>");
|
|
1530
|
+
process.exit(1);
|
|
1531
|
+
}
|
|
1532
|
+
await runTest(subcommand);
|
|
1533
|
+
break;
|
|
1534
|
+
case "generate":
|
|
1535
|
+
if (!subcommand) {
|
|
1536
|
+
console.error(import_chalk3.default.red("Error: Please specify a test file"));
|
|
1537
|
+
console.log("Usage: az generate <file>");
|
|
1538
|
+
process.exit(1);
|
|
1539
|
+
}
|
|
1540
|
+
await generateTest(subcommand);
|
|
1541
|
+
break;
|
|
1542
|
+
case "init":
|
|
1543
|
+
initConfig();
|
|
1544
|
+
break;
|
|
1545
|
+
case "cache":
|
|
1546
|
+
if (subcommand === "clear") {
|
|
1547
|
+
clearCache();
|
|
1548
|
+
} else if (subcommand === "stats") {
|
|
1549
|
+
showCacheStats();
|
|
1550
|
+
} else {
|
|
1551
|
+
console.error(import_chalk3.default.red("Error: Unknown cache command"));
|
|
1552
|
+
console.log("Usage: az cache <clear|stats>");
|
|
1553
|
+
process.exit(1);
|
|
1554
|
+
}
|
|
1555
|
+
break;
|
|
1556
|
+
default:
|
|
1557
|
+
console.error(import_chalk3.default.red(`Error: Unknown command: ${command}`));
|
|
1558
|
+
showHelp();
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
main().catch((error) => {
|
|
1563
|
+
console.error(import_chalk3.default.red("Fatal error:"), error);
|
|
1564
|
+
process.exit(1);
|
|
1565
|
+
});
|
|
1566
|
+
//# sourceMappingURL=cli.js.map
|