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.
- package/README.md +200 -0
- package/dist/chunk-35WJGNDA.cjs +136 -0
- package/dist/chunk-35WJGNDA.cjs.map +1 -0
- package/dist/chunk-4B54JUOP.js +234 -0
- package/dist/chunk-4B54JUOP.js.map +1 -0
- package/dist/chunk-5LFSLMQ7.js +2517 -0
- package/dist/chunk-5LFSLMQ7.js.map +1 -0
- package/dist/chunk-6PYKWWH5.js +63 -0
- package/dist/chunk-6PYKWWH5.js.map +1 -0
- package/dist/chunk-ARJYJVRM.cjs +302 -0
- package/dist/chunk-ARJYJVRM.cjs.map +1 -0
- package/dist/chunk-CN6HSJJX.js +133 -0
- package/dist/chunk-CN6HSJJX.js.map +1 -0
- package/dist/chunk-DE5UFTTG.js +31 -0
- package/dist/chunk-DE5UFTTG.js.map +1 -0
- package/dist/chunk-ECBA4GJ3.js +287 -0
- package/dist/chunk-ECBA4GJ3.js.map +1 -0
- package/dist/chunk-OFXNJXMV.cjs +237 -0
- package/dist/chunk-OFXNJXMV.cjs.map +1 -0
- package/dist/chunk-PAKODOH4.cjs +66 -0
- package/dist/chunk-PAKODOH4.cjs.map +1 -0
- package/dist/chunk-QMYM2TCH.cjs +36 -0
- package/dist/chunk-QMYM2TCH.cjs.map +1 -0
- package/dist/chunk-SAVY6D3X.js +125 -0
- package/dist/chunk-SAVY6D3X.js.map +1 -0
- package/dist/chunk-UUJXCHVT.cjs +128 -0
- package/dist/chunk-UUJXCHVT.cjs.map +1 -0
- package/dist/chunk-XWGUA67E.cjs +2552 -0
- package/dist/chunk-XWGUA67E.cjs.map +1 -0
- package/dist/cli/index.cjs +1985 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +1957 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/cleanup/index.cjs +45 -0
- package/dist/core/cleanup/index.cjs.map +1 -0
- package/dist/core/cleanup/index.d.cts +117 -0
- package/dist/core/cleanup/index.d.ts +117 -0
- package/dist/core/cleanup/index.js +8 -0
- package/dist/core/cleanup/index.js.map +1 -0
- package/dist/index.cjs +110 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +852 -0
- package/dist/index.d.ts +852 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/index.cjs +22 -0
- package/dist/integration/index.cjs.map +1 -0
- package/dist/integration/index.d.cts +42 -0
- package/dist/integration/index.d.ts +42 -0
- package/dist/integration/index.js +20 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/providers/appwrite/index.cjs +16 -0
- package/dist/providers/appwrite/index.cjs.map +1 -0
- package/dist/providers/appwrite/index.d.cts +12 -0
- package/dist/providers/appwrite/index.d.ts +12 -0
- package/dist/providers/appwrite/index.js +3 -0
- package/dist/providers/appwrite/index.js.map +1 -0
- package/dist/providers/index.cjs +60 -0
- package/dist/providers/index.cjs.map +1 -0
- package/dist/providers/index.d.cts +13 -0
- package/dist/providers/index.d.ts +13 -0
- package/dist/providers/index.js +7 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/mysql/index.cjs +16 -0
- package/dist/providers/mysql/index.cjs.map +1 -0
- package/dist/providers/mysql/index.d.cts +14 -0
- package/dist/providers/mysql/index.d.ts +14 -0
- package/dist/providers/mysql/index.js +3 -0
- package/dist/providers/mysql/index.js.map +1 -0
- package/dist/providers/postgres/index.cjs +16 -0
- package/dist/providers/postgres/index.cjs.map +1 -0
- package/dist/providers/postgres/index.d.cts +10 -0
- package/dist/providers/postgres/index.d.ts +10 -0
- package/dist/providers/postgres/index.js +3 -0
- package/dist/providers/postgres/index.js.map +1 -0
- package/dist/providers/sqlite/index.cjs +16 -0
- package/dist/providers/sqlite/index.cjs.map +1 -0
- package/dist/providers/sqlite/index.d.cts +11 -0
- package/dist/providers/sqlite/index.d.ts +11 -0
- package/dist/providers/sqlite/index.js +3 -0
- package/dist/providers/sqlite/index.js.map +1 -0
- package/dist/types-LONNVTIF.d.cts +56 -0
- package/dist/types-l-ZaFKC-.d.ts +56 -0
- package/package.json +114 -0
- package/schemas/intellitester.config.schema.json +384 -0
- package/schemas/test.schema.json +517 -0
- 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
|