@testdino/playwright 1.0.1
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/LICENSE +71 -0
- package/README.md +307 -0
- package/bin/tdpw.js +12 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +759 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/index.mjs +751 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/index.d.mts +779 -0
- package/dist/index.d.ts +779 -0
- package/dist/index.js +2695 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +2688 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +77 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { readFileSync, existsSync, statSync, writeFileSync, unlinkSync } from 'fs';
|
|
5
|
+
import { dirname, join } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import jiti from 'jiti';
|
|
8
|
+
import { randomUUID } from 'crypto';
|
|
9
|
+
import { tmpdir } from 'os';
|
|
10
|
+
import { execa } from 'execa';
|
|
11
|
+
|
|
12
|
+
var CONFIG_FILENAMES = ["testdino.config.ts", "testdino.config.js"];
|
|
13
|
+
var ConfigLoader = class {
|
|
14
|
+
cwd;
|
|
15
|
+
constructor(cwd = process.cwd()) {
|
|
16
|
+
this.cwd = cwd;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Load config file from current directory or parent directories
|
|
20
|
+
*/
|
|
21
|
+
async load() {
|
|
22
|
+
const configPath = this.findConfigFile();
|
|
23
|
+
if (!configPath) {
|
|
24
|
+
return { config: {} };
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const config = this.loadConfigFile(configPath);
|
|
28
|
+
return { config, configPath };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Failed to load config file: ${configPath}
|
|
32
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Find config file by searching up directory tree
|
|
38
|
+
* Stops at .git directory
|
|
39
|
+
*/
|
|
40
|
+
findConfigFile() {
|
|
41
|
+
let currentDir = this.cwd;
|
|
42
|
+
while (true) {
|
|
43
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
44
|
+
const configPath = join(currentDir, filename);
|
|
45
|
+
if (existsSync(configPath)) {
|
|
46
|
+
return configPath;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const gitDir = join(currentDir, ".git");
|
|
50
|
+
if (existsSync(gitDir) && statSync(gitDir).isDirectory()) {
|
|
51
|
+
for (const filename of CONFIG_FILENAMES) {
|
|
52
|
+
const configPath = join(currentDir, filename);
|
|
53
|
+
if (existsSync(configPath)) {
|
|
54
|
+
return configPath;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
const parentDir = dirname(currentDir);
|
|
60
|
+
if (parentDir === currentDir) {
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
currentDir = parentDir;
|
|
64
|
+
}
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Load and parse config file using jiti
|
|
69
|
+
*/
|
|
70
|
+
loadConfigFile(configPath) {
|
|
71
|
+
const jitiLoader = jiti(dirname(configPath), {
|
|
72
|
+
interopDefault: true,
|
|
73
|
+
cache: false,
|
|
74
|
+
extensions: [".ts", ".js"]
|
|
75
|
+
});
|
|
76
|
+
let loaded;
|
|
77
|
+
try {
|
|
78
|
+
const resolved = jitiLoader.esmResolve(configPath, { try: true });
|
|
79
|
+
if (!resolved) {
|
|
80
|
+
throw new Error(`Could not resolve config file: ${configPath}`);
|
|
81
|
+
}
|
|
82
|
+
const resolvedPath = typeof resolved === "string" ? resolved : fileURLToPath(resolved);
|
|
83
|
+
loaded = jitiLoader(resolvedPath);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error(`Syntax error in config file:
|
|
86
|
+
${error instanceof Error ? error.message : String(error)}`);
|
|
87
|
+
}
|
|
88
|
+
let config;
|
|
89
|
+
if (loaded && typeof loaded === "object" && "__esModule" in loaded) {
|
|
90
|
+
config = loaded.default;
|
|
91
|
+
} else if (loaded && typeof loaded === "object" && "default" in loaded) {
|
|
92
|
+
config = loaded.default;
|
|
93
|
+
} else {
|
|
94
|
+
config = loaded;
|
|
95
|
+
}
|
|
96
|
+
if (config === null || config === void 0) {
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
if (typeof config === "function") {
|
|
100
|
+
try {
|
|
101
|
+
config = config();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new Error(`Error executing config function:
|
|
104
|
+
${error instanceof Error ? error.message : String(error)}`);
|
|
105
|
+
}
|
|
106
|
+
if (config instanceof Promise) {
|
|
107
|
+
throw new Error("Async config functions are not supported");
|
|
108
|
+
}
|
|
109
|
+
if (config === null || config === void 0) {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (config && typeof config !== "object") {
|
|
114
|
+
throw new Error("Config must be an object");
|
|
115
|
+
}
|
|
116
|
+
return config ?? {};
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
var PLAYWRIGHT_CONFIG_FILENAMES = ["playwright.config.ts", "playwright.config.js"];
|
|
120
|
+
var TESTDINO_REPORTER_NAMES = ["@testdino/playwright", "testdino-playwright", "TestdinoReporter"];
|
|
121
|
+
var ConfigDetector = class {
|
|
122
|
+
cwd;
|
|
123
|
+
constructor(cwd = process.cwd()) {
|
|
124
|
+
this.cwd = cwd;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Detect TestdinoReporter in Playwright config
|
|
128
|
+
*/
|
|
129
|
+
async detect() {
|
|
130
|
+
const configPath = this.findPlaywrightConfig();
|
|
131
|
+
if (!configPath) {
|
|
132
|
+
return { hasReporter: false };
|
|
133
|
+
}
|
|
134
|
+
try {
|
|
135
|
+
const config = this.loadPlaywrightConfig(configPath);
|
|
136
|
+
const result = this.extractTestdinoReporter(config);
|
|
137
|
+
return {
|
|
138
|
+
...result,
|
|
139
|
+
configPath
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Failed to load Playwright config: ${configPath}
|
|
144
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Find playwright.config.[ts|js] in current directory
|
|
150
|
+
*/
|
|
151
|
+
findPlaywrightConfig() {
|
|
152
|
+
for (const filename of PLAYWRIGHT_CONFIG_FILENAMES) {
|
|
153
|
+
const configPath = join(this.cwd, filename);
|
|
154
|
+
if (existsSync(configPath)) {
|
|
155
|
+
return configPath;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return void 0;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Load and parse Playwright config using jiti
|
|
162
|
+
*/
|
|
163
|
+
loadPlaywrightConfig(configPath) {
|
|
164
|
+
const jitiLoader = jiti(dirname(configPath), {
|
|
165
|
+
interopDefault: true,
|
|
166
|
+
cache: false,
|
|
167
|
+
extensions: [".ts", ".js"]
|
|
168
|
+
});
|
|
169
|
+
let loaded;
|
|
170
|
+
try {
|
|
171
|
+
const resolved = jitiLoader.esmResolve(configPath, { try: true });
|
|
172
|
+
if (!resolved) {
|
|
173
|
+
throw new Error(`Could not resolve Playwright config: ${configPath}`);
|
|
174
|
+
}
|
|
175
|
+
const resolvedPath = typeof resolved === "string" ? resolved : fileURLToPath(resolved);
|
|
176
|
+
loaded = jitiLoader(resolvedPath);
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new Error(`Syntax error in Playwright config:
|
|
179
|
+
${error instanceof Error ? error.message : String(error)}`);
|
|
180
|
+
}
|
|
181
|
+
let config;
|
|
182
|
+
if (loaded && typeof loaded === "object" && "__esModule" in loaded) {
|
|
183
|
+
config = loaded.default;
|
|
184
|
+
} else if (loaded && typeof loaded === "object" && "default" in loaded) {
|
|
185
|
+
config = loaded.default;
|
|
186
|
+
} else {
|
|
187
|
+
config = loaded;
|
|
188
|
+
}
|
|
189
|
+
if (typeof config === "function") {
|
|
190
|
+
try {
|
|
191
|
+
config = config();
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Error executing Playwright config function:
|
|
195
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (!config || typeof config !== "object") {
|
|
200
|
+
throw new Error("Playwright config must be an object");
|
|
201
|
+
}
|
|
202
|
+
return config;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Extract TestdinoReporter configuration from Playwright config
|
|
206
|
+
*/
|
|
207
|
+
extractTestdinoReporter(config) {
|
|
208
|
+
const { reporter } = config;
|
|
209
|
+
if (!reporter) {
|
|
210
|
+
return { hasReporter: false };
|
|
211
|
+
}
|
|
212
|
+
if (typeof reporter === "string") {
|
|
213
|
+
if (this.isTestdinoReporter(reporter)) {
|
|
214
|
+
return { hasReporter: true, options: {} };
|
|
215
|
+
}
|
|
216
|
+
return { hasReporter: false };
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(reporter) && reporter.length > 0) {
|
|
219
|
+
if (typeof reporter[0] === "string") {
|
|
220
|
+
const [name, options] = reporter;
|
|
221
|
+
if (this.isTestdinoReporter(name)) {
|
|
222
|
+
return {
|
|
223
|
+
hasReporter: true,
|
|
224
|
+
options: this.extractOptions(options)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
for (const item of reporter) {
|
|
229
|
+
if (typeof item === "string") {
|
|
230
|
+
if (this.isTestdinoReporter(item)) {
|
|
231
|
+
return { hasReporter: true, options: {} };
|
|
232
|
+
}
|
|
233
|
+
} else if (Array.isArray(item) && item.length > 0) {
|
|
234
|
+
const [name, options] = item;
|
|
235
|
+
if (this.isTestdinoReporter(name)) {
|
|
236
|
+
return {
|
|
237
|
+
hasReporter: true,
|
|
238
|
+
options: this.extractOptions(options)
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { hasReporter: false };
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Check if reporter name matches TestdinoReporter
|
|
248
|
+
*/
|
|
249
|
+
isTestdinoReporter(name) {
|
|
250
|
+
return TESTDINO_REPORTER_NAMES.some((testdinoName) => name === testdinoName || name.includes(testdinoName));
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Extract and validate TestdinoConfig options
|
|
254
|
+
*/
|
|
255
|
+
extractOptions(options) {
|
|
256
|
+
if (!options || typeof options !== "object") {
|
|
257
|
+
return {};
|
|
258
|
+
}
|
|
259
|
+
const config = {};
|
|
260
|
+
if ("token" in options && typeof options.token === "string") {
|
|
261
|
+
config.token = options.token;
|
|
262
|
+
}
|
|
263
|
+
if ("serverUrl" in options && typeof options.serverUrl === "string") {
|
|
264
|
+
config.serverUrl = options.serverUrl;
|
|
265
|
+
}
|
|
266
|
+
if ("ciBuildId" in options && typeof options.ciBuildId === "string") {
|
|
267
|
+
config.ciBuildId = options.ciBuildId;
|
|
268
|
+
}
|
|
269
|
+
if ("debug" in options && typeof options.debug === "boolean") {
|
|
270
|
+
config.debug = options.debug;
|
|
271
|
+
}
|
|
272
|
+
return config;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
var ConfigValidationError = class extends Error {
|
|
276
|
+
constructor(message) {
|
|
277
|
+
super(message);
|
|
278
|
+
this.name = "ConfigValidationError";
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
var ConfigMerger = class _ConfigMerger {
|
|
282
|
+
static DEFAULT_SERVER_URL = "https://api.testdino.com";
|
|
283
|
+
/**
|
|
284
|
+
* Merge configurations from all sources
|
|
285
|
+
* Priority (highest to lowest):
|
|
286
|
+
* 1. CLI flags
|
|
287
|
+
* 2. testdino.config.[ts|js]
|
|
288
|
+
* 3. playwright.config reporter options
|
|
289
|
+
* 4. Environment variables
|
|
290
|
+
*/
|
|
291
|
+
merge(sources) {
|
|
292
|
+
const { env = {}, playwrightConfig = {}, testdinoConfig = {}, cliOptions = {} } = sources;
|
|
293
|
+
const token = this.selectValue(cliOptions.token, testdinoConfig.token, playwrightConfig.token, env.token);
|
|
294
|
+
const serverUrl = this.selectValue(cliOptions.serverUrl, testdinoConfig.serverUrl, playwrightConfig.serverUrl, env.serverUrl) || _ConfigMerger.DEFAULT_SERVER_URL;
|
|
295
|
+
const ciRunId = this.selectValue(cliOptions.ciRunId, testdinoConfig.ciRunId, playwrightConfig.ciBuildId) || this.generateCiRunId();
|
|
296
|
+
const debug = this.selectValue(cliOptions.debug, testdinoConfig.debug, playwrightConfig.debug, env.debug) ?? false;
|
|
297
|
+
const artifacts = cliOptions.noArtifacts === true ? false : this.selectValue(testdinoConfig.artifacts, playwrightConfig.artifacts) ?? true;
|
|
298
|
+
const mergedConfig = {
|
|
299
|
+
token,
|
|
300
|
+
serverUrl,
|
|
301
|
+
ciRunId,
|
|
302
|
+
debug,
|
|
303
|
+
artifacts
|
|
304
|
+
};
|
|
305
|
+
this.validate(mergedConfig);
|
|
306
|
+
return mergedConfig;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Select first non-undefined value from arguments
|
|
310
|
+
*/
|
|
311
|
+
selectValue(...values) {
|
|
312
|
+
return values.find((value) => value !== void 0 && value !== null);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Generate a unique CI run ID
|
|
316
|
+
*/
|
|
317
|
+
generateCiRunId() {
|
|
318
|
+
return `run-${randomUUID()}`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Validate merged configuration
|
|
322
|
+
*/
|
|
323
|
+
validate(config) {
|
|
324
|
+
const errors = [];
|
|
325
|
+
if (!config.token || typeof config.token !== "string" || config.token.trim().length === 0) {
|
|
326
|
+
errors.push("Token is required and must be a non-empty string");
|
|
327
|
+
}
|
|
328
|
+
if (config.serverUrl) {
|
|
329
|
+
if (typeof config.serverUrl !== "string") {
|
|
330
|
+
errors.push("Server URL must be a string");
|
|
331
|
+
} else if (!this.isValidUrl(config.serverUrl)) {
|
|
332
|
+
errors.push("Server URL must be a valid HTTP/HTTPS URL");
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (config.ciRunId && typeof config.ciRunId !== "string") {
|
|
336
|
+
errors.push("CI run ID must be a string");
|
|
337
|
+
}
|
|
338
|
+
if (config.debug !== void 0 && typeof config.debug !== "boolean") {
|
|
339
|
+
errors.push("Debug flag must be a boolean");
|
|
340
|
+
}
|
|
341
|
+
if (errors.length > 0) {
|
|
342
|
+
throw new ConfigValidationError(`Configuration validation failed:
|
|
343
|
+
${errors.map((e) => ` - ${e}`).join("\n")}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Check if string is a valid URL
|
|
348
|
+
*/
|
|
349
|
+
isValidUrl(urlString) {
|
|
350
|
+
try {
|
|
351
|
+
const url = new URL(urlString);
|
|
352
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
353
|
+
} catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Get environment variables as config
|
|
359
|
+
*/
|
|
360
|
+
static getEnvConfig() {
|
|
361
|
+
const config = {};
|
|
362
|
+
if (process.env.TESTDINO_TOKEN) {
|
|
363
|
+
config.token = process.env.TESTDINO_TOKEN;
|
|
364
|
+
}
|
|
365
|
+
if (process.env.TESTDINO_SERVER_URL) {
|
|
366
|
+
config.serverUrl = process.env.TESTDINO_SERVER_URL;
|
|
367
|
+
}
|
|
368
|
+
if (process.env.TESTDINO_DEBUG) {
|
|
369
|
+
config.debug = process.env.TESTDINO_DEBUG === "true" || process.env.TESTDINO_DEBUG === "1";
|
|
370
|
+
}
|
|
371
|
+
return config;
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/cli/arg-filter.ts
|
|
376
|
+
var TESTDINO_FLAGS = ["--token", "-t", "--ci-run-id", "--server-url", "--debug", "--no-artifacts"];
|
|
377
|
+
var FLAGS_WITH_VALUES = ["--token", "-t", "--ci-run-id", "--server-url"];
|
|
378
|
+
var ArgFilter = class {
|
|
379
|
+
/**
|
|
380
|
+
* Filter TestDino-specific arguments from the argument list
|
|
381
|
+
* Removes both flags and their values
|
|
382
|
+
*
|
|
383
|
+
* @param args - Raw command line arguments
|
|
384
|
+
* @returns Filtered arguments safe to pass to Playwright
|
|
385
|
+
*/
|
|
386
|
+
filter(args) {
|
|
387
|
+
const result = [];
|
|
388
|
+
let skipNext = false;
|
|
389
|
+
for (let i = 0; i < args.length; i++) {
|
|
390
|
+
const arg = args[i];
|
|
391
|
+
if (skipNext) {
|
|
392
|
+
skipNext = false;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (this.isTestdinoFlag(arg)) {
|
|
396
|
+
if (this.isFlagWithValue(arg) && !arg.includes("=")) {
|
|
397
|
+
skipNext = true;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
if (this.isTestdinoFlagWithEquals(arg)) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
result.push(arg);
|
|
405
|
+
}
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Check if argument is a TestDino flag
|
|
410
|
+
*/
|
|
411
|
+
isTestdinoFlag(arg) {
|
|
412
|
+
const flagName = arg.split("=")[0];
|
|
413
|
+
return TESTDINO_FLAGS.includes(flagName);
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Check if argument is a TestDino flag with = syntax
|
|
417
|
+
*/
|
|
418
|
+
isTestdinoFlagWithEquals(arg) {
|
|
419
|
+
if (!arg.includes("=")) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
const flagName = arg.split("=")[0];
|
|
423
|
+
return TESTDINO_FLAGS.includes(flagName);
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Check if flag takes a value
|
|
427
|
+
*/
|
|
428
|
+
isFlagWithValue(arg) {
|
|
429
|
+
const flagName = arg.split("=")[0];
|
|
430
|
+
return FLAGS_WITH_VALUES.includes(flagName);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Get list of TestDino flags (for reference/testing)
|
|
434
|
+
*/
|
|
435
|
+
static getTestdinoFlags() {
|
|
436
|
+
return [...TESTDINO_FLAGS];
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
var TempConfigManager = class {
|
|
440
|
+
tempFiles = /* @__PURE__ */ new Set();
|
|
441
|
+
cleanupHandlersRegistered = false;
|
|
442
|
+
exitHandler;
|
|
443
|
+
sigintHandler;
|
|
444
|
+
sigtermHandler;
|
|
445
|
+
uncaughtExceptionHandler;
|
|
446
|
+
unhandledRejectionHandler;
|
|
447
|
+
/**
|
|
448
|
+
* Create a temporary config file with the merged configuration
|
|
449
|
+
* @param config - Merged configuration to write
|
|
450
|
+
* @returns Temp config info with path and config
|
|
451
|
+
*/
|
|
452
|
+
create(config) {
|
|
453
|
+
const tempPath = this.generateTempPath();
|
|
454
|
+
try {
|
|
455
|
+
const configJson = JSON.stringify(config, null, 2);
|
|
456
|
+
writeFileSync(tempPath, configJson, "utf-8");
|
|
457
|
+
this.tempFiles.add(tempPath);
|
|
458
|
+
if (!this.cleanupHandlersRegistered) {
|
|
459
|
+
this.registerCleanupHandlers();
|
|
460
|
+
this.cleanupHandlersRegistered = true;
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
path: tempPath,
|
|
464
|
+
config
|
|
465
|
+
};
|
|
466
|
+
} catch (error) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`Failed to create temp config file: ${tempPath}
|
|
469
|
+
${error instanceof Error ? error.message : String(error)}`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Clean up a specific temp config file
|
|
475
|
+
* @param tempPath - Path to temp file to clean up
|
|
476
|
+
*/
|
|
477
|
+
cleanup(tempPath) {
|
|
478
|
+
try {
|
|
479
|
+
if (existsSync(tempPath)) {
|
|
480
|
+
unlinkSync(tempPath);
|
|
481
|
+
}
|
|
482
|
+
this.tempFiles.delete(tempPath);
|
|
483
|
+
} catch {
|
|
484
|
+
console.warn(`\u26A0\uFE0F TestDino: Failed to cleanup temp file: ${tempPath}`);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Clean up all tracked temp files
|
|
489
|
+
*/
|
|
490
|
+
cleanupAll() {
|
|
491
|
+
for (const tempPath of this.tempFiles) {
|
|
492
|
+
this.cleanup(tempPath);
|
|
493
|
+
}
|
|
494
|
+
this.tempFiles.clear();
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Remove all event handlers (for testing)
|
|
498
|
+
*/
|
|
499
|
+
removeHandlers() {
|
|
500
|
+
if (this.exitHandler) {
|
|
501
|
+
process.removeListener("exit", this.exitHandler);
|
|
502
|
+
}
|
|
503
|
+
if (this.sigintHandler) {
|
|
504
|
+
process.removeListener("SIGINT", this.sigintHandler);
|
|
505
|
+
}
|
|
506
|
+
if (this.sigtermHandler) {
|
|
507
|
+
process.removeListener("SIGTERM", this.sigtermHandler);
|
|
508
|
+
}
|
|
509
|
+
if (this.uncaughtExceptionHandler) {
|
|
510
|
+
process.removeListener("uncaughtException", this.uncaughtExceptionHandler);
|
|
511
|
+
}
|
|
512
|
+
if (this.unhandledRejectionHandler) {
|
|
513
|
+
process.removeListener("unhandledRejection", this.unhandledRejectionHandler);
|
|
514
|
+
}
|
|
515
|
+
this.cleanupHandlersRegistered = false;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Generate unique temp file path
|
|
519
|
+
*/
|
|
520
|
+
generateTempPath() {
|
|
521
|
+
const filename = `testdino-config-${randomUUID()}.json`;
|
|
522
|
+
return join(tmpdir(), filename);
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Register cleanup handlers for process exit and signals
|
|
526
|
+
*/
|
|
527
|
+
registerCleanupHandlers() {
|
|
528
|
+
this.exitHandler = () => {
|
|
529
|
+
this.cleanupAll();
|
|
530
|
+
};
|
|
531
|
+
process.on("exit", this.exitHandler);
|
|
532
|
+
this.sigintHandler = () => {
|
|
533
|
+
this.cleanupAll();
|
|
534
|
+
process.exit(130);
|
|
535
|
+
};
|
|
536
|
+
process.on("SIGINT", this.sigintHandler);
|
|
537
|
+
this.sigtermHandler = () => {
|
|
538
|
+
this.cleanupAll();
|
|
539
|
+
process.exit(143);
|
|
540
|
+
};
|
|
541
|
+
process.on("SIGTERM", this.sigtermHandler);
|
|
542
|
+
this.uncaughtExceptionHandler = (error) => {
|
|
543
|
+
console.error("Uncaught exception:", error);
|
|
544
|
+
this.cleanupAll();
|
|
545
|
+
process.exit(1);
|
|
546
|
+
};
|
|
547
|
+
process.on("uncaughtException", this.uncaughtExceptionHandler);
|
|
548
|
+
this.unhandledRejectionHandler = (reason) => {
|
|
549
|
+
console.error("Unhandled rejection:", reason);
|
|
550
|
+
this.cleanupAll();
|
|
551
|
+
process.exit(1);
|
|
552
|
+
};
|
|
553
|
+
process.on("unhandledRejection", this.unhandledRejectionHandler);
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Get list of tracked temp files (for testing)
|
|
557
|
+
*/
|
|
558
|
+
getTempFiles() {
|
|
559
|
+
return Array.from(this.tempFiles);
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var PlaywrightSpawner = class {
|
|
563
|
+
/**
|
|
564
|
+
* Spawn Playwright test process
|
|
565
|
+
* @param options - Spawn options
|
|
566
|
+
* @returns Spawn result with exit code
|
|
567
|
+
*/
|
|
568
|
+
async spawn(options) {
|
|
569
|
+
const { args, tempConfigPath, config, cwd = process.cwd() } = options;
|
|
570
|
+
try {
|
|
571
|
+
const env = {
|
|
572
|
+
...process.env,
|
|
573
|
+
TESTDINO_CLI_CONFIG_PATH: tempConfigPath,
|
|
574
|
+
TESTDINO_TOKEN: config.token,
|
|
575
|
+
TESTDINO_SERVER_URL: config.serverUrl,
|
|
576
|
+
TESTDINO_CI_RUN_ID: config.ciRunId,
|
|
577
|
+
TESTDINO_DEBUG: config.debug ? "true" : "false"
|
|
578
|
+
};
|
|
579
|
+
const playwrightArgs = ["playwright", "test", "--reporter", "@testdino/playwright", ...args];
|
|
580
|
+
const result = await execa("npx", playwrightArgs, {
|
|
581
|
+
stdio: "inherit",
|
|
582
|
+
// Forward stdout/stderr in real-time
|
|
583
|
+
cwd,
|
|
584
|
+
env,
|
|
585
|
+
reject: false
|
|
586
|
+
// Don't throw on non-zero exit codes
|
|
587
|
+
});
|
|
588
|
+
const exitCode = result.exitCode ?? 0;
|
|
589
|
+
return {
|
|
590
|
+
exitCode,
|
|
591
|
+
success: exitCode === 0
|
|
592
|
+
};
|
|
593
|
+
} catch (error) {
|
|
594
|
+
return this.handleSpawnError(error);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Handle spawn errors
|
|
599
|
+
*/
|
|
600
|
+
handleSpawnError(error) {
|
|
601
|
+
const execaError = error;
|
|
602
|
+
if (execaError.code === "ENOENT") {
|
|
603
|
+
console.error("\u274C TestDino: Failed to spawn Playwright");
|
|
604
|
+
console.error("");
|
|
605
|
+
console.error("Playwright is not installed or npx is not available.");
|
|
606
|
+
console.error("");
|
|
607
|
+
console.error("To install Playwright:");
|
|
608
|
+
console.error(" npm install -D @playwright/test");
|
|
609
|
+
console.error(" npx playwright install");
|
|
610
|
+
console.error("");
|
|
611
|
+
return {
|
|
612
|
+
exitCode: 1,
|
|
613
|
+
success: false
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
if (execaError.code === "EACCES") {
|
|
617
|
+
console.error("\u274C TestDino: Permission denied when trying to spawn Playwright");
|
|
618
|
+
console.error("");
|
|
619
|
+
console.error("Please check file permissions and try again.");
|
|
620
|
+
console.error("");
|
|
621
|
+
return {
|
|
622
|
+
exitCode: 1,
|
|
623
|
+
success: false
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
console.error("\u274C TestDino: Failed to spawn Playwright");
|
|
627
|
+
console.error("");
|
|
628
|
+
console.error("Error:", execaError.message || String(error));
|
|
629
|
+
console.error("");
|
|
630
|
+
return {
|
|
631
|
+
exitCode: 1,
|
|
632
|
+
success: false
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
// src/cli/commands/test.ts
|
|
638
|
+
var TestCommand = class {
|
|
639
|
+
configLoader;
|
|
640
|
+
configDetector;
|
|
641
|
+
configMerger;
|
|
642
|
+
argFilter;
|
|
643
|
+
tempConfigManager;
|
|
644
|
+
playwrightSpawner;
|
|
645
|
+
constructor(configLoader, configDetector, configMerger, argFilter, tempConfigManager, playwrightSpawner) {
|
|
646
|
+
this.configLoader = configLoader || new ConfigLoader();
|
|
647
|
+
this.configDetector = configDetector || new ConfigDetector();
|
|
648
|
+
this.configMerger = configMerger || new ConfigMerger();
|
|
649
|
+
this.argFilter = argFilter || new ArgFilter();
|
|
650
|
+
this.tempConfigManager = tempConfigManager || new TempConfigManager();
|
|
651
|
+
this.playwrightSpawner = playwrightSpawner || new PlaywrightSpawner();
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Execute the test command
|
|
655
|
+
* @param options - CLI options from commander
|
|
656
|
+
* @param args - Remaining arguments to pass to Playwright
|
|
657
|
+
* @returns Spawn result with exit code
|
|
658
|
+
*/
|
|
659
|
+
async execute(options, args) {
|
|
660
|
+
let tempConfigPath;
|
|
661
|
+
try {
|
|
662
|
+
const testdinoConfigResult = await this.configLoader.load();
|
|
663
|
+
const playwrightConfigResult = await this.configDetector.detect();
|
|
664
|
+
const envConfig = ConfigMerger.getEnvConfig();
|
|
665
|
+
const mergedConfig = this.configMerger.merge({
|
|
666
|
+
env: envConfig,
|
|
667
|
+
playwrightConfig: playwrightConfigResult.options,
|
|
668
|
+
testdinoConfig: testdinoConfigResult.config,
|
|
669
|
+
cliOptions: options
|
|
670
|
+
});
|
|
671
|
+
const tempConfigInfo = this.tempConfigManager.create(mergedConfig);
|
|
672
|
+
tempConfigPath = tempConfigInfo.path;
|
|
673
|
+
const filteredArgs = this.argFilter.filter(args);
|
|
674
|
+
const result = await this.playwrightSpawner.spawn({
|
|
675
|
+
args: filteredArgs,
|
|
676
|
+
tempConfigPath,
|
|
677
|
+
config: mergedConfig
|
|
678
|
+
});
|
|
679
|
+
return result;
|
|
680
|
+
} finally {
|
|
681
|
+
if (tempConfigPath) {
|
|
682
|
+
this.tempConfigManager.cleanup(tempConfigPath);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
};
|
|
687
|
+
|
|
688
|
+
// src/cli/index.ts
|
|
689
|
+
var __filename$1 = fileURLToPath(import.meta.url);
|
|
690
|
+
var __dirname$1 = dirname(__filename$1);
|
|
691
|
+
function getVersion() {
|
|
692
|
+
try {
|
|
693
|
+
const packagePath = join(__dirname$1, "../../package.json");
|
|
694
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
695
|
+
return packageJson.version;
|
|
696
|
+
} catch {
|
|
697
|
+
return "0.0.0";
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function printBanner() {
|
|
701
|
+
const version = getVersion();
|
|
702
|
+
console.log(
|
|
703
|
+
chalk.cyan(`
|
|
704
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
705
|
+
\u2551 \u2551
|
|
706
|
+
\u2551 ${chalk.bold("TestDino Playwright")} v${version.padEnd(36)}\u2551
|
|
707
|
+
\u2551 ${chalk.dim("https://testdino.com")} \u2551
|
|
708
|
+
\u2551 \u2551
|
|
709
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
710
|
+
`)
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
function buildProgram() {
|
|
714
|
+
const program = new Command().name("tdpw").description("Run Playwright tests with TestDino reporting").version(getVersion(), "-v, --version", "Output the current version").helpOption("-h, --help", "Display help for command");
|
|
715
|
+
program.command("test").description("Run Playwright tests with TestDino reporter").option("-t, --token <token>", "TestDino authentication token").option("--ci-run-id <id>", "CI run ID for grouping test runs").option("--server-url <url>", "TestDino server URL").option("--debug", "Enable debug logging").option("--no-artifacts", "Disable artifact uploads (screenshots, videos, traces)").allowUnknownOption().allowExcessArguments().action(async (options, command) => {
|
|
716
|
+
try {
|
|
717
|
+
const args = command.args || [];
|
|
718
|
+
const testCommand = new TestCommand();
|
|
719
|
+
const result = await testCommand.execute(options, args);
|
|
720
|
+
process.exit(result.exitCode);
|
|
721
|
+
} catch (error) {
|
|
722
|
+
console.error(chalk.red("\n\u2716 Unexpected error:"), error instanceof Error ? error.message : String(error));
|
|
723
|
+
if (options.debug) {
|
|
724
|
+
console.error(chalk.dim("\nStack trace:"));
|
|
725
|
+
console.error(error);
|
|
726
|
+
}
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
return program;
|
|
731
|
+
}
|
|
732
|
+
async function main() {
|
|
733
|
+
try {
|
|
734
|
+
printBanner();
|
|
735
|
+
const program = buildProgram();
|
|
736
|
+
await program.parseAsync(process.argv);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
console.error(chalk.red("\n\u2716 Error:"), error instanceof Error ? error.message : String(error));
|
|
739
|
+
if (process.env.DEBUG || process.env.TESTDINO_DEBUG) {
|
|
740
|
+
console.error(chalk.dim("\nStack trace:"));
|
|
741
|
+
console.error(error);
|
|
742
|
+
}
|
|
743
|
+
process.exit(1);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
main().catch((error) => {
|
|
747
|
+
console.error(chalk.red("Unexpected error:"), error);
|
|
748
|
+
process.exit(1);
|
|
749
|
+
});
|
|
750
|
+
//# sourceMappingURL=index.mjs.map
|
|
751
|
+
//# sourceMappingURL=index.mjs.map
|