intellitester 0.1.12

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 (89) hide show
  1. package/README.md +200 -0
  2. package/dist/chunk-35WJGNDA.cjs +136 -0
  3. package/dist/chunk-35WJGNDA.cjs.map +1 -0
  4. package/dist/chunk-4B54JUOP.js +234 -0
  5. package/dist/chunk-4B54JUOP.js.map +1 -0
  6. package/dist/chunk-5LFSLMQ7.js +2517 -0
  7. package/dist/chunk-5LFSLMQ7.js.map +1 -0
  8. package/dist/chunk-6PYKWWH5.js +63 -0
  9. package/dist/chunk-6PYKWWH5.js.map +1 -0
  10. package/dist/chunk-ARJYJVRM.cjs +302 -0
  11. package/dist/chunk-ARJYJVRM.cjs.map +1 -0
  12. package/dist/chunk-CN6HSJJX.js +133 -0
  13. package/dist/chunk-CN6HSJJX.js.map +1 -0
  14. package/dist/chunk-DE5UFTTG.js +31 -0
  15. package/dist/chunk-DE5UFTTG.js.map +1 -0
  16. package/dist/chunk-ECBA4GJ3.js +287 -0
  17. package/dist/chunk-ECBA4GJ3.js.map +1 -0
  18. package/dist/chunk-OFXNJXMV.cjs +237 -0
  19. package/dist/chunk-OFXNJXMV.cjs.map +1 -0
  20. package/dist/chunk-PAKODOH4.cjs +66 -0
  21. package/dist/chunk-PAKODOH4.cjs.map +1 -0
  22. package/dist/chunk-QMYM2TCH.cjs +36 -0
  23. package/dist/chunk-QMYM2TCH.cjs.map +1 -0
  24. package/dist/chunk-SAVY6D3X.js +125 -0
  25. package/dist/chunk-SAVY6D3X.js.map +1 -0
  26. package/dist/chunk-UUJXCHVT.cjs +128 -0
  27. package/dist/chunk-UUJXCHVT.cjs.map +1 -0
  28. package/dist/chunk-XWGUA67E.cjs +2552 -0
  29. package/dist/chunk-XWGUA67E.cjs.map +1 -0
  30. package/dist/cli/index.cjs +1985 -0
  31. package/dist/cli/index.cjs.map +1 -0
  32. package/dist/cli/index.d.cts +1 -0
  33. package/dist/cli/index.d.ts +1 -0
  34. package/dist/cli/index.js +1957 -0
  35. package/dist/cli/index.js.map +1 -0
  36. package/dist/core/cleanup/index.cjs +45 -0
  37. package/dist/core/cleanup/index.cjs.map +1 -0
  38. package/dist/core/cleanup/index.d.cts +117 -0
  39. package/dist/core/cleanup/index.d.ts +117 -0
  40. package/dist/core/cleanup/index.js +8 -0
  41. package/dist/core/cleanup/index.js.map +1 -0
  42. package/dist/index.cjs +110 -0
  43. package/dist/index.cjs.map +1 -0
  44. package/dist/index.d.cts +852 -0
  45. package/dist/index.d.ts +852 -0
  46. package/dist/index.js +9 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/integration/index.cjs +22 -0
  49. package/dist/integration/index.cjs.map +1 -0
  50. package/dist/integration/index.d.cts +42 -0
  51. package/dist/integration/index.d.ts +42 -0
  52. package/dist/integration/index.js +20 -0
  53. package/dist/integration/index.js.map +1 -0
  54. package/dist/providers/appwrite/index.cjs +16 -0
  55. package/dist/providers/appwrite/index.cjs.map +1 -0
  56. package/dist/providers/appwrite/index.d.cts +12 -0
  57. package/dist/providers/appwrite/index.d.ts +12 -0
  58. package/dist/providers/appwrite/index.js +3 -0
  59. package/dist/providers/appwrite/index.js.map +1 -0
  60. package/dist/providers/index.cjs +60 -0
  61. package/dist/providers/index.cjs.map +1 -0
  62. package/dist/providers/index.d.cts +13 -0
  63. package/dist/providers/index.d.ts +13 -0
  64. package/dist/providers/index.js +7 -0
  65. package/dist/providers/index.js.map +1 -0
  66. package/dist/providers/mysql/index.cjs +16 -0
  67. package/dist/providers/mysql/index.cjs.map +1 -0
  68. package/dist/providers/mysql/index.d.cts +14 -0
  69. package/dist/providers/mysql/index.d.ts +14 -0
  70. package/dist/providers/mysql/index.js +3 -0
  71. package/dist/providers/mysql/index.js.map +1 -0
  72. package/dist/providers/postgres/index.cjs +16 -0
  73. package/dist/providers/postgres/index.cjs.map +1 -0
  74. package/dist/providers/postgres/index.d.cts +10 -0
  75. package/dist/providers/postgres/index.d.ts +10 -0
  76. package/dist/providers/postgres/index.js +3 -0
  77. package/dist/providers/postgres/index.js.map +1 -0
  78. package/dist/providers/sqlite/index.cjs +16 -0
  79. package/dist/providers/sqlite/index.cjs.map +1 -0
  80. package/dist/providers/sqlite/index.d.cts +11 -0
  81. package/dist/providers/sqlite/index.d.ts +11 -0
  82. package/dist/providers/sqlite/index.js +3 -0
  83. package/dist/providers/sqlite/index.js.map +1 -0
  84. package/dist/types-LONNVTIF.d.cts +56 -0
  85. package/dist/types-l-ZaFKC-.d.ts +56 -0
  86. package/package.json +114 -0
  87. package/schemas/intellitester.config.schema.json +384 -0
  88. package/schemas/test.schema.json +517 -0
  89. package/schemas/workflow.schema.json +227 -0
@@ -0,0 +1,2517 @@
1
+ import { loadCleanupHandlers, executeCleanup, saveFailedCleanup } from './chunk-ECBA4GJ3.js';
2
+ import { z } from 'zod';
3
+ import fs2 from 'fs/promises';
4
+ import { parse } from 'yaml';
5
+ import crypto2 from 'crypto';
6
+ import path from 'path';
7
+ import { spawn } from 'child_process';
8
+ import { chromium, webkit, firefox } from 'playwright';
9
+ import prompts from 'prompts';
10
+ import { Client, Users, TablesDB, Storage, Teams } from 'node-appwrite';
11
+ import { Anthropic } from '@llamaindex/anthropic';
12
+ import { OpenAI } from '@llamaindex/openai';
13
+ import { Ollama } from '@llamaindex/ollama';
14
+ import { createServer } from 'http';
15
+
16
+ var nonEmptyString = z.string().trim().min(1, "Value cannot be empty");
17
+ var LocatorSchema = z.object({
18
+ description: z.string().trim().optional(),
19
+ testId: z.string().trim().optional(),
20
+ text: z.string().trim().optional(),
21
+ css: z.string().trim().optional(),
22
+ xpath: z.string().trim().optional(),
23
+ role: z.string().trim().optional(),
24
+ name: z.string().trim().optional()
25
+ }).refine(
26
+ (locator) => Boolean(
27
+ locator.description || locator.testId || locator.text || locator.css || locator.xpath || locator.role || locator.name
28
+ ),
29
+ { message: "Locator requires at least one selector or description" }
30
+ );
31
+ var navigateActionSchema = z.object({
32
+ type: z.literal("navigate"),
33
+ value: nonEmptyString
34
+ });
35
+ var tapActionSchema = z.object({
36
+ type: z.literal("tap"),
37
+ target: LocatorSchema
38
+ });
39
+ var inputActionSchema = z.object({
40
+ type: z.literal("input"),
41
+ target: LocatorSchema,
42
+ value: z.string()
43
+ });
44
+ var assertActionSchema = z.object({
45
+ type: z.literal("assert"),
46
+ target: LocatorSchema,
47
+ value: z.string().optional()
48
+ });
49
+ var waitActionSchema = z.object({
50
+ type: z.literal("wait"),
51
+ target: LocatorSchema.optional(),
52
+ timeout: z.number().int().positive().optional()
53
+ }).refine((action) => action.target || action.timeout, {
54
+ message: "wait requires a target or timeout"
55
+ });
56
+ var scrollActionSchema = z.object({
57
+ type: z.literal("scroll"),
58
+ target: LocatorSchema.optional(),
59
+ direction: z.enum(["up", "down"]).optional(),
60
+ amount: z.number().int().positive().optional()
61
+ });
62
+ var screenshotActionSchema = z.object({
63
+ type: z.literal("screenshot"),
64
+ name: z.string().optional()
65
+ });
66
+ var setVarActionSchema = z.object({
67
+ type: z.literal("setVar"),
68
+ name: nonEmptyString,
69
+ value: z.string().optional(),
70
+ from: z.enum(["response", "element", "email"]).optional(),
71
+ path: z.string().optional(),
72
+ pattern: z.string().optional()
73
+ });
74
+ var emailWaitForActionSchema = z.object({
75
+ type: z.literal("email.waitFor"),
76
+ mailbox: nonEmptyString,
77
+ timeout: z.number().int().positive().optional(),
78
+ subjectContains: z.string().optional()
79
+ });
80
+ var emailExtractCodeActionSchema = z.object({
81
+ type: z.literal("email.extractCode"),
82
+ saveTo: nonEmptyString,
83
+ pattern: z.string().optional()
84
+ });
85
+ var emailExtractLinkActionSchema = z.object({
86
+ type: z.literal("email.extractLink"),
87
+ saveTo: nonEmptyString,
88
+ pattern: z.string().optional()
89
+ });
90
+ var emailClearActionSchema = z.object({
91
+ type: z.literal("email.clear"),
92
+ mailbox: nonEmptyString
93
+ });
94
+ var appwriteVerifyEmailActionSchema = z.object({
95
+ type: z.literal("appwrite.verifyEmail")
96
+ });
97
+ var debugActionSchema = z.object({
98
+ type: z.literal("debug")
99
+ });
100
+ var ActionSchema = z.discriminatedUnion("type", [
101
+ navigateActionSchema,
102
+ tapActionSchema,
103
+ inputActionSchema,
104
+ assertActionSchema,
105
+ waitActionSchema,
106
+ scrollActionSchema,
107
+ screenshotActionSchema,
108
+ setVarActionSchema,
109
+ emailWaitForActionSchema,
110
+ emailExtractCodeActionSchema,
111
+ emailExtractLinkActionSchema,
112
+ emailClearActionSchema,
113
+ appwriteVerifyEmailActionSchema,
114
+ debugActionSchema
115
+ ]);
116
+ var defaultsSchema = z.object({
117
+ timeout: z.number().int().positive().optional(),
118
+ screenshots: z.enum(["on-failure", "always", "never"]).optional()
119
+ });
120
+ var webConfigSchema = z.object({
121
+ baseUrl: nonEmptyString.url().optional(),
122
+ browser: z.string().trim().optional(),
123
+ headless: z.boolean().optional(),
124
+ timeout: z.number().int().positive().optional()
125
+ });
126
+ var androidConfigSchema = z.object({
127
+ appId: z.string().trim().optional(),
128
+ device: z.string().trim().optional()
129
+ });
130
+ var iosConfigSchema = z.object({
131
+ bundleId: z.string().trim().optional(),
132
+ simulator: z.string().trim().optional()
133
+ });
134
+ var emailConfigSchema = z.object({
135
+ provider: z.literal("inbucket"),
136
+ endpoint: nonEmptyString.url().optional()
137
+ });
138
+ var appwriteConfigSchema = z.object({
139
+ endpoint: nonEmptyString.url(),
140
+ projectId: nonEmptyString,
141
+ apiKey: nonEmptyString,
142
+ cleanup: z.boolean().optional(),
143
+ cleanupOnFailure: z.boolean().optional()
144
+ });
145
+ var healingSchema = z.object({
146
+ enabled: z.boolean().optional(),
147
+ strategies: z.array(z.string().trim()).optional()
148
+ });
149
+ var webServerSchema = z.object({
150
+ // Option 1: Explicit command
151
+ command: nonEmptyString.optional(),
152
+ // Option 2: Auto-detect
153
+ auto: z.boolean().optional(),
154
+ // Option 3: Static directory
155
+ static: z.string().optional(),
156
+ // Required
157
+ url: nonEmptyString.url(),
158
+ port: z.number().int().positive().optional(),
159
+ reuseExistingServer: z.boolean().default(true),
160
+ timeout: z.number().int().positive().default(3e4),
161
+ cwd: z.string().optional()
162
+ }).refine((config) => config.command || config.auto || config.static, {
163
+ message: "WebServerConfig requires command, auto: true, or static directory"
164
+ });
165
+ var aiSourceSchema = z.object({
166
+ pagesDir: z.string().optional(),
167
+ componentsDir: z.string().optional(),
168
+ extensions: z.array(z.string()).default([".vue", ".astro", ".tsx", ".jsx", ".svelte"])
169
+ }).optional();
170
+ var aiConfigSchema = z.object({
171
+ provider: z.enum(["anthropic", "openai", "ollama"]),
172
+ model: nonEmptyString,
173
+ apiKey: z.string().trim().optional(),
174
+ baseUrl: z.string().trim().url().optional(),
175
+ temperature: z.number().min(0).max(2).default(0.2),
176
+ maxTokens: z.number().int().positive().default(4096),
177
+ source: aiSourceSchema
178
+ });
179
+ var cleanupDiscoverSchema = z.object({
180
+ enabled: z.boolean().default(true),
181
+ paths: z.array(z.string()).default(["./tests/cleanup"]),
182
+ pattern: z.string().default("**/*.ts")
183
+ }).optional();
184
+ var cleanupConfigSchema = z.object({
185
+ provider: z.string().optional(),
186
+ parallel: z.boolean().default(false),
187
+ retries: z.number().min(1).max(10).default(3),
188
+ types: z.record(z.string(), z.string()).optional(),
189
+ handlers: z.array(z.string()).optional(),
190
+ discover: cleanupDiscoverSchema
191
+ }).passthrough();
192
+ var platformsSchema = z.object({
193
+ web: webConfigSchema.optional(),
194
+ android: androidConfigSchema.optional(),
195
+ ios: iosConfigSchema.optional()
196
+ });
197
+ var TestConfigSchema = z.object({
198
+ defaults: defaultsSchema.optional(),
199
+ web: webConfigSchema.optional(),
200
+ android: androidConfigSchema.optional(),
201
+ ios: iosConfigSchema.optional(),
202
+ email: emailConfigSchema.optional(),
203
+ appwrite: appwriteConfigSchema.optional()
204
+ });
205
+ var TestDefinitionSchema = z.object({
206
+ name: nonEmptyString,
207
+ platform: z.enum(["web", "android", "ios"]),
208
+ variables: z.record(z.string(), z.string()).optional(),
209
+ config: TestConfigSchema.optional(),
210
+ steps: z.array(ActionSchema).min(1)
211
+ });
212
+ var IntellitesterConfigSchema = z.object({
213
+ defaults: defaultsSchema.optional(),
214
+ ai: aiConfigSchema.optional(),
215
+ platforms: platformsSchema.optional(),
216
+ healing: healingSchema.optional(),
217
+ email: emailConfigSchema.optional(),
218
+ appwrite: appwriteConfigSchema.optional(),
219
+ cleanup: cleanupConfigSchema.optional(),
220
+ webServer: webServerSchema.optional(),
221
+ secrets: z.record(z.string(), z.string().trim()).optional()
222
+ });
223
+ var nonEmptyString2 = z.string().trim().min(1, "Value cannot be empty");
224
+ var testReferenceSchema = z.object({
225
+ file: nonEmptyString2,
226
+ id: nonEmptyString2.optional(),
227
+ // Optional ID for referencing in variables
228
+ variables: z.record(z.string(), z.string()).optional()
229
+ // Override/inject variables
230
+ });
231
+ var workflowWebConfigSchema = z.object({
232
+ baseUrl: nonEmptyString2.url().optional(),
233
+ browser: z.enum(["chromium", "firefox", "webkit"]).optional(),
234
+ headless: z.boolean().optional()
235
+ });
236
+ var workflowAppwriteConfigSchema = z.object({
237
+ endpoint: nonEmptyString2.url(),
238
+ projectId: nonEmptyString2,
239
+ apiKey: nonEmptyString2,
240
+ cleanup: z.boolean().default(true),
241
+ // Backwards compatibility
242
+ cleanupOnFailure: z.boolean().default(true)
243
+ // Backwards compatibility
244
+ });
245
+ var workflowCleanupDiscoverSchema = z.object({
246
+ enabled: z.boolean().default(true),
247
+ paths: z.array(z.string()).default(["./tests/cleanup"]),
248
+ pattern: z.string().default("**/*.ts")
249
+ }).optional();
250
+ var workflowCleanupConfigSchema = z.object({
251
+ provider: z.string().optional(),
252
+ parallel: z.boolean().default(false),
253
+ retries: z.number().min(1).max(10).default(3),
254
+ types: z.record(z.string(), z.string()).optional(),
255
+ handlers: z.array(z.string()).optional(),
256
+ discover: workflowCleanupDiscoverSchema
257
+ }).passthrough();
258
+ var workflowWebServerSchema = z.object({
259
+ command: nonEmptyString2.optional(),
260
+ auto: z.boolean().optional(),
261
+ url: nonEmptyString2.url(),
262
+ reuseExistingServer: z.boolean().default(true),
263
+ timeout: z.number().int().positive().default(3e4)
264
+ });
265
+ var workflowConfigSchema = z.object({
266
+ web: workflowWebConfigSchema.optional(),
267
+ appwrite: workflowAppwriteConfigSchema.optional(),
268
+ cleanup: workflowCleanupConfigSchema.optional(),
269
+ webServer: workflowWebServerSchema.optional()
270
+ });
271
+ var WorkflowDefinitionSchema = z.object({
272
+ name: nonEmptyString2,
273
+ platform: z.enum(["web", "android", "ios"]).default("web"),
274
+ config: workflowConfigSchema.optional(),
275
+ continueOnFailure: z.boolean().default(false),
276
+ tests: z.array(testReferenceSchema).min(1, "Workflow must contain at least one test")
277
+ });
278
+ var nonEmptyString3 = z.string().trim().min(1, "Value cannot be empty");
279
+ var workflowReferenceSchema = z.object({
280
+ file: nonEmptyString3,
281
+ id: nonEmptyString3.optional(),
282
+ depends_on: z.array(nonEmptyString3).optional(),
283
+ on_failure: z.enum(["skip", "fail", "ignore"]).optional(),
284
+ variables: z.record(z.string(), z.string()).optional()
285
+ });
286
+ var pipelineWebConfigSchema = z.object({
287
+ baseUrl: nonEmptyString3.url().optional(),
288
+ browser: z.enum(["chromium", "firefox", "webkit"]).optional(),
289
+ headless: z.boolean().optional()
290
+ });
291
+ var pipelineAppwriteConfigSchema = z.object({
292
+ endpoint: nonEmptyString3.url(),
293
+ projectId: nonEmptyString3,
294
+ apiKey: nonEmptyString3,
295
+ cleanup: z.boolean().default(true),
296
+ cleanupOnFailure: z.boolean().default(true)
297
+ });
298
+ var pipelineCleanupDiscoverSchema = z.object({
299
+ enabled: z.boolean().default(true),
300
+ paths: z.array(z.string()).default(["./tests/cleanup"]),
301
+ pattern: z.string().default("**/*.ts")
302
+ }).optional();
303
+ var pipelineCleanupConfigSchema = z.object({
304
+ provider: z.string().optional(),
305
+ parallel: z.boolean().default(false),
306
+ retries: z.number().min(1).max(10).default(3),
307
+ types: z.record(z.string(), z.string()).optional(),
308
+ handlers: z.array(z.string()).optional(),
309
+ discover: pipelineCleanupDiscoverSchema,
310
+ on_failure: z.boolean().default(true)
311
+ // Run cleanup even if pipeline fails
312
+ }).passthrough();
313
+ var pipelineWebServerSchema = z.object({
314
+ command: nonEmptyString3.optional(),
315
+ auto: z.boolean().optional(),
316
+ url: nonEmptyString3.url(),
317
+ reuseExistingServer: z.boolean().default(true),
318
+ timeout: z.number().int().positive().default(3e4)
319
+ });
320
+ var pipelineConfigSchema = z.object({
321
+ web: pipelineWebConfigSchema.optional(),
322
+ appwrite: pipelineAppwriteConfigSchema.optional(),
323
+ cleanup: pipelineCleanupConfigSchema.optional(),
324
+ webServer: pipelineWebServerSchema.optional()
325
+ });
326
+ var PipelineDefinitionSchema = z.object({
327
+ name: nonEmptyString3,
328
+ platform: z.enum(["web", "android", "ios"]).default("web"),
329
+ config: pipelineConfigSchema.optional(),
330
+ on_failure: z.enum(["skip", "fail", "ignore"]).default("skip"),
331
+ cleanup_on_failure: z.boolean().default(true),
332
+ workflows: z.array(workflowReferenceSchema).min(1, "Pipeline must contain at least one workflow")
333
+ });
334
+
335
+ // src/core/loader.ts
336
+ var formatIssues = (issues) => issues.map((issue) => {
337
+ const path3 = issue.path.join(".") || "<root>";
338
+ return `${path3}: ${issue.message}`;
339
+ }).join("; ");
340
+ var interpolateEnvVars = (obj) => {
341
+ if (typeof obj === "string") {
342
+ return obj.replace(/\$\{([^}]+)\}/g, (_, varName) => {
343
+ const value = process.env[varName];
344
+ if (value === void 0) {
345
+ throw new Error(`Environment variable ${varName} is not defined`);
346
+ }
347
+ return value;
348
+ });
349
+ }
350
+ if (Array.isArray(obj)) {
351
+ return obj.map(interpolateEnvVars);
352
+ }
353
+ if (obj !== null && typeof obj === "object") {
354
+ const result = {};
355
+ for (const [key, value] of Object.entries(obj)) {
356
+ result[key] = interpolateEnvVars(value);
357
+ }
358
+ return result;
359
+ }
360
+ return obj;
361
+ };
362
+ var parseWithSchema = (content, schema, subject) => {
363
+ let parsed;
364
+ try {
365
+ parsed = parse(content);
366
+ } catch (error) {
367
+ const message = error instanceof Error ? error.message : String(error);
368
+ throw new Error(`Invalid YAML for ${subject}: ${message}`);
369
+ }
370
+ const interpolated = interpolateEnvVars(parsed);
371
+ const result = schema.safeParse(interpolated);
372
+ if (!result.success) {
373
+ throw new Error(`Invalid ${subject}: ${formatIssues(result.error.issues)}`);
374
+ }
375
+ return result.data;
376
+ };
377
+ var parseTestDefinition = (content) => parseWithSchema(content, TestDefinitionSchema, "test definition");
378
+ var loadTestDefinition = async (filePath) => {
379
+ const fileContent = await fs2.readFile(filePath, "utf8");
380
+ return parseTestDefinition(fileContent);
381
+ };
382
+ var parseIntellitesterConfig = (content) => parseWithSchema(content, IntellitesterConfigSchema, "config");
383
+ var loadIntellitesterConfig = async (filePath) => {
384
+ const fileContent = await fs2.readFile(filePath, "utf8");
385
+ return parseIntellitesterConfig(fileContent);
386
+ };
387
+ var parseWorkflowDefinition = (content) => parseWithSchema(content, WorkflowDefinitionSchema, "workflow definition");
388
+ var loadWorkflowDefinition = async (filePath) => {
389
+ const fileContent = await fs2.readFile(filePath, "utf8");
390
+ return parseWorkflowDefinition(fileContent);
391
+ };
392
+ var isWorkflowFile = (filePath) => {
393
+ return filePath.endsWith(".workflow.yaml") || filePath.endsWith(".workflow.yml");
394
+ };
395
+ var isPipelineFile = (filePath) => {
396
+ return filePath.endsWith(".pipeline.yaml") || filePath.endsWith(".pipeline.yml");
397
+ };
398
+ var parsePipelineDefinition = (content) => parseWithSchema(content, PipelineDefinitionSchema, "pipeline definition");
399
+ var loadPipelineDefinition = async (filePath) => {
400
+ const fileContent = await fs2.readFile(filePath, "utf8");
401
+ return parsePipelineDefinition(fileContent);
402
+ };
403
+ var collectMissingEnvVars = (obj) => {
404
+ const missing = [];
405
+ const collect = (value) => {
406
+ if (typeof value === "string") {
407
+ const matches = value.matchAll(/\$\{([^}]+)\}/g);
408
+ for (const match of matches) {
409
+ const varName = match[1];
410
+ if (process.env[varName] === void 0 && !missing.includes(varName)) {
411
+ missing.push(varName);
412
+ }
413
+ }
414
+ } else if (Array.isArray(value)) {
415
+ value.forEach(collect);
416
+ } else if (value !== null && typeof value === "object") {
417
+ Object.values(value).forEach(collect);
418
+ }
419
+ };
420
+ collect(obj);
421
+ return missing;
422
+ };
423
+
424
+ // src/integrations/email/inbucketClient.ts
425
+ var InbucketClient = class {
426
+ constructor(config) {
427
+ this.endpoint = config.endpoint.replace(/\/$/, "");
428
+ }
429
+ /**
430
+ * Extract mailbox name from email (e.g., "test@example.com" → "test")
431
+ */
432
+ getMailboxName(email) {
433
+ return email.split("@")[0];
434
+ }
435
+ /**
436
+ * List all messages in a mailbox
437
+ */
438
+ async listMessages(email) {
439
+ const mailbox = this.getMailboxName(email);
440
+ const url = `${this.endpoint}/api/v1/mailbox/${mailbox}`;
441
+ const response = await fetch(url);
442
+ if (!response.ok) {
443
+ throw new Error(
444
+ `Failed to list messages for ${email}: ${response.status} ${response.statusText}`
445
+ );
446
+ }
447
+ const messages = await response.json();
448
+ return messages;
449
+ }
450
+ /**
451
+ * Get a specific message
452
+ */
453
+ async getMessage(email, id) {
454
+ const mailbox = this.getMailboxName(email);
455
+ const url = `${this.endpoint}/api/v1/mailbox/${mailbox}/${id}`;
456
+ const response = await fetch(url);
457
+ if (!response.ok) {
458
+ throw new Error(
459
+ `Failed to get message ${id} for ${email}: ${response.status} ${response.statusText}`
460
+ );
461
+ }
462
+ const message = await response.json();
463
+ return message;
464
+ }
465
+ /**
466
+ * Wait for an email to arrive (polling with timeout)
467
+ */
468
+ async waitForEmail(email, options) {
469
+ const timeout = options?.timeout ?? 3e4;
470
+ const pollInterval = options?.pollInterval ?? 1e3;
471
+ const subjectContains = options?.subjectContains;
472
+ const startTime = Date.now();
473
+ while (Date.now() - startTime < timeout) {
474
+ const messages = await this.listMessages(email);
475
+ const matchingMessage = messages.find((msg) => {
476
+ if (subjectContains) {
477
+ return msg.subject.includes(subjectContains);
478
+ }
479
+ return true;
480
+ });
481
+ if (matchingMessage) {
482
+ return await this.getMessage(email, matchingMessage.id);
483
+ }
484
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
485
+ }
486
+ throw new Error(
487
+ `Timeout waiting for email to ${email}${subjectContains ? ` with subject containing "${subjectContains}"` : ""}`
488
+ );
489
+ }
490
+ /**
491
+ * Extract verification code from email body
492
+ */
493
+ extractCode(email, pattern) {
494
+ const regex = pattern ?? /\b(\d{6})\b/;
495
+ const text = email.body.text || email.body.html;
496
+ const match = text.match(regex);
497
+ return match ? match[1] : null;
498
+ }
499
+ /**
500
+ * Extract link from email body
501
+ */
502
+ extractLink(email, pattern) {
503
+ const regex = pattern ?? /https?:\/\/[^\s"'<>]+/;
504
+ const text = email.body.text || email.body.html;
505
+ const match = text.match(regex);
506
+ return match ? match[0] : null;
507
+ }
508
+ /**
509
+ * Delete a specific message
510
+ */
511
+ async deleteMessage(email, id) {
512
+ const mailbox = this.getMailboxName(email);
513
+ const url = `${this.endpoint}/api/v1/mailbox/${mailbox}/${id}`;
514
+ const response = await fetch(url, {
515
+ method: "DELETE"
516
+ });
517
+ if (!response.ok) {
518
+ throw new Error(
519
+ `Failed to delete message ${id} for ${email}: ${response.status} ${response.statusText}`
520
+ );
521
+ }
522
+ }
523
+ /**
524
+ * Clear all messages in a mailbox
525
+ */
526
+ async clearMailbox(email) {
527
+ const mailbox = this.getMailboxName(email);
528
+ const url = `${this.endpoint}/api/v1/mailbox/${mailbox}`;
529
+ const response = await fetch(url, {
530
+ method: "DELETE"
531
+ });
532
+ if (!response.ok) {
533
+ throw new Error(
534
+ `Failed to clear mailbox for ${email}: ${response.status} ${response.statusText}`
535
+ );
536
+ }
537
+ }
538
+ };
539
+ var AppwriteTestClient = class {
540
+ constructor(config) {
541
+ this.config = config;
542
+ this.client = new Client().setEndpoint(config.endpoint).setProject(config.projectId).setKey(config.apiKey);
543
+ this.users = new Users(this.client);
544
+ this.tablesDB = new TablesDB(this.client);
545
+ this.storage = new Storage(this.client);
546
+ this.teams = new Teams(this.client);
547
+ }
548
+ async cleanup(context, sessionId, cwd) {
549
+ const deleted = [];
550
+ const failed = [];
551
+ const sortedResources = [...context.resources].reverse();
552
+ for (const resource of sortedResources) {
553
+ if (resource.deleted) {
554
+ deleted.push(`${resource.type}:${resource.id} (already deleted)`);
555
+ continue;
556
+ }
557
+ try {
558
+ switch (resource.type) {
559
+ case "row":
560
+ if (resource.databaseId && resource.tableId) {
561
+ await this.tablesDB.deleteRow({
562
+ databaseId: resource.databaseId,
563
+ tableId: resource.tableId,
564
+ rowId: resource.id
565
+ });
566
+ }
567
+ break;
568
+ case "file":
569
+ if (resource.bucketId) {
570
+ await this.storage.deleteFile(resource.bucketId, resource.id);
571
+ }
572
+ break;
573
+ case "membership":
574
+ if (resource.teamId) {
575
+ await this.teams.deleteMembership(resource.teamId, resource.id);
576
+ }
577
+ break;
578
+ case "team":
579
+ await this.teams.delete(resource.id);
580
+ break;
581
+ case "message":
582
+ deleted.push(`${resource.type}:${resource.id} (skipped - messages cannot be deleted)`);
583
+ continue;
584
+ case "user":
585
+ break;
586
+ }
587
+ deleted.push(`${resource.type}:${resource.id}`);
588
+ } catch (error) {
589
+ failed.push(`${resource.type}:${resource.id}`);
590
+ console.warn(`Failed to delete ${resource.type} ${resource.id}:`, error);
591
+ }
592
+ }
593
+ if (context.userId) {
594
+ try {
595
+ await this.users.delete(context.userId);
596
+ deleted.push(`user:${context.userId}`);
597
+ } catch (error) {
598
+ failed.push(`user:${context.userId}`);
599
+ console.warn(`Failed to delete user ${context.userId}:`, error);
600
+ }
601
+ }
602
+ if (failed.length > 0 && sessionId) {
603
+ const failedResources = context.resources.filter(
604
+ (r) => failed.some((f) => f.includes(r.id))
605
+ );
606
+ await saveFailedCleanup({
607
+ sessionId,
608
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
609
+ resources: failedResources.map((r) => ({
610
+ type: r.type,
611
+ id: r.id,
612
+ databaseId: r.databaseId,
613
+ tableId: r.tableId,
614
+ bucketId: r.bucketId,
615
+ teamId: r.teamId
616
+ })),
617
+ providerConfig: {
618
+ provider: "appwrite",
619
+ endpoint: this.config.endpoint,
620
+ projectId: this.config.projectId
621
+ },
622
+ errors: failed
623
+ }, cwd);
624
+ }
625
+ return { success: failed.length === 0, deleted, failed };
626
+ }
627
+ };
628
+ function createTestContext() {
629
+ return {
630
+ resources: [],
631
+ variables: /* @__PURE__ */ new Map()
632
+ };
633
+ }
634
+
635
+ // src/integrations/appwrite/types.ts
636
+ var APPWRITE_PATTERNS = {
637
+ userCreate: /\/v1\/account$/,
638
+ rowCreate: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows$/,
639
+ fileCreate: /\/v1\/storage\/buckets\/([\w-]+)\/files$/,
640
+ teamCreate: /\/v1\/teams$/,
641
+ membershipCreate: /\/v1\/teams\/([\w-]+)\/memberships$/,
642
+ messageCreate: /\/v1\/messaging\/messages$/
643
+ };
644
+ var APPWRITE_UPDATE_PATTERNS = {
645
+ rowUpdate: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows\/([\w-]+)$/,
646
+ fileUpdate: /\/v1\/storage\/buckets\/([\w-]+)\/files\/([\w-]+)$/,
647
+ teamUpdate: /\/v1\/teams\/([\w-]+)$/
648
+ };
649
+ var APPWRITE_DELETE_PATTERNS = {
650
+ rowDelete: /\/v1\/tablesdb\/([\w-]+)\/tables\/([\w-]+)\/rows\/([\w-]+)$/,
651
+ fileDelete: /\/v1\/storage\/buckets\/([\w-]+)\/files\/([\w-]+)$/,
652
+ teamDelete: /\/v1\/teams\/([\w-]+)$/,
653
+ membershipDelete: /\/v1\/teams\/([\w-]+)\/memberships\/([\w-]+)$/
654
+ };
655
+ function resolveEnvVars(value) {
656
+ return value.replace(/\$\{(\w+)\}/g, (_, name) => process.env[name] || "");
657
+ }
658
+ var AnthropicProvider = class {
659
+ constructor(config) {
660
+ this.config = config;
661
+ const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : void 0;
662
+ this.client = new Anthropic({
663
+ apiKey,
664
+ model: this.config.model,
665
+ temperature: this.config.temperature
666
+ });
667
+ }
668
+ async generateCompletion(prompt, systemPrompt) {
669
+ const messages = [];
670
+ if (systemPrompt) {
671
+ messages.push({ role: "system", content: systemPrompt });
672
+ }
673
+ messages.push({ role: "user", content: prompt });
674
+ const response = await this.client.chat({ messages });
675
+ const content = response.message.content;
676
+ if (!content) {
677
+ throw new Error("No content in Anthropic response");
678
+ }
679
+ return typeof content === "string" ? content : JSON.stringify(content);
680
+ }
681
+ };
682
+ var OpenAIProvider = class {
683
+ constructor(config) {
684
+ this.config = config;
685
+ const apiKey = config.apiKey ? resolveEnvVars(config.apiKey) : void 0;
686
+ const baseURL = config.baseUrl;
687
+ this.client = new OpenAI({
688
+ apiKey,
689
+ model: this.config.model,
690
+ temperature: this.config.temperature,
691
+ baseURL
692
+ });
693
+ }
694
+ async generateCompletion(prompt, systemPrompt) {
695
+ const messages = [];
696
+ if (systemPrompt) {
697
+ messages.push({ role: "system", content: systemPrompt });
698
+ }
699
+ messages.push({ role: "user", content: prompt });
700
+ const response = await this.client.chat({ messages });
701
+ const content = response.message.content;
702
+ if (!content) {
703
+ throw new Error("No content in OpenAI response");
704
+ }
705
+ return typeof content === "string" ? content : JSON.stringify(content);
706
+ }
707
+ };
708
+ var OllamaProvider = class {
709
+ constructor(config) {
710
+ this.config = config;
711
+ this.client = new Ollama({
712
+ model: this.config.model,
713
+ options: {
714
+ temperature: this.config.temperature
715
+ }
716
+ });
717
+ }
718
+ async generateCompletion(prompt, systemPrompt) {
719
+ const messages = [];
720
+ if (systemPrompt) {
721
+ messages.push({ role: "system", content: systemPrompt });
722
+ }
723
+ messages.push({ role: "user", content: prompt });
724
+ const response = await this.client.chat({ messages });
725
+ const content = response.message.content;
726
+ if (!content) {
727
+ throw new Error("No content in Ollama response");
728
+ }
729
+ return typeof content === "string" ? content : JSON.stringify(content);
730
+ }
731
+ };
732
+ function createAIProvider(config) {
733
+ switch (config.provider) {
734
+ case "anthropic":
735
+ return new AnthropicProvider(config);
736
+ case "openai":
737
+ return new OpenAIProvider(config);
738
+ case "ollama":
739
+ return new OllamaProvider(config);
740
+ }
741
+ }
742
+
743
+ // src/ai/errorHelper.ts
744
+ function formatLocator(locator) {
745
+ const parts = [];
746
+ if (locator.testId) parts.push(`testId: "${locator.testId}"`);
747
+ if (locator.text) parts.push(`text: "${locator.text}"`);
748
+ if (locator.css) parts.push(`css: "${locator.css}"`);
749
+ if (locator.xpath) parts.push(`xpath: "${locator.xpath}"`);
750
+ if (locator.role) parts.push(`role: "${locator.role}"`);
751
+ if (locator.name) parts.push(`name: "${locator.name}"`);
752
+ if (locator.description) parts.push(`description: "${locator.description}"`);
753
+ return parts.join(", ");
754
+ }
755
+ function formatAction(action) {
756
+ switch (action.type) {
757
+ case "tap":
758
+ return `tap on element (${formatLocator(action.target)})`;
759
+ case "input":
760
+ return `input into element (${formatLocator(action.target)})`;
761
+ case "assert":
762
+ return `assert element exists (${formatLocator(action.target)})`;
763
+ case "wait":
764
+ return action.target ? `wait for element (${formatLocator(action.target)})` : `wait ${action.timeout}ms`;
765
+ case "scroll":
766
+ return action.target ? `scroll to element (${formatLocator(action.target)})` : `scroll ${action.direction || "down"}`;
767
+ default:
768
+ return action.type;
769
+ }
770
+ }
771
+ async function getAISuggestion(error, action, pageContent, screenshot, aiConfig) {
772
+ if (!aiConfig) {
773
+ return {
774
+ hasSuggestion: false,
775
+ explanation: "AI configuration not provided. Cannot generate suggestions."
776
+ };
777
+ }
778
+ try {
779
+ const provider = createAIProvider(aiConfig);
780
+ const systemPrompt = `You are an expert at analyzing web automation errors and suggesting better element selectors.
781
+ Your task is to analyze failed actions and suggest better selectors based on the page content and error message.
782
+
783
+ Return your response in the following JSON format:
784
+ {
785
+ "hasSuggestion": boolean,
786
+ "suggestedSelector": {
787
+ "testId": "string (optional)",
788
+ "text": "string (optional)",
789
+ "css": "string (optional)",
790
+ "role": "string (optional)",
791
+ "name": "string (optional)"
792
+ },
793
+ "explanation": "string explaining why this selector is better"
794
+ }
795
+
796
+ Prefer selectors in this order:
797
+ 1. testId (most reliable)
798
+ 2. text (good for user-facing elements)
799
+ 3. role with name (semantic and accessible)
800
+ 4. css (last resort, but can be precise)
801
+
802
+ Do not suggest xpath unless absolutely necessary.`;
803
+ const prompt = `Action failed: ${formatAction(action)}
804
+
805
+ Error message:
806
+ ${error}
807
+
808
+ Page content (truncated to 10000 chars):
809
+ ${pageContent.slice(0, 1e4)}
810
+
811
+ ${screenshot ? "[Screenshot attached but not analyzed in this implementation]" : ""}
812
+
813
+ Please analyze the error and suggest a better selector that would work reliably. Focus on:
814
+ - What went wrong with the current selector
815
+ - What selector would be more reliable
816
+ - Why the suggested selector is better
817
+
818
+ Return ONLY valid JSON, no additional text.`;
819
+ const response = await provider.generateCompletion(prompt, systemPrompt);
820
+ let jsonStr = response.trim();
821
+ if (jsonStr.startsWith("```json")) {
822
+ jsonStr = jsonStr.replace(/^```json\s*/, "").replace(/\s*```$/, "");
823
+ } else if (jsonStr.startsWith("```")) {
824
+ jsonStr = jsonStr.replace(/^```\s*/, "").replace(/\s*```$/, "");
825
+ }
826
+ const parsed = JSON.parse(jsonStr);
827
+ return parsed;
828
+ } catch (err) {
829
+ const message = err instanceof Error ? err.message : String(err);
830
+ return {
831
+ hasSuggestion: false,
832
+ explanation: `Failed to generate AI suggestion: ${message}`
833
+ };
834
+ }
835
+ }
836
+ var TrackingServer = class {
837
+ constructor() {
838
+ this.server = null;
839
+ this.resources = /* @__PURE__ */ new Map();
840
+ this.port = 0;
841
+ }
842
+ async start(options = {}) {
843
+ return new Promise((resolve, reject) => {
844
+ this.server = createServer((req, res) => {
845
+ this.handleRequest(req, res);
846
+ });
847
+ this.server.on("error", (error) => {
848
+ reject(error);
849
+ });
850
+ const port = options.port ?? 0;
851
+ this.server.listen(port, () => {
852
+ const address = this.server?.address();
853
+ if (address && typeof address === "object") {
854
+ this.port = address.port;
855
+ resolve();
856
+ } else {
857
+ reject(new Error("Failed to get server port"));
858
+ }
859
+ });
860
+ });
861
+ }
862
+ handleRequest(req, res) {
863
+ res.setHeader("Access-Control-Allow-Origin", "*");
864
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
865
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
866
+ if (req.method === "OPTIONS") {
867
+ res.writeHead(204);
868
+ res.end();
869
+ return;
870
+ }
871
+ const url = new URL(req.url || "/", `http://${req.headers.host}`);
872
+ if (req.method === "POST" && url.pathname === "/track") {
873
+ this.handleTrackRequest(req, res);
874
+ } else if (req.method === "GET" && url.pathname.startsWith("/resources/")) {
875
+ this.handleGetResources(url, res);
876
+ } else {
877
+ res.writeHead(404, { "Content-Type": "application/json" });
878
+ res.end(JSON.stringify({ error: "Not found" }));
879
+ }
880
+ }
881
+ handleTrackRequest(req, res) {
882
+ let body = "";
883
+ req.on("data", (chunk) => {
884
+ body += chunk.toString();
885
+ });
886
+ req.on("end", () => {
887
+ try {
888
+ const trackRequest = JSON.parse(body);
889
+ if (!trackRequest.sessionId || !trackRequest.type || !trackRequest.id) {
890
+ res.writeHead(400, { "Content-Type": "application/json" });
891
+ res.end(JSON.stringify({ error: "Missing required fields (sessionId, type, id)" }));
892
+ return;
893
+ }
894
+ const { sessionId, ...resourceData } = trackRequest;
895
+ const resource = {
896
+ ...resourceData,
897
+ type: trackRequest.type,
898
+ id: trackRequest.id,
899
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
900
+ };
901
+ const sessionResources = this.resources.get(sessionId) || [];
902
+ sessionResources.push(resource);
903
+ this.resources.set(sessionId, sessionResources);
904
+ res.writeHead(200, { "Content-Type": "application/json" });
905
+ res.end(JSON.stringify({ success: true }));
906
+ } catch {
907
+ res.writeHead(400, { "Content-Type": "application/json" });
908
+ res.end(JSON.stringify({ error: "Invalid JSON" }));
909
+ }
910
+ });
911
+ req.on("error", () => {
912
+ res.writeHead(500, { "Content-Type": "application/json" });
913
+ res.end(JSON.stringify({ error: "Internal server error" }));
914
+ });
915
+ }
916
+ handleGetResources(url, res) {
917
+ const sessionId = url.pathname.split("/").pop();
918
+ if (!sessionId) {
919
+ res.writeHead(400, { "Content-Type": "application/json" });
920
+ res.end(JSON.stringify({ error: "Missing sessionId" }));
921
+ return;
922
+ }
923
+ const resources = this.resources.get(sessionId) || [];
924
+ res.writeHead(200, { "Content-Type": "application/json" });
925
+ res.end(JSON.stringify({ resources }));
926
+ }
927
+ getResources(sessionId) {
928
+ return this.resources.get(sessionId) ?? [];
929
+ }
930
+ clearSession(sessionId) {
931
+ this.resources.delete(sessionId);
932
+ }
933
+ async stop() {
934
+ return new Promise((resolve, reject) => {
935
+ if (!this.server) {
936
+ resolve();
937
+ return;
938
+ }
939
+ this.server.close((error) => {
940
+ if (error) {
941
+ reject(error);
942
+ } else {
943
+ this.server = null;
944
+ resolve();
945
+ }
946
+ });
947
+ });
948
+ }
949
+ };
950
+ async function startTrackingServer(options) {
951
+ const server = new TrackingServer();
952
+ await server.start(options);
953
+ return server;
954
+ }
955
+
956
+ // src/executors/web/playwrightExecutor.ts
957
+ var defaultScreenshotDir = path.join(process.cwd(), "artifacts", "screenshots");
958
+ function interpolateVariables(value, variables) {
959
+ return value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
960
+ if (varName === "uuid") {
961
+ return crypto2.randomUUID().split("-")[0];
962
+ }
963
+ return variables.get(varName) ?? match;
964
+ });
965
+ }
966
+ var resolveUrl = (value, baseUrl) => {
967
+ if (!baseUrl) return value;
968
+ try {
969
+ const url = new URL(value, baseUrl);
970
+ return url.toString();
971
+ } catch {
972
+ return value;
973
+ }
974
+ };
975
+ var resolveLocator = (page, locator) => {
976
+ if (locator.testId) return page.getByTestId(locator.testId);
977
+ if (locator.text) return page.getByText(locator.text);
978
+ if (locator.css) return page.locator(locator.css);
979
+ if (locator.xpath) return page.locator(`xpath=${locator.xpath}`);
980
+ if (locator.role) {
981
+ const options = {};
982
+ if (locator.name) options.name = locator.name;
983
+ return page.getByRole(locator.role, options);
984
+ }
985
+ if (locator.description) return page.getByText(locator.description);
986
+ throw new Error("No usable selector found for locator");
987
+ };
988
+ async function ensureScreenshotDir(dir) {
989
+ await fs2.mkdir(dir, { recursive: true });
990
+ }
991
+ var runNavigate = async (page, value, baseUrl, context) => {
992
+ const interpolated = interpolateVariables(value, context.variables);
993
+ const target = resolveUrl(interpolated, baseUrl);
994
+ await page.goto(target);
995
+ };
996
+ var runTap = async (page, locator) => {
997
+ const handle = resolveLocator(page, locator);
998
+ await handle.click();
999
+ };
1000
+ var runInput = async (page, locator, value, context) => {
1001
+ const interpolated = interpolateVariables(value, context.variables);
1002
+ const handle = resolveLocator(page, locator);
1003
+ await handle.fill(interpolated);
1004
+ };
1005
+ var runAssert = async (page, locator, value, context) => {
1006
+ const handle = resolveLocator(page, locator);
1007
+ await handle.waitFor({ state: "visible" });
1008
+ if (value) {
1009
+ const interpolated = interpolateVariables(value, context.variables);
1010
+ const text = (await handle.textContent())?.trim() ?? "";
1011
+ if (!text.includes(interpolated)) {
1012
+ throw new Error(
1013
+ `Assertion failed: expected element text to include "${interpolated}", got "${text}"`
1014
+ );
1015
+ }
1016
+ }
1017
+ };
1018
+ var runWait = async (page, action) => {
1019
+ if (action.target) {
1020
+ const handle = resolveLocator(page, action.target);
1021
+ await handle.waitFor({ state: "visible", timeout: action.timeout });
1022
+ return;
1023
+ }
1024
+ await page.waitForTimeout(action.timeout ?? 1e3);
1025
+ };
1026
+ var runScroll = async (page, action) => {
1027
+ if (action.target) {
1028
+ const handle = resolveLocator(page, action.target);
1029
+ await handle.scrollIntoViewIfNeeded();
1030
+ return;
1031
+ }
1032
+ const amount = action.amount ?? 500;
1033
+ const direction = action.direction ?? "down";
1034
+ const deltaY = direction === "up" ? -amount : amount;
1035
+ await page.evaluate((value) => window.scrollBy(0, value), deltaY);
1036
+ };
1037
+ var runScreenshot = async (page, name, screenshotDir, stepIndex) => {
1038
+ await ensureScreenshotDir(screenshotDir);
1039
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => {
1040
+ });
1041
+ const filename = name ?? `step-${stepIndex + 1}.png`;
1042
+ const filePath = path.join(screenshotDir, filename);
1043
+ await page.screenshot({ path: filePath, fullPage: true });
1044
+ return filePath;
1045
+ };
1046
+ var getBrowser = (browser) => {
1047
+ switch (browser) {
1048
+ case "firefox":
1049
+ return firefox;
1050
+ case "webkit":
1051
+ return webkit;
1052
+ default:
1053
+ return chromium;
1054
+ }
1055
+ };
1056
+ async function isServerRunning(url) {
1057
+ try {
1058
+ const response = await fetch(url, { method: "HEAD" });
1059
+ return response.ok || response.status < 500;
1060
+ } catch {
1061
+ return false;
1062
+ }
1063
+ }
1064
+ async function waitForServer(url, timeout) {
1065
+ const start = Date.now();
1066
+ while (Date.now() - start < timeout) {
1067
+ if (await isServerRunning(url)) return;
1068
+ await new Promise((r) => setTimeout(r, 500));
1069
+ }
1070
+ throw new Error(`Server at ${url} not ready after ${timeout}ms`);
1071
+ }
1072
+ async function detectBuildDirectory(cwd) {
1073
+ const commonDirs = [
1074
+ ".next",
1075
+ // Next.js
1076
+ ".output",
1077
+ // Nuxt 3
1078
+ ".svelte-kit",
1079
+ // SvelteKit
1080
+ "dist",
1081
+ // Vite, Astro, Rollup, generic
1082
+ "build",
1083
+ // CRA, Remix, generic
1084
+ "out"
1085
+ // Next.js static export
1086
+ ];
1087
+ for (const dir of commonDirs) {
1088
+ const fullPath = path.join(cwd, dir);
1089
+ try {
1090
+ const stat = await fs2.stat(fullPath);
1091
+ if (stat.isDirectory()) {
1092
+ return dir;
1093
+ }
1094
+ } catch {
1095
+ }
1096
+ }
1097
+ return null;
1098
+ }
1099
+ async function readPackageJson(cwd) {
1100
+ try {
1101
+ const packagePath = path.join(cwd, "package.json");
1102
+ const content = await fs2.readFile(packagePath, "utf-8");
1103
+ return JSON.parse(content);
1104
+ } catch {
1105
+ return null;
1106
+ }
1107
+ }
1108
+ function detectFramework(pkg) {
1109
+ if (!pkg) return null;
1110
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1111
+ if (deps["next"]) {
1112
+ return { name: "next", buildCommand: "npx -y next start", devCommand: "next dev" };
1113
+ }
1114
+ if (deps["nuxt"]) {
1115
+ return { name: "nuxt", buildCommand: "node .output/server/index.mjs", devCommand: "nuxi dev" };
1116
+ }
1117
+ if (deps["astro"]) {
1118
+ return { name: "astro", buildCommand: "npx -y astro dev", devCommand: "astro dev" };
1119
+ }
1120
+ if (deps["@sveltejs/kit"]) {
1121
+ return { name: "sveltekit", buildCommand: "npx -y vite preview", devCommand: "vite dev" };
1122
+ }
1123
+ if (deps["@remix-run/serve"] || deps["@remix-run/dev"]) {
1124
+ return { name: "remix", buildCommand: "npx -y remix-serve build/server/index.js", devCommand: "remix vite:dev" };
1125
+ }
1126
+ if (deps["vite"]) {
1127
+ return { name: "vite", buildCommand: "npx -y vite preview", devCommand: "vite dev" };
1128
+ }
1129
+ if (deps["react-scripts"]) {
1130
+ return { name: "cra", buildCommand: "npx -y serve -s build", devCommand: "react-scripts start" };
1131
+ }
1132
+ return null;
1133
+ }
1134
+ async function detectPackageManager(cwd) {
1135
+ const hasDenoLock = await fs2.stat(path.join(cwd, "deno.lock")).catch(() => null);
1136
+ const hasBunLock = await fs2.stat(path.join(cwd, "bun.lockb")).catch(() => null);
1137
+ const hasPnpmLock = await fs2.stat(path.join(cwd, "pnpm-lock.yaml")).catch(() => null);
1138
+ const hasYarnLock = await fs2.stat(path.join(cwd, "yarn.lock")).catch(() => null);
1139
+ if (hasDenoLock) return "deno";
1140
+ if (hasBunLock) return "bun";
1141
+ if (hasPnpmLock) return "pnpm";
1142
+ if (hasYarnLock) return "yarn";
1143
+ return "npm";
1144
+ }
1145
+ function getDevCommand(pm, script) {
1146
+ switch (pm) {
1147
+ case "deno":
1148
+ return `deno task ${script}`;
1149
+ case "bun":
1150
+ return `bun run ${script}`;
1151
+ case "pnpm":
1152
+ return `pnpm ${script}`;
1153
+ case "yarn":
1154
+ return `yarn ${script}`;
1155
+ case "npm":
1156
+ return `npm run ${script}`;
1157
+ }
1158
+ }
1159
+ async function detectServerCommand(cwd) {
1160
+ const pkg = await readPackageJson(cwd);
1161
+ const framework = detectFramework(pkg);
1162
+ const pm = await detectPackageManager(cwd);
1163
+ const buildDir = await detectBuildDirectory(cwd);
1164
+ if (buildDir) {
1165
+ if (framework) {
1166
+ console.log(`Detected ${framework.name} project with build at ${buildDir}`);
1167
+ return framework.buildCommand;
1168
+ }
1169
+ console.log(`Detected build directory at ${buildDir}, using static server`);
1170
+ return `npx -y serve ${buildDir}`;
1171
+ }
1172
+ if (pkg?.scripts?.dev) {
1173
+ if (framework) {
1174
+ console.log(`Detected ${framework.name} project, running dev server`);
1175
+ }
1176
+ return getDevCommand(pm, "dev");
1177
+ }
1178
+ if (pkg?.scripts?.start) {
1179
+ return getDevCommand(pm, "start");
1180
+ }
1181
+ throw new Error("Could not auto-detect server command. Please specify command explicitly.");
1182
+ }
1183
+ async function startWebServer(config) {
1184
+ const { url, reuseExistingServer = true, timeout = 3e4, cwd = process.cwd() } = config;
1185
+ if (reuseExistingServer && await isServerRunning(url)) {
1186
+ console.log(`Server already running at ${url}`);
1187
+ return null;
1188
+ }
1189
+ let command;
1190
+ if (config.command) {
1191
+ command = config.command;
1192
+ } else if (config.static) {
1193
+ const port = config.port ?? new URL(url).port ?? "3000";
1194
+ command = `npx -y serve ${config.static} -l ${port}`;
1195
+ } else if (config.auto) {
1196
+ command = await detectServerCommand(cwd);
1197
+ } else {
1198
+ throw new Error("WebServerConfig requires command, auto: true, or static directory");
1199
+ }
1200
+ console.log(`Starting server: ${command}`);
1201
+ const serverProcess = spawn(command, {
1202
+ shell: true,
1203
+ stdio: "pipe",
1204
+ cwd,
1205
+ detached: false
1206
+ });
1207
+ serverProcess.stdout?.on("data", (data) => {
1208
+ process.stdout.write(`[server] ${data}`);
1209
+ });
1210
+ serverProcess.stderr?.on("data", (data) => {
1211
+ process.stderr.write(`[server] ${data}`);
1212
+ });
1213
+ await waitForServer(url, timeout);
1214
+ console.log(`Server ready at ${url}`);
1215
+ return serverProcess;
1216
+ }
1217
+ function killServer(serverProcess) {
1218
+ if (serverProcess && !serverProcess.killed) {
1219
+ console.log("Stopping server...");
1220
+ serverProcess.kill("SIGTERM");
1221
+ }
1222
+ }
1223
+ async function handleInteractiveError(page, action, error, screenshotDir, stepIndex, aiConfig) {
1224
+ console.error(`
1225
+ \u274C Action failed: ${action.type}`);
1226
+ console.error(` Error: ${error.message}
1227
+ `);
1228
+ await ensureScreenshotDir(screenshotDir);
1229
+ const screenshotPath = path.join(screenshotDir, `error-step-${stepIndex + 1}.png`);
1230
+ await page.screenshot({ path: screenshotPath, fullPage: true });
1231
+ const pageContent = await page.content();
1232
+ if (aiConfig) {
1233
+ console.log("\u{1F916} Analyzing error with AI...\n");
1234
+ const screenshot = await fs2.readFile(screenshotPath);
1235
+ const suggestion = await getAISuggestion(error.message, action, pageContent, screenshot, aiConfig);
1236
+ if (suggestion.hasSuggestion && suggestion.suggestedSelector) {
1237
+ console.log("\u{1F916} AI Suggestion:");
1238
+ console.log(` ${suggestion.explanation}
1239
+ `);
1240
+ console.log(" Suggested selector:");
1241
+ console.log(" target:");
1242
+ if (suggestion.suggestedSelector.testId) {
1243
+ console.log(` testId: "${suggestion.suggestedSelector.testId}"`);
1244
+ }
1245
+ if (suggestion.suggestedSelector.text) {
1246
+ console.log(` text: "${suggestion.suggestedSelector.text}"`);
1247
+ }
1248
+ if (suggestion.suggestedSelector.css) {
1249
+ console.log(` css: "${suggestion.suggestedSelector.css}"`);
1250
+ }
1251
+ if (suggestion.suggestedSelector.role) {
1252
+ console.log(` role: "${suggestion.suggestedSelector.role}"`);
1253
+ }
1254
+ if (suggestion.suggestedSelector.name) {
1255
+ console.log(` name: "${suggestion.suggestedSelector.name}"`);
1256
+ }
1257
+ console.log("");
1258
+ } else {
1259
+ console.log(`\u{1F916} AI Analysis: ${suggestion.explanation}
1260
+ `);
1261
+ }
1262
+ }
1263
+ const response = await prompts({
1264
+ type: "select",
1265
+ name: "action",
1266
+ message: "What would you like to do?",
1267
+ choices: [
1268
+ { title: "Retry with AI suggestion", value: "retry", disabled: !aiConfig },
1269
+ { title: "Skip this step", value: "skip" },
1270
+ { title: "Abort test", value: "abort" },
1271
+ { title: "Open in browser (pause)", value: "debug" }
1272
+ ],
1273
+ initial: 0
1274
+ });
1275
+ return response.action || "abort";
1276
+ }
1277
+ async function executeActionWithRetry(page, action, index, options) {
1278
+ const { baseUrl, context, screenshotDir, debugMode, interactive, aiConfig } = options;
1279
+ while (true) {
1280
+ try {
1281
+ switch (action.type) {
1282
+ case "navigate": {
1283
+ const interpolated = interpolateVariables(action.value, context.variables);
1284
+ const target = resolveUrl(interpolated, baseUrl);
1285
+ if (debugMode) {
1286
+ console.log(`[DEBUG] Navigating to: ${target}`);
1287
+ }
1288
+ await runNavigate(page, action.value, baseUrl, context);
1289
+ break;
1290
+ }
1291
+ case "tap": {
1292
+ if (debugMode) {
1293
+ console.log(`[DEBUG] Tapping element:`, action.target);
1294
+ }
1295
+ await runTap(page, action.target);
1296
+ break;
1297
+ }
1298
+ case "input": {
1299
+ if (debugMode) {
1300
+ const interpolated = interpolateVariables(action.value, context.variables);
1301
+ console.log(`[DEBUG] Inputting value into element:`, action.target);
1302
+ console.log(`[DEBUG] Value: ${interpolated}`);
1303
+ }
1304
+ await runInput(page, action.target, action.value, context);
1305
+ break;
1306
+ }
1307
+ case "assert": {
1308
+ if (debugMode) {
1309
+ console.log(`[DEBUG] Asserting element:`, action.target);
1310
+ if (action.value) {
1311
+ const interpolated = interpolateVariables(action.value, context.variables);
1312
+ console.log(`[DEBUG] Expected text contains: ${interpolated}`);
1313
+ }
1314
+ }
1315
+ await runAssert(page, action.target, action.value, context);
1316
+ break;
1317
+ }
1318
+ case "wait":
1319
+ await runWait(page, action);
1320
+ break;
1321
+ case "scroll":
1322
+ await runScroll(page, action);
1323
+ break;
1324
+ case "screenshot":
1325
+ throw new Error("Screenshot action should be handled separately");
1326
+ case "setVar": {
1327
+ let value;
1328
+ if (action.value) {
1329
+ value = interpolateVariables(action.value, context.variables);
1330
+ } else if (action.from === "response") {
1331
+ throw new Error("setVar from response not yet implemented");
1332
+ } else if (action.from === "element") {
1333
+ throw new Error("setVar from element not yet implemented");
1334
+ } else if (action.from === "email") {
1335
+ throw new Error("Use email.extractCode or email.extractLink instead");
1336
+ } else {
1337
+ throw new Error("setVar requires value or from");
1338
+ }
1339
+ context.variables.set(action.name, value);
1340
+ break;
1341
+ }
1342
+ case "email.waitFor": {
1343
+ if (!context.emailClient) {
1344
+ throw new Error("Email client not configured");
1345
+ }
1346
+ const mailbox = interpolateVariables(action.mailbox, context.variables);
1347
+ context.lastEmail = await context.emailClient.waitForEmail(mailbox, {
1348
+ timeout: action.timeout,
1349
+ subjectContains: action.subjectContains
1350
+ });
1351
+ break;
1352
+ }
1353
+ case "email.extractCode": {
1354
+ if (!context.emailClient) {
1355
+ throw new Error("Email client not configured");
1356
+ }
1357
+ if (!context.lastEmail) {
1358
+ throw new Error("No email loaded - call email.waitFor first");
1359
+ }
1360
+ const code = context.emailClient.extractCode(
1361
+ context.lastEmail,
1362
+ action.pattern ? new RegExp(action.pattern) : void 0
1363
+ );
1364
+ if (!code) {
1365
+ throw new Error("No code found in email");
1366
+ }
1367
+ context.variables.set(action.saveTo, code);
1368
+ break;
1369
+ }
1370
+ case "email.extractLink": {
1371
+ if (!context.emailClient) {
1372
+ throw new Error("Email client not configured");
1373
+ }
1374
+ if (!context.lastEmail) {
1375
+ throw new Error("No email loaded - call email.waitFor first");
1376
+ }
1377
+ const link = context.emailClient.extractLink(
1378
+ context.lastEmail,
1379
+ action.pattern ? new RegExp(action.pattern) : void 0
1380
+ );
1381
+ if (!link) {
1382
+ throw new Error("No link found in email");
1383
+ }
1384
+ context.variables.set(action.saveTo, link);
1385
+ break;
1386
+ }
1387
+ case "email.clear": {
1388
+ if (!context.emailClient) {
1389
+ throw new Error("Email client not configured");
1390
+ }
1391
+ const mailbox = interpolateVariables(action.mailbox, context.variables);
1392
+ await context.emailClient.clearMailbox(mailbox);
1393
+ break;
1394
+ }
1395
+ case "appwrite.verifyEmail": {
1396
+ if (!context.appwriteContext.userId) {
1397
+ throw new Error("No user tracked. appwrite.verifyEmail requires a user signup to have occurred first.");
1398
+ }
1399
+ if (!context.appwriteConfig?.apiKey) {
1400
+ throw new Error("appwrite.verifyEmail requires appwrite.apiKey in config");
1401
+ }
1402
+ const { Client: Client2, Users: Users2 } = await import('node-appwrite');
1403
+ const client = new Client2().setEndpoint(context.appwriteConfig.endpoint).setProject(context.appwriteConfig.projectId).setKey(context.appwriteConfig.apiKey);
1404
+ const users = new Users2(client);
1405
+ await users.updateEmailVerification(context.appwriteContext.userId, true);
1406
+ console.log(`Verified email for user ${context.appwriteContext.userId}`);
1407
+ break;
1408
+ }
1409
+ case "debug": {
1410
+ console.log("[DEBUG] Pausing execution - Playwright Inspector will open");
1411
+ await page.pause();
1412
+ break;
1413
+ }
1414
+ default:
1415
+ throw new Error(`Unsupported action type: ${action.type}`);
1416
+ }
1417
+ return;
1418
+ } catch (err) {
1419
+ const error = err instanceof Error ? err : new Error(String(err));
1420
+ if (interactive && aiConfig && hasTarget(action)) {
1421
+ const choice = await handleInteractiveError(page, action, error, screenshotDir, index, aiConfig);
1422
+ switch (choice) {
1423
+ case "retry":
1424
+ console.log("Retrying with AI suggestion...\n");
1425
+ continue;
1426
+ case "skip":
1427
+ console.log("Skipping step...\n");
1428
+ return;
1429
+ case "debug":
1430
+ console.log("Opening Playwright Inspector...\n");
1431
+ await page.pause();
1432
+ continue;
1433
+ case "abort":
1434
+ default:
1435
+ throw error;
1436
+ }
1437
+ }
1438
+ if (debugMode) {
1439
+ console.error(`[DEBUG] Action failed: ${error.message}`);
1440
+ console.log("[DEBUG] Opening Playwright Inspector for debugging...");
1441
+ await page.pause();
1442
+ }
1443
+ throw error;
1444
+ }
1445
+ }
1446
+ }
1447
+ function hasTarget(action) {
1448
+ return "target" in action && action.target !== void 0;
1449
+ }
1450
+ var runWebTest = async (test, options = {}) => {
1451
+ if (test.platform !== "web") {
1452
+ throw new Error(`runWebTest only supports web platform, received ${test.platform}`);
1453
+ }
1454
+ const browserName = options.browser ?? "chromium";
1455
+ const headless = options.headed ? false : true;
1456
+ const screenshotDir = options.screenshotDir ?? defaultScreenshotDir;
1457
+ const defaultTimeout = options.defaultTimeoutMs ?? 3e4;
1458
+ const sessionId = crypto2.randomUUID();
1459
+ const trackingServer = new TrackingServer();
1460
+ await trackingServer.start();
1461
+ process.env.INTELLITESTER_SESSION_ID = sessionId;
1462
+ process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
1463
+ let serverProcess = null;
1464
+ if (options.webServer) {
1465
+ serverProcess = await startWebServer(options.webServer);
1466
+ }
1467
+ const cleanup = () => {
1468
+ trackingServer.stop();
1469
+ killServer(serverProcess);
1470
+ process.exit(1);
1471
+ };
1472
+ process.on("SIGINT", cleanup);
1473
+ process.on("SIGTERM", cleanup);
1474
+ const browser = await getBrowser(browserName).launch({ headless });
1475
+ const browserContext = await browser.newContext();
1476
+ const page = await browserContext.newPage();
1477
+ page.setDefaultTimeout(defaultTimeout);
1478
+ const executionContext = {
1479
+ variables: /* @__PURE__ */ new Map(),
1480
+ lastEmail: null,
1481
+ emailClient: null,
1482
+ appwriteContext: createTestContext(),
1483
+ appwriteConfig: test.config?.appwrite ? {
1484
+ endpoint: test.config.appwrite.endpoint,
1485
+ projectId: test.config.appwrite.projectId,
1486
+ apiKey: test.config.appwrite.apiKey
1487
+ } : void 0
1488
+ };
1489
+ if (test.config?.email) {
1490
+ const emailEndpoint = test.config.email.endpoint ?? process.env.INBUCKET_URL;
1491
+ if (!emailEndpoint) {
1492
+ throw new Error("Email testing requires endpoint in config or INBUCKET_URL env var");
1493
+ }
1494
+ executionContext.emailClient = new InbucketClient({
1495
+ endpoint: emailEndpoint
1496
+ });
1497
+ }
1498
+ page.on("response", async (response) => {
1499
+ const url = response.url();
1500
+ const method = response.request().method();
1501
+ try {
1502
+ if (method === "POST") {
1503
+ if (APPWRITE_PATTERNS.userCreate.test(url)) {
1504
+ const data = await response.json();
1505
+ executionContext.appwriteContext.userId = data.$id;
1506
+ executionContext.appwriteContext.userEmail = data.email;
1507
+ return;
1508
+ }
1509
+ const rowMatch = url.match(APPWRITE_PATTERNS.rowCreate);
1510
+ if (rowMatch) {
1511
+ const data = await response.json();
1512
+ executionContext.appwriteContext.resources.push({
1513
+ type: "row",
1514
+ id: data.$id,
1515
+ databaseId: rowMatch[1],
1516
+ tableId: rowMatch[2],
1517
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1518
+ });
1519
+ return;
1520
+ }
1521
+ const fileMatch = url.match(APPWRITE_PATTERNS.fileCreate);
1522
+ if (fileMatch) {
1523
+ const data = await response.json();
1524
+ executionContext.appwriteContext.resources.push({
1525
+ type: "file",
1526
+ id: data.$id,
1527
+ bucketId: fileMatch[1],
1528
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1529
+ });
1530
+ return;
1531
+ }
1532
+ const teamMatch = url.match(APPWRITE_PATTERNS.teamCreate);
1533
+ if (teamMatch) {
1534
+ const data = await response.json();
1535
+ executionContext.appwriteContext.resources.push({
1536
+ type: "team",
1537
+ id: data.$id,
1538
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1539
+ });
1540
+ return;
1541
+ }
1542
+ const membershipMatch = url.match(APPWRITE_PATTERNS.membershipCreate);
1543
+ if (membershipMatch) {
1544
+ const data = await response.json();
1545
+ executionContext.appwriteContext.resources.push({
1546
+ type: "membership",
1547
+ id: data.$id,
1548
+ teamId: membershipMatch[1],
1549
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1550
+ });
1551
+ return;
1552
+ }
1553
+ const messageMatch = url.match(APPWRITE_PATTERNS.messageCreate);
1554
+ if (messageMatch) {
1555
+ const data = await response.json();
1556
+ executionContext.appwriteContext.resources.push({
1557
+ type: "message",
1558
+ id: data.$id,
1559
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1560
+ });
1561
+ return;
1562
+ }
1563
+ }
1564
+ if (method === "PUT" || method === "PATCH") {
1565
+ const rowUpdateMatch = url.match(APPWRITE_UPDATE_PATTERNS.rowUpdate);
1566
+ if (rowUpdateMatch) {
1567
+ const resourceId = rowUpdateMatch[3];
1568
+ const existingResource = executionContext.appwriteContext.resources.find(
1569
+ (r) => r.type === "row" && r.id === resourceId
1570
+ );
1571
+ if (!existingResource) {
1572
+ executionContext.appwriteContext.resources.push({
1573
+ type: "row",
1574
+ id: resourceId,
1575
+ databaseId: rowUpdateMatch[1],
1576
+ tableId: rowUpdateMatch[2],
1577
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1578
+ });
1579
+ }
1580
+ return;
1581
+ }
1582
+ const fileUpdateMatch = url.match(APPWRITE_UPDATE_PATTERNS.fileUpdate);
1583
+ if (fileUpdateMatch) {
1584
+ const resourceId = fileUpdateMatch[2];
1585
+ const existingResource = executionContext.appwriteContext.resources.find(
1586
+ (r) => r.type === "file" && r.id === resourceId
1587
+ );
1588
+ if (!existingResource) {
1589
+ executionContext.appwriteContext.resources.push({
1590
+ type: "file",
1591
+ id: resourceId,
1592
+ bucketId: fileUpdateMatch[1],
1593
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1594
+ });
1595
+ }
1596
+ return;
1597
+ }
1598
+ const teamUpdateMatch = url.match(APPWRITE_UPDATE_PATTERNS.teamUpdate);
1599
+ if (teamUpdateMatch) {
1600
+ const resourceId = teamUpdateMatch[1];
1601
+ const existingResource = executionContext.appwriteContext.resources.find(
1602
+ (r) => r.type === "team" && r.id === resourceId
1603
+ );
1604
+ if (!existingResource) {
1605
+ executionContext.appwriteContext.resources.push({
1606
+ type: "team",
1607
+ id: resourceId,
1608
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1609
+ });
1610
+ }
1611
+ return;
1612
+ }
1613
+ }
1614
+ if (method === "DELETE") {
1615
+ const rowDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.rowDelete);
1616
+ if (rowDeleteMatch) {
1617
+ const resourceId = rowDeleteMatch[3];
1618
+ const resource = executionContext.appwriteContext.resources.find(
1619
+ (r) => r.type === "row" && r.id === resourceId
1620
+ );
1621
+ if (resource) {
1622
+ resource.deleted = true;
1623
+ }
1624
+ return;
1625
+ }
1626
+ const fileDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.fileDelete);
1627
+ if (fileDeleteMatch) {
1628
+ const resourceId = fileDeleteMatch[2];
1629
+ const resource = executionContext.appwriteContext.resources.find(
1630
+ (r) => r.type === "file" && r.id === resourceId
1631
+ );
1632
+ if (resource) {
1633
+ resource.deleted = true;
1634
+ }
1635
+ return;
1636
+ }
1637
+ const teamDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.teamDelete);
1638
+ if (teamDeleteMatch) {
1639
+ const resourceId = teamDeleteMatch[1];
1640
+ const resource = executionContext.appwriteContext.resources.find(
1641
+ (r) => r.type === "team" && r.id === resourceId
1642
+ );
1643
+ if (resource) {
1644
+ resource.deleted = true;
1645
+ }
1646
+ return;
1647
+ }
1648
+ const membershipDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.membershipDelete);
1649
+ if (membershipDeleteMatch) {
1650
+ const resourceId = membershipDeleteMatch[2];
1651
+ const resource = executionContext.appwriteContext.resources.find(
1652
+ (r) => r.type === "membership" && r.id === resourceId
1653
+ );
1654
+ if (resource) {
1655
+ resource.deleted = true;
1656
+ }
1657
+ return;
1658
+ }
1659
+ }
1660
+ } catch {
1661
+ }
1662
+ });
1663
+ if (test.variables) {
1664
+ for (const [key, value] of Object.entries(test.variables)) {
1665
+ const interpolated = interpolateVariables(value, executionContext.variables);
1666
+ executionContext.variables.set(key, interpolated);
1667
+ }
1668
+ }
1669
+ const results = [];
1670
+ const debugMode = options.debug ?? false;
1671
+ const interactive = options.interactive ?? false;
1672
+ try {
1673
+ for (const [index, action] of test.steps.entries()) {
1674
+ if (debugMode) {
1675
+ console.log(`[DEBUG] Executing step ${index + 1}: ${action.type}`);
1676
+ }
1677
+ const serverResources = trackingServer.getResources(sessionId);
1678
+ for (const resource of serverResources) {
1679
+ if (resource.type === "user" && !executionContext.appwriteContext.userId) {
1680
+ executionContext.appwriteContext.userId = resource.id;
1681
+ }
1682
+ const knownTypes = ["row", "file", "user", "team", "membership", "message"];
1683
+ if (knownTypes.includes(resource.type)) {
1684
+ const exists = executionContext.appwriteContext.resources.some(
1685
+ (r) => r.type === resource.type && r.id === resource.id
1686
+ );
1687
+ if (!exists) {
1688
+ executionContext.appwriteContext.resources.push({
1689
+ type: resource.type,
1690
+ id: resource.id,
1691
+ databaseId: resource.databaseId,
1692
+ tableId: resource.tableId,
1693
+ bucketId: resource.bucketId,
1694
+ teamId: resource.teamId,
1695
+ createdAt: resource.createdAt || (/* @__PURE__ */ new Date()).toISOString()
1696
+ });
1697
+ }
1698
+ }
1699
+ }
1700
+ try {
1701
+ if (action.type === "screenshot") {
1702
+ const screenshotPath = await runScreenshot(page, action.name, screenshotDir, index);
1703
+ results.push({ action, status: "passed", screenshotPath });
1704
+ continue;
1705
+ }
1706
+ await executeActionWithRetry(page, action, index, {
1707
+ baseUrl: options.baseUrl ?? test.config?.web?.baseUrl,
1708
+ context: executionContext,
1709
+ screenshotDir,
1710
+ debugMode,
1711
+ interactive,
1712
+ aiConfig: options.aiConfig
1713
+ });
1714
+ results.push({ action, status: "passed" });
1715
+ } catch (error) {
1716
+ const message = error instanceof Error ? error.message : String(error);
1717
+ results.push({ action, status: "failed", error: message });
1718
+ throw error;
1719
+ }
1720
+ }
1721
+ } finally {
1722
+ process.off("SIGINT", cleanup);
1723
+ process.off("SIGTERM", cleanup);
1724
+ if (test.config?.appwrite?.cleanup) {
1725
+ const appwriteClient = new AppwriteTestClient({
1726
+ endpoint: test.config.appwrite.endpoint,
1727
+ projectId: test.config.appwrite.projectId,
1728
+ apiKey: test.config.appwrite.apiKey,
1729
+ cleanup: true
1730
+ });
1731
+ const cleanupResult = await appwriteClient.cleanup(
1732
+ executionContext.appwriteContext,
1733
+ sessionId,
1734
+ process.cwd()
1735
+ );
1736
+ console.log("Cleanup result:", cleanupResult);
1737
+ }
1738
+ await browserContext.close();
1739
+ await browser.close();
1740
+ trackingServer.stop();
1741
+ killServer(serverProcess);
1742
+ }
1743
+ return {
1744
+ status: results.every((step) => step.status === "passed") ? "passed" : "failed",
1745
+ steps: results,
1746
+ variables: executionContext.variables
1747
+ };
1748
+ };
1749
+ var defaultScreenshotDir2 = path.join(process.cwd(), "artifacts", "screenshots");
1750
+ var getBrowser2 = (browser) => {
1751
+ switch (browser) {
1752
+ case "firefox":
1753
+ return firefox;
1754
+ case "webkit":
1755
+ return webkit;
1756
+ default:
1757
+ return chromium;
1758
+ }
1759
+ };
1760
+ function interpolateWorkflowVariables(value, currentVariables, testResults) {
1761
+ return value.replace(/\{\{([^}]+)\}\}/g, (match, path3) => {
1762
+ if (path3.includes(".")) {
1763
+ const [testId, _varName] = path3.split(".", 2);
1764
+ testResults.find((t) => t.id === testId);
1765
+ console.warn(`Cross-test variable interpolation {{${path3}}} not yet fully implemented`);
1766
+ return match;
1767
+ }
1768
+ if (path3 === "uuid") {
1769
+ return crypto2.randomUUID().split("-")[0];
1770
+ }
1771
+ return currentVariables.get(path3) ?? match;
1772
+ });
1773
+ }
1774
+ async function runTestInWorkflow(test, page, context, options, _workflowDir) {
1775
+ const results = [];
1776
+ const debugMode = options.debug ?? false;
1777
+ const screenshotDir = defaultScreenshotDir2;
1778
+ const resolveUrl2 = (value, baseUrl) => {
1779
+ if (!baseUrl) return value;
1780
+ try {
1781
+ const url = new URL(value, baseUrl);
1782
+ return url.toString();
1783
+ } catch {
1784
+ return value;
1785
+ }
1786
+ };
1787
+ const interpolateVariables2 = (value) => {
1788
+ return value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
1789
+ if (varName === "uuid") {
1790
+ return crypto2.randomUUID().split("-")[0];
1791
+ }
1792
+ return context.variables.get(varName) ?? match;
1793
+ });
1794
+ };
1795
+ const resolveLocator2 = (locator) => {
1796
+ if (locator.testId) return page.getByTestId(locator.testId);
1797
+ if (locator.text) return page.getByText(locator.text);
1798
+ if (locator.css) return page.locator(locator.css);
1799
+ if (locator.xpath) return page.locator(`xpath=${locator.xpath}`);
1800
+ if (locator.role) {
1801
+ const options2 = {};
1802
+ if (locator.name) options2.name = locator.name;
1803
+ return page.getByRole(locator.role, options2);
1804
+ }
1805
+ if (locator.description) return page.getByText(locator.description);
1806
+ throw new Error("No usable selector found for locator");
1807
+ };
1808
+ try {
1809
+ for (const [index, action] of test.steps.entries()) {
1810
+ if (debugMode) {
1811
+ console.log(` [DEBUG] Step ${index + 1}: ${action.type}`);
1812
+ }
1813
+ try {
1814
+ switch (action.type) {
1815
+ case "navigate": {
1816
+ const interpolated = interpolateVariables2(action.value);
1817
+ const target = resolveUrl2(interpolated, test.config?.web?.baseUrl);
1818
+ if (debugMode) console.log(` [DEBUG] Navigating to: ${target}`);
1819
+ await page.goto(target);
1820
+ break;
1821
+ }
1822
+ case "tap": {
1823
+ if (debugMode) console.log(` [DEBUG] Tapping element:`, action.target);
1824
+ const handle = resolveLocator2(action.target);
1825
+ await handle.click();
1826
+ break;
1827
+ }
1828
+ case "input": {
1829
+ const interpolated = interpolateVariables2(action.value);
1830
+ if (debugMode) console.log(` [DEBUG] Input: ${interpolated}`);
1831
+ const handle = resolveLocator2(action.target);
1832
+ await handle.fill(interpolated);
1833
+ break;
1834
+ }
1835
+ case "assert": {
1836
+ if (debugMode) console.log(` [DEBUG] Assert:`, action.target);
1837
+ const handle = resolveLocator2(action.target);
1838
+ await handle.waitFor({ state: "visible" });
1839
+ if (action.value) {
1840
+ const interpolated = interpolateVariables2(action.value);
1841
+ const text = (await handle.textContent())?.trim() ?? "";
1842
+ if (!text.includes(interpolated)) {
1843
+ throw new Error(
1844
+ `Assertion failed: expected "${interpolated}", got "${text}"`
1845
+ );
1846
+ }
1847
+ }
1848
+ break;
1849
+ }
1850
+ case "wait": {
1851
+ if (action.target) {
1852
+ const handle = resolveLocator2(action.target);
1853
+ await handle.waitFor({ state: "visible", timeout: action.timeout });
1854
+ } else {
1855
+ await page.waitForTimeout(action.timeout ?? 1e3);
1856
+ }
1857
+ break;
1858
+ }
1859
+ case "scroll": {
1860
+ if (action.target) {
1861
+ const handle = resolveLocator2(action.target);
1862
+ await handle.scrollIntoViewIfNeeded();
1863
+ } else {
1864
+ const amount = action.amount ?? 500;
1865
+ const direction = action.direction ?? "down";
1866
+ const deltaY = direction === "up" ? -amount : amount;
1867
+ await page.evaluate((value) => window.scrollBy(0, value), deltaY);
1868
+ }
1869
+ break;
1870
+ }
1871
+ case "screenshot": {
1872
+ const filename = action.name ?? `step-${index + 1}.png`;
1873
+ const filePath = path.join(screenshotDir, filename);
1874
+ await page.screenshot({ path: filePath, fullPage: true });
1875
+ results.push({ action, status: "passed", screenshotPath: filePath });
1876
+ continue;
1877
+ }
1878
+ case "setVar": {
1879
+ let value;
1880
+ if (action.value) {
1881
+ value = interpolateVariables2(action.value);
1882
+ } else if (action.from === "response") {
1883
+ throw new Error("setVar from response not yet implemented");
1884
+ } else if (action.from === "element") {
1885
+ throw new Error("setVar from element not yet implemented");
1886
+ } else if (action.from === "email") {
1887
+ throw new Error("Use email.extractCode or email.extractLink instead");
1888
+ } else {
1889
+ throw new Error("setVar requires value or from");
1890
+ }
1891
+ context.variables.set(action.name, value);
1892
+ if (debugMode) console.log(` [DEBUG] Set variable ${action.name} = ${value}`);
1893
+ break;
1894
+ }
1895
+ case "email.waitFor": {
1896
+ if (!context.emailClient) {
1897
+ throw new Error("Email client not configured");
1898
+ }
1899
+ const mailbox = interpolateVariables2(action.mailbox);
1900
+ context.lastEmail = await context.emailClient.waitForEmail(mailbox, {
1901
+ timeout: action.timeout,
1902
+ subjectContains: action.subjectContains
1903
+ });
1904
+ break;
1905
+ }
1906
+ case "email.extractCode": {
1907
+ if (!context.emailClient) {
1908
+ throw new Error("Email client not configured");
1909
+ }
1910
+ if (!context.lastEmail) {
1911
+ throw new Error("No email loaded - call email.waitFor first");
1912
+ }
1913
+ const code = context.emailClient.extractCode(
1914
+ context.lastEmail,
1915
+ action.pattern ? new RegExp(action.pattern) : void 0
1916
+ );
1917
+ if (!code) {
1918
+ throw new Error("No code found in email");
1919
+ }
1920
+ context.variables.set(action.saveTo, code);
1921
+ break;
1922
+ }
1923
+ case "email.extractLink": {
1924
+ if (!context.emailClient) {
1925
+ throw new Error("Email client not configured");
1926
+ }
1927
+ if (!context.lastEmail) {
1928
+ throw new Error("No email loaded - call email.waitFor first");
1929
+ }
1930
+ const link = context.emailClient.extractLink(
1931
+ context.lastEmail,
1932
+ action.pattern ? new RegExp(action.pattern) : void 0
1933
+ );
1934
+ if (!link) {
1935
+ throw new Error("No link found in email");
1936
+ }
1937
+ context.variables.set(action.saveTo, link);
1938
+ break;
1939
+ }
1940
+ case "email.clear": {
1941
+ if (!context.emailClient) {
1942
+ throw new Error("Email client not configured");
1943
+ }
1944
+ const mailbox = interpolateVariables2(action.mailbox);
1945
+ await context.emailClient.clearMailbox(mailbox);
1946
+ break;
1947
+ }
1948
+ case "appwrite.verifyEmail": {
1949
+ if (!context.appwriteContext.userId) {
1950
+ throw new Error("No user tracked. appwrite.verifyEmail requires a user signup first.");
1951
+ }
1952
+ if (!context.appwriteConfig?.apiKey) {
1953
+ throw new Error("appwrite.verifyEmail requires appwrite.apiKey in config");
1954
+ }
1955
+ const { Client: Client2, Users: Users2 } = await import('node-appwrite');
1956
+ const client = new Client2().setEndpoint(context.appwriteConfig.endpoint).setProject(context.appwriteConfig.projectId).setKey(context.appwriteConfig.apiKey);
1957
+ const users = new Users2(client);
1958
+ await users.updateEmailVerification(context.appwriteContext.userId, true);
1959
+ if (debugMode) console.log(` [DEBUG] Verified email for user ${context.appwriteContext.userId}`);
1960
+ break;
1961
+ }
1962
+ case "debug": {
1963
+ console.log(" [DEBUG] Pausing execution - Playwright Inspector will open");
1964
+ await page.pause();
1965
+ break;
1966
+ }
1967
+ default:
1968
+ throw new Error(`Unsupported action type: ${action.type}`);
1969
+ }
1970
+ results.push({ action, status: "passed" });
1971
+ } catch (error) {
1972
+ const message = error instanceof Error ? error.message : String(error);
1973
+ results.push({ action, status: "failed", error: message });
1974
+ throw error;
1975
+ }
1976
+ }
1977
+ return {
1978
+ status: "passed",
1979
+ steps: results
1980
+ };
1981
+ } catch {
1982
+ return {
1983
+ status: "failed",
1984
+ steps: results
1985
+ };
1986
+ }
1987
+ }
1988
+ function setupAppwriteTracking(page, context) {
1989
+ page.on("response", async (response) => {
1990
+ const url = response.url();
1991
+ const method = response.request().method();
1992
+ try {
1993
+ if (method === "POST") {
1994
+ if (APPWRITE_PATTERNS.userCreate.test(url)) {
1995
+ const data = await response.json();
1996
+ context.appwriteContext.userId = data.$id;
1997
+ context.appwriteContext.userEmail = data.email;
1998
+ return;
1999
+ }
2000
+ const rowMatch = url.match(APPWRITE_PATTERNS.rowCreate);
2001
+ if (rowMatch) {
2002
+ const data = await response.json();
2003
+ context.appwriteContext.resources.push({
2004
+ type: "row",
2005
+ id: data.$id,
2006
+ databaseId: rowMatch[1],
2007
+ tableId: rowMatch[2],
2008
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2009
+ });
2010
+ return;
2011
+ }
2012
+ const fileMatch = url.match(APPWRITE_PATTERNS.fileCreate);
2013
+ if (fileMatch) {
2014
+ const data = await response.json();
2015
+ context.appwriteContext.resources.push({
2016
+ type: "file",
2017
+ id: data.$id,
2018
+ bucketId: fileMatch[1],
2019
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2020
+ });
2021
+ return;
2022
+ }
2023
+ const teamMatch = url.match(APPWRITE_PATTERNS.teamCreate);
2024
+ if (teamMatch) {
2025
+ const data = await response.json();
2026
+ context.appwriteContext.resources.push({
2027
+ type: "team",
2028
+ id: data.$id,
2029
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2030
+ });
2031
+ return;
2032
+ }
2033
+ const membershipMatch = url.match(APPWRITE_PATTERNS.membershipCreate);
2034
+ if (membershipMatch) {
2035
+ const data = await response.json();
2036
+ context.appwriteContext.resources.push({
2037
+ type: "membership",
2038
+ id: data.$id,
2039
+ teamId: membershipMatch[1],
2040
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2041
+ });
2042
+ return;
2043
+ }
2044
+ const messageMatch = url.match(APPWRITE_PATTERNS.messageCreate);
2045
+ if (messageMatch) {
2046
+ const data = await response.json();
2047
+ context.appwriteContext.resources.push({
2048
+ type: "message",
2049
+ id: data.$id,
2050
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2051
+ });
2052
+ return;
2053
+ }
2054
+ }
2055
+ if (method === "PUT" || method === "PATCH") {
2056
+ const rowUpdateMatch = url.match(APPWRITE_UPDATE_PATTERNS.rowUpdate);
2057
+ if (rowUpdateMatch) {
2058
+ const resourceId = rowUpdateMatch[3];
2059
+ const existing = context.appwriteContext.resources.find(
2060
+ (r) => r.type === "row" && r.id === resourceId
2061
+ );
2062
+ if (!existing) {
2063
+ context.appwriteContext.resources.push({
2064
+ type: "row",
2065
+ id: resourceId,
2066
+ databaseId: rowUpdateMatch[1],
2067
+ tableId: rowUpdateMatch[2],
2068
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2069
+ });
2070
+ }
2071
+ return;
2072
+ }
2073
+ const fileUpdateMatch = url.match(APPWRITE_UPDATE_PATTERNS.fileUpdate);
2074
+ if (fileUpdateMatch) {
2075
+ const resourceId = fileUpdateMatch[2];
2076
+ const existing = context.appwriteContext.resources.find(
2077
+ (r) => r.type === "file" && r.id === resourceId
2078
+ );
2079
+ if (!existing) {
2080
+ context.appwriteContext.resources.push({
2081
+ type: "file",
2082
+ id: resourceId,
2083
+ bucketId: fileUpdateMatch[1],
2084
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2085
+ });
2086
+ }
2087
+ return;
2088
+ }
2089
+ const teamUpdateMatch = url.match(APPWRITE_UPDATE_PATTERNS.teamUpdate);
2090
+ if (teamUpdateMatch) {
2091
+ const resourceId = teamUpdateMatch[1];
2092
+ const existing = context.appwriteContext.resources.find(
2093
+ (r) => r.type === "team" && r.id === resourceId
2094
+ );
2095
+ if (!existing) {
2096
+ context.appwriteContext.resources.push({
2097
+ type: "team",
2098
+ id: resourceId,
2099
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2100
+ });
2101
+ }
2102
+ return;
2103
+ }
2104
+ }
2105
+ if (method === "DELETE") {
2106
+ const rowDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.rowDelete);
2107
+ if (rowDeleteMatch) {
2108
+ const resource = context.appwriteContext.resources.find(
2109
+ (r) => r.type === "row" && r.id === rowDeleteMatch[3]
2110
+ );
2111
+ if (resource) resource.deleted = true;
2112
+ return;
2113
+ }
2114
+ const fileDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.fileDelete);
2115
+ if (fileDeleteMatch) {
2116
+ const resource = context.appwriteContext.resources.find(
2117
+ (r) => r.type === "file" && r.id === fileDeleteMatch[2]
2118
+ );
2119
+ if (resource) resource.deleted = true;
2120
+ return;
2121
+ }
2122
+ const teamDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.teamDelete);
2123
+ if (teamDeleteMatch) {
2124
+ const resource = context.appwriteContext.resources.find(
2125
+ (r) => r.type === "team" && r.id === teamDeleteMatch[1]
2126
+ );
2127
+ if (resource) resource.deleted = true;
2128
+ return;
2129
+ }
2130
+ const membershipDeleteMatch = url.match(APPWRITE_DELETE_PATTERNS.membershipDelete);
2131
+ if (membershipDeleteMatch) {
2132
+ const resource = context.appwriteContext.resources.find(
2133
+ (r) => r.type === "membership" && r.id === membershipDeleteMatch[2]
2134
+ );
2135
+ if (resource) resource.deleted = true;
2136
+ return;
2137
+ }
2138
+ }
2139
+ } catch {
2140
+ }
2141
+ });
2142
+ }
2143
+ function inferCleanupConfig(config) {
2144
+ if (!config) return void 0;
2145
+ if (config.cleanup) {
2146
+ return config.cleanup;
2147
+ }
2148
+ if (config.appwrite?.cleanup) {
2149
+ return {
2150
+ provider: "appwrite",
2151
+ appwrite: {
2152
+ endpoint: config.appwrite.endpoint,
2153
+ projectId: config.appwrite.projectId,
2154
+ apiKey: config.appwrite.apiKey,
2155
+ cleanupOnFailure: config.appwrite.cleanupOnFailure
2156
+ }
2157
+ };
2158
+ }
2159
+ return void 0;
2160
+ }
2161
+ async function runWorkflowWithContext(workflow, workflowFilePath, options) {
2162
+ const { page, executionContext, skipCleanup = false, sessionId: providedSessionId, testStartTime: providedTestStartTime } = options;
2163
+ const workflowDir = path.dirname(workflowFilePath);
2164
+ const sessionId = providedSessionId ?? crypto2.randomUUID();
2165
+ const testStartTime = providedTestStartTime ?? (/* @__PURE__ */ new Date()).toISOString();
2166
+ console.log(`
2167
+ Starting workflow: ${workflow.name}`);
2168
+ console.log(`Session ID: ${sessionId}
2169
+ `);
2170
+ if (workflow.config?.appwrite) {
2171
+ if (!executionContext.appwriteConfig) {
2172
+ executionContext.appwriteConfig = {
2173
+ endpoint: workflow.config.appwrite.endpoint,
2174
+ projectId: workflow.config.appwrite.projectId,
2175
+ apiKey: workflow.config.appwrite.apiKey
2176
+ };
2177
+ }
2178
+ setupAppwriteTracking(page, executionContext);
2179
+ }
2180
+ const testResults = [];
2181
+ let workflowFailed = false;
2182
+ for (const [index, testRef] of workflow.tests.entries()) {
2183
+ const testFilePath = path.resolve(workflowDir, testRef.file);
2184
+ console.log(`
2185
+ [${index + 1}/${workflow.tests.length}] Running: ${testRef.file}`);
2186
+ if (testRef.id) {
2187
+ console.log(` Test ID: ${testRef.id}`);
2188
+ }
2189
+ try {
2190
+ const test = await loadTestDefinition(testFilePath);
2191
+ if (testRef.variables) {
2192
+ for (const [key, value] of Object.entries(testRef.variables)) {
2193
+ const interpolated = interpolateWorkflowVariables(
2194
+ value,
2195
+ executionContext.variables,
2196
+ testResults
2197
+ );
2198
+ if (!test.variables) test.variables = {};
2199
+ test.variables[key] = interpolated;
2200
+ executionContext.variables.set(key, interpolated);
2201
+ }
2202
+ }
2203
+ if (test.variables) {
2204
+ for (const [key, value] of Object.entries(test.variables)) {
2205
+ const interpolated = value.replace(/\{\{(\w+)\}\}/g, (match, varName) => {
2206
+ if (varName === "uuid") {
2207
+ return crypto2.randomUUID().split("-")[0];
2208
+ }
2209
+ return executionContext.variables.get(varName) ?? match;
2210
+ });
2211
+ executionContext.variables.set(key, interpolated);
2212
+ }
2213
+ }
2214
+ const result = await runTestInWorkflow(test, page, executionContext, options, workflowDir);
2215
+ const testResult = {
2216
+ id: testRef.id,
2217
+ file: testRef.file,
2218
+ status: result.status,
2219
+ steps: result.steps
2220
+ };
2221
+ testResults.push(testResult);
2222
+ if (result.status === "passed") {
2223
+ console.log(` \u2713 Passed (${result.steps.length} steps)`);
2224
+ } else {
2225
+ console.log(` \u2717 Failed`);
2226
+ const failedStep = result.steps.find((s) => s.status === "failed");
2227
+ if (failedStep) {
2228
+ console.log(` Error: ${failedStep.error}`);
2229
+ testResult.error = failedStep.error;
2230
+ }
2231
+ if (!workflow.continueOnFailure) {
2232
+ workflowFailed = true;
2233
+ break;
2234
+ }
2235
+ }
2236
+ } catch (error) {
2237
+ const message = error instanceof Error ? error.message : String(error);
2238
+ console.log(` \u2717 Failed to load/run test: ${message}`);
2239
+ testResults.push({
2240
+ id: testRef.id,
2241
+ file: testRef.file,
2242
+ status: "failed",
2243
+ steps: [],
2244
+ error: message
2245
+ });
2246
+ if (!workflow.continueOnFailure) {
2247
+ workflowFailed = true;
2248
+ break;
2249
+ }
2250
+ }
2251
+ }
2252
+ let cleanupResult;
2253
+ if (!skipCleanup) {
2254
+ const cleanupConfig = inferCleanupConfig(workflow.config);
2255
+ if (cleanupConfig) {
2256
+ const appwriteConfig = cleanupConfig.appwrite;
2257
+ const cleanupOnFailure = appwriteConfig?.cleanupOnFailure ?? true;
2258
+ const shouldCleanup = workflowFailed ? cleanupOnFailure : true;
2259
+ if (shouldCleanup) {
2260
+ try {
2261
+ console.log("\n[Cleanup] Starting cleanup...");
2262
+ const { handlers, typeMappings, provider } = await loadCleanupHandlers(
2263
+ cleanupConfig,
2264
+ process.cwd()
2265
+ );
2266
+ const genericResources = executionContext.appwriteContext.resources.map((r) => ({
2267
+ ...r
2268
+ }));
2269
+ const providerConfig = {
2270
+ provider: cleanupConfig.provider || "appwrite"
2271
+ };
2272
+ if (cleanupConfig.provider === "appwrite" && cleanupConfig.appwrite) {
2273
+ const appwriteCleanupConfig = cleanupConfig.appwrite;
2274
+ providerConfig.endpoint = appwriteCleanupConfig.endpoint;
2275
+ providerConfig.projectId = appwriteCleanupConfig.projectId;
2276
+ } else if (cleanupConfig.provider === "postgres" && cleanupConfig.postgres) {
2277
+ const pgConfig = cleanupConfig.postgres;
2278
+ const connString = pgConfig.connectionString;
2279
+ if (connString) {
2280
+ try {
2281
+ const url = new URL(connString.replace("postgresql://", "http://"));
2282
+ providerConfig.host = url.hostname;
2283
+ providerConfig.port = url.port;
2284
+ providerConfig.database = url.pathname.slice(1);
2285
+ providerConfig.user = url.username;
2286
+ } catch {
2287
+ providerConfig.configured = true;
2288
+ }
2289
+ }
2290
+ } else if (cleanupConfig.provider === "mysql" && cleanupConfig.mysql) {
2291
+ const mysqlConfig = cleanupConfig.mysql;
2292
+ providerConfig.host = mysqlConfig.host;
2293
+ providerConfig.port = mysqlConfig.port;
2294
+ providerConfig.database = mysqlConfig.database;
2295
+ providerConfig.user = mysqlConfig.user;
2296
+ } else if (cleanupConfig.provider === "sqlite" && cleanupConfig.sqlite) {
2297
+ const sqliteConfig = cleanupConfig.sqlite;
2298
+ providerConfig.database = sqliteConfig.database;
2299
+ }
2300
+ cleanupResult = await executeCleanup(
2301
+ genericResources,
2302
+ handlers,
2303
+ typeMappings,
2304
+ {
2305
+ parallel: cleanupConfig.parallel ?? false,
2306
+ retries: cleanupConfig.retries ?? 3,
2307
+ sessionId,
2308
+ testStartTime,
2309
+ userId: executionContext.appwriteContext.userId,
2310
+ providerConfig,
2311
+ cwd: process.cwd(),
2312
+ config: cleanupConfig,
2313
+ provider
2314
+ }
2315
+ );
2316
+ if (cleanupResult.success) {
2317
+ console.log(`[Cleanup] Cleanup complete: ${cleanupResult.deleted.length} resources deleted`);
2318
+ } else {
2319
+ console.log(`[Cleanup] Cleanup partial: ${cleanupResult.deleted.length} deleted, ${cleanupResult.failed.length} failed`);
2320
+ for (const failed of cleanupResult.failed) {
2321
+ console.log(` - ${failed}`);
2322
+ }
2323
+ }
2324
+ } catch (error) {
2325
+ console.error("[Cleanup] Cleanup failed:", error);
2326
+ }
2327
+ } else {
2328
+ console.log("\nSkipping cleanup (cleanupOnFailure is false)");
2329
+ }
2330
+ }
2331
+ }
2332
+ const overallStatus = testResults.every((t) => t.status === "passed") ? "passed" : "failed";
2333
+ console.log(`
2334
+ ${"=".repeat(60)}`);
2335
+ console.log(`Workflow: ${overallStatus === "passed" ? "\u2713 PASSED" : "\u2717 FAILED"}`);
2336
+ console.log(`Tests: ${testResults.filter((t) => t.status === "passed").length}/${testResults.length} passed`);
2337
+ console.log(`${"=".repeat(60)}
2338
+ `);
2339
+ return {
2340
+ status: overallStatus,
2341
+ tests: testResults,
2342
+ sessionId,
2343
+ cleanupResult,
2344
+ workflowFailed
2345
+ };
2346
+ }
2347
+ async function runWorkflow(workflow, workflowFilePath, options = {}) {
2348
+ const workflowDir = path.dirname(workflowFilePath);
2349
+ const sessionId = crypto2.randomUUID();
2350
+ const testStartTime = (/* @__PURE__ */ new Date()).toISOString();
2351
+ let trackingServer = null;
2352
+ try {
2353
+ trackingServer = await startTrackingServer({ port: 0 });
2354
+ console.log(`Tracking server started on port ${trackingServer.port}`);
2355
+ } catch (error) {
2356
+ console.warn("Failed to start tracking server:", error);
2357
+ }
2358
+ if (trackingServer) {
2359
+ process.env.INTELLITESTER_SESSION_ID = sessionId;
2360
+ process.env.INTELLITESTER_TRACK_URL = `http://localhost:${trackingServer.port}`;
2361
+ }
2362
+ let serverProcess = null;
2363
+ if (workflow.config?.webServer) {
2364
+ try {
2365
+ serverProcess = await startWebServer({
2366
+ ...workflow.config.webServer,
2367
+ cwd: workflowDir
2368
+ });
2369
+ } catch (error) {
2370
+ console.error("Failed to start web server:", error);
2371
+ if (trackingServer) await trackingServer.stop();
2372
+ throw error;
2373
+ }
2374
+ }
2375
+ const signalCleanup = async () => {
2376
+ console.log("\n\nInterrupted - cleaning up...");
2377
+ killServer(serverProcess);
2378
+ if (trackingServer) await trackingServer.stop();
2379
+ process.exit(1);
2380
+ };
2381
+ process.on("SIGINT", signalCleanup);
2382
+ process.on("SIGTERM", signalCleanup);
2383
+ const browserName = options.browser ?? workflow.config?.web?.browser ?? "chromium";
2384
+ const headless = options.headed ? false : workflow.config?.web?.headless ?? true;
2385
+ const browser = await getBrowser2(browserName).launch({ headless });
2386
+ const browserContext = await browser.newContext();
2387
+ const page = await browserContext.newPage();
2388
+ page.setDefaultTimeout(3e4);
2389
+ const executionContext = {
2390
+ variables: /* @__PURE__ */ new Map(),
2391
+ lastEmail: null,
2392
+ emailClient: null,
2393
+ appwriteContext: createTestContext(),
2394
+ appwriteConfig: workflow.config?.appwrite ? {
2395
+ endpoint: workflow.config.appwrite.endpoint,
2396
+ projectId: workflow.config.appwrite.projectId,
2397
+ apiKey: workflow.config.appwrite.apiKey
2398
+ } : void 0
2399
+ };
2400
+ try {
2401
+ const result = await runWorkflowWithContext(workflow, workflowFilePath, {
2402
+ ...options,
2403
+ page,
2404
+ executionContext,
2405
+ skipCleanup: true,
2406
+ sessionId,
2407
+ testStartTime
2408
+ });
2409
+ if (trackingServer) {
2410
+ const serverResources = trackingServer.getResources(sessionId);
2411
+ if (serverResources.length > 0) {
2412
+ console.log(`
2413
+ Collected ${serverResources.length} server-tracked resources`);
2414
+ executionContext.appwriteContext.resources.push(...serverResources);
2415
+ }
2416
+ }
2417
+ let cleanupResult;
2418
+ const cleanupConfig = inferCleanupConfig(workflow.config);
2419
+ if (cleanupConfig) {
2420
+ const appwriteConfig = cleanupConfig.appwrite;
2421
+ const cleanupOnFailure = appwriteConfig?.cleanupOnFailure ?? true;
2422
+ const shouldCleanup = result.workflowFailed ? cleanupOnFailure : true;
2423
+ if (shouldCleanup) {
2424
+ try {
2425
+ console.log("\n[Cleanup] Starting cleanup...");
2426
+ const { handlers, typeMappings, provider } = await loadCleanupHandlers(
2427
+ cleanupConfig,
2428
+ process.cwd()
2429
+ );
2430
+ const genericResources = executionContext.appwriteContext.resources.map((r) => ({
2431
+ ...r
2432
+ }));
2433
+ const providerConfig = {
2434
+ provider: cleanupConfig.provider || "appwrite"
2435
+ };
2436
+ if (cleanupConfig.provider === "appwrite" && cleanupConfig.appwrite) {
2437
+ const appwriteCleanupConfig = cleanupConfig.appwrite;
2438
+ providerConfig.endpoint = appwriteCleanupConfig.endpoint;
2439
+ providerConfig.projectId = appwriteCleanupConfig.projectId;
2440
+ } else if (cleanupConfig.provider === "postgres" && cleanupConfig.postgres) {
2441
+ const pgConfig = cleanupConfig.postgres;
2442
+ const connString = pgConfig.connectionString;
2443
+ if (connString) {
2444
+ try {
2445
+ const url = new URL(connString.replace("postgresql://", "http://"));
2446
+ providerConfig.host = url.hostname;
2447
+ providerConfig.port = url.port;
2448
+ providerConfig.database = url.pathname.slice(1);
2449
+ providerConfig.user = url.username;
2450
+ } catch {
2451
+ providerConfig.configured = true;
2452
+ }
2453
+ }
2454
+ } else if (cleanupConfig.provider === "mysql" && cleanupConfig.mysql) {
2455
+ const mysqlConfig = cleanupConfig.mysql;
2456
+ providerConfig.host = mysqlConfig.host;
2457
+ providerConfig.port = mysqlConfig.port;
2458
+ providerConfig.database = mysqlConfig.database;
2459
+ providerConfig.user = mysqlConfig.user;
2460
+ } else if (cleanupConfig.provider === "sqlite" && cleanupConfig.sqlite) {
2461
+ const sqliteConfig = cleanupConfig.sqlite;
2462
+ providerConfig.database = sqliteConfig.database;
2463
+ }
2464
+ cleanupResult = await executeCleanup(
2465
+ genericResources,
2466
+ handlers,
2467
+ typeMappings,
2468
+ {
2469
+ parallel: cleanupConfig.parallel ?? false,
2470
+ retries: cleanupConfig.retries ?? 3,
2471
+ sessionId,
2472
+ testStartTime,
2473
+ userId: executionContext.appwriteContext.userId,
2474
+ providerConfig,
2475
+ cwd: process.cwd(),
2476
+ config: cleanupConfig,
2477
+ provider
2478
+ }
2479
+ );
2480
+ if (cleanupResult.success) {
2481
+ console.log(`[Cleanup] Cleanup complete: ${cleanupResult.deleted.length} resources deleted`);
2482
+ } else {
2483
+ console.log(`[Cleanup] Cleanup partial: ${cleanupResult.deleted.length} deleted, ${cleanupResult.failed.length} failed`);
2484
+ for (const failed of cleanupResult.failed) {
2485
+ console.log(` - ${failed}`);
2486
+ }
2487
+ }
2488
+ } catch (error) {
2489
+ console.error("[Cleanup] Cleanup failed:", error);
2490
+ }
2491
+ } else {
2492
+ console.log("\nSkipping cleanup (cleanupOnFailure is false)");
2493
+ }
2494
+ }
2495
+ return {
2496
+ status: result.status,
2497
+ tests: result.tests,
2498
+ sessionId,
2499
+ cleanupResult
2500
+ };
2501
+ } finally {
2502
+ process.off("SIGINT", signalCleanup);
2503
+ process.off("SIGTERM", signalCleanup);
2504
+ await browserContext.close();
2505
+ await browser.close();
2506
+ killServer(serverProcess);
2507
+ if (trackingServer) {
2508
+ await trackingServer.stop();
2509
+ }
2510
+ delete process.env.INTELLITESTER_SESSION_ID;
2511
+ delete process.env.INTELLITESTER_TRACK_URL;
2512
+ }
2513
+ }
2514
+
2515
+ export { ActionSchema, IntellitesterConfigSchema, LocatorSchema, TestConfigSchema, TestDefinitionSchema, cleanupConfigSchema, cleanupDiscoverSchema, collectMissingEnvVars, createAIProvider, createTestContext, isPipelineFile, isWorkflowFile, killServer, loadIntellitesterConfig, loadPipelineDefinition, loadTestDefinition, loadWorkflowDefinition, parseIntellitesterConfig, parsePipelineDefinition, parseTestDefinition, parseWorkflowDefinition, runWebTest, runWorkflow, runWorkflowWithContext, setupAppwriteTracking, startTrackingServer, startWebServer };
2516
+ //# sourceMappingURL=chunk-5LFSLMQ7.js.map
2517
+ //# sourceMappingURL=chunk-5LFSLMQ7.js.map