@probolabs/playwright 1.4.0-rc.5 โ 1.4.0-rc.7
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/bin/probo.js +2 -2
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli.js +408 -22
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +352 -20
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +352 -20
- package/dist/index.js.map +1 -1
- package/dist/types/cli.d.ts.map +1 -1
- package/dist/types/codegen-api.d.ts.map +1 -1
- package/dist/types/test-suite-runner.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -404,6 +404,18 @@ interface TestSuiteCodeGenResult {
|
|
|
404
404
|
code: string;
|
|
405
405
|
}>;
|
|
406
406
|
}
|
|
407
|
+
/**
|
|
408
|
+
* Result of generating code for a project (all scenarios)
|
|
409
|
+
*/
|
|
410
|
+
interface ProjectCodeGenResult {
|
|
411
|
+
projectId: number;
|
|
412
|
+
projectName: string;
|
|
413
|
+
scenarios: Array<{
|
|
414
|
+
scenarioId: number;
|
|
415
|
+
scenarioName: string;
|
|
416
|
+
code: string;
|
|
417
|
+
}>;
|
|
418
|
+
}
|
|
407
419
|
/**
|
|
408
420
|
* ProboCodeGenerator - Handles fetching test suite/scenario data and generating Playwright code
|
|
409
421
|
*/
|
|
@@ -421,6 +433,14 @@ declare class ProboCodeGenerator {
|
|
|
421
433
|
* Fetch test suite data from the backend API
|
|
422
434
|
*/
|
|
423
435
|
private static fetchTestSuiteData;
|
|
436
|
+
/**
|
|
437
|
+
* Fetch project data from the backend API
|
|
438
|
+
*/
|
|
439
|
+
private static fetchProjectData;
|
|
440
|
+
/**
|
|
441
|
+
* Fetch all scenarios for a project from the backend API
|
|
442
|
+
*/
|
|
443
|
+
private static fetchProjectScenarios;
|
|
424
444
|
/**
|
|
425
445
|
* Convert backend interaction format to Interaction[] format
|
|
426
446
|
*/
|
|
@@ -448,6 +468,11 @@ declare class ProboCodeGenerator {
|
|
|
448
468
|
* Returns map of scenario names to generated code
|
|
449
469
|
*/
|
|
450
470
|
static generateCodeForTestSuite(testSuiteId: number, apiToken: string, apiUrl: string, options?: CodeGenOptions): Promise<TestSuiteCodeGenResult>;
|
|
471
|
+
/**
|
|
472
|
+
* Generate code for all scenarios in a project
|
|
473
|
+
* Returns map of scenario names to generated code
|
|
474
|
+
*/
|
|
475
|
+
static generateCodeForProject(projectId: number, apiToken: string, apiUrl: string, options?: CodeGenOptions): Promise<ProjectCodeGenResult>;
|
|
451
476
|
}
|
|
452
477
|
|
|
453
478
|
/**
|
|
@@ -476,6 +501,7 @@ interface TestSuiteRunOptions {
|
|
|
476
501
|
outputDir?: string;
|
|
477
502
|
includeReporter?: boolean;
|
|
478
503
|
playwrightArgs?: string[];
|
|
504
|
+
skipRunCreation?: boolean;
|
|
479
505
|
onStatusUpdate?: (updates: Partial<{
|
|
480
506
|
status: string;
|
|
481
507
|
exit_code: number;
|
|
@@ -495,6 +521,10 @@ interface TestSuiteRunOptions {
|
|
|
495
521
|
* TestSuiteRunner - Handles test suite file generation and execution
|
|
496
522
|
*/
|
|
497
523
|
declare class TestSuiteRunner {
|
|
524
|
+
/**
|
|
525
|
+
* Lookup project ID by name
|
|
526
|
+
*/
|
|
527
|
+
static lookupProjectByName(projectName: string, apiToken: string, apiUrl: string): Promise<number>;
|
|
498
528
|
/**
|
|
499
529
|
* Lookup test suite ID by name and project
|
|
500
530
|
*/
|
|
@@ -506,7 +536,7 @@ declare class TestSuiteRunner {
|
|
|
506
536
|
/**
|
|
507
537
|
* Generate package.json file
|
|
508
538
|
*/
|
|
509
|
-
static generatePackageJson(outputDir: string, codeGenResult: TestSuiteCodeGenResult): Promise<void>;
|
|
539
|
+
static generatePackageJson(outputDir: string, codeGenResult: TestSuiteCodeGenResult | ProjectCodeGenResult): Promise<void>;
|
|
510
540
|
/**
|
|
511
541
|
* Generate playwright.config.ts file
|
|
512
542
|
*/
|
|
@@ -520,6 +550,16 @@ declare class TestSuiteRunner {
|
|
|
520
550
|
* Generates files, installs dependencies, and executes tests
|
|
521
551
|
*/
|
|
522
552
|
static runTestSuite(testSuiteId: number, apiToken: string, apiUrl: string, testSuiteName?: string, runId?: number, options?: TestSuiteRunOptions): Promise<TestSuiteRunResult>;
|
|
553
|
+
/**
|
|
554
|
+
* Generate all files for a project (all scenarios)
|
|
555
|
+
*/
|
|
556
|
+
static generateProjectFiles(projectId: number, apiToken: string, apiUrl: string, outputDir?: string, projectName?: string, includeReporter?: boolean): Promise<void>;
|
|
557
|
+
/**
|
|
558
|
+
* Run all scenarios in a project
|
|
559
|
+
* Generates files, installs dependencies, and executes tests
|
|
560
|
+
* Does not create run records in the database (CLI mode)
|
|
561
|
+
*/
|
|
562
|
+
static runProjectScenarios(projectId: number, apiToken: string, apiUrl: string, projectName?: string, options?: TestSuiteRunOptions): Promise<TestSuiteRunResult>;
|
|
523
563
|
/**
|
|
524
564
|
* Upload artifacts for a test suite run
|
|
525
565
|
*
|
package/dist/index.js
CHANGED
|
@@ -5742,6 +5742,7 @@ class ProboCodeGenerator {
|
|
|
5742
5742
|
*/
|
|
5743
5743
|
static async fetchScenarioData(scenarioId, apiToken, apiUrl) {
|
|
5744
5744
|
const normalizedUrl = this.normalizeApiUrl(apiUrl);
|
|
5745
|
+
// Scenario interactions endpoint is under /api/ (special endpoint)
|
|
5745
5746
|
const url = `${normalizedUrl}/api/scenarios/${scenarioId}/interactions`;
|
|
5746
5747
|
const response = await fetch(url, {
|
|
5747
5748
|
method: 'GET',
|
|
@@ -5776,6 +5777,54 @@ class ProboCodeGenerator {
|
|
|
5776
5777
|
}
|
|
5777
5778
|
return response.json();
|
|
5778
5779
|
}
|
|
5780
|
+
/**
|
|
5781
|
+
* Fetch project data from the backend API
|
|
5782
|
+
*/
|
|
5783
|
+
static async fetchProjectData(projectId, apiToken, apiUrl) {
|
|
5784
|
+
const normalizedUrl = this.normalizeApiUrl(apiUrl);
|
|
5785
|
+
// Projects endpoint is at root level, not under /api/
|
|
5786
|
+
const url = `${normalizedUrl}/projects/${projectId}/`;
|
|
5787
|
+
const response = await fetch(url, {
|
|
5788
|
+
method: 'GET',
|
|
5789
|
+
headers: {
|
|
5790
|
+
'Authorization': `Token ${apiToken}`,
|
|
5791
|
+
'Content-Type': 'application/json',
|
|
5792
|
+
},
|
|
5793
|
+
});
|
|
5794
|
+
if (!response.ok) {
|
|
5795
|
+
const errorText = await this.readResponseErrorText(response);
|
|
5796
|
+
throw new Error(`Failed to fetch project ${projectId}: ${response.status} ${errorText}`);
|
|
5797
|
+
}
|
|
5798
|
+
return response.json();
|
|
5799
|
+
}
|
|
5800
|
+
/**
|
|
5801
|
+
* Fetch all scenarios for a project from the backend API
|
|
5802
|
+
*/
|
|
5803
|
+
static async fetchProjectScenarios(projectId, apiToken, apiUrl) {
|
|
5804
|
+
const normalizedUrl = this.normalizeApiUrl(apiUrl);
|
|
5805
|
+
// Scenarios endpoint is at root level, not under /api/
|
|
5806
|
+
const url = `${normalizedUrl}/scenarios/?project_id=${projectId}`;
|
|
5807
|
+
const response = await fetch(url, {
|
|
5808
|
+
method: 'GET',
|
|
5809
|
+
headers: {
|
|
5810
|
+
'Authorization': `Token ${apiToken}`,
|
|
5811
|
+
'Content-Type': 'application/json',
|
|
5812
|
+
},
|
|
5813
|
+
});
|
|
5814
|
+
if (!response.ok) {
|
|
5815
|
+
const errorText = await this.readResponseErrorText(response);
|
|
5816
|
+
throw new Error(`Failed to fetch scenarios for project ${projectId}: ${response.status} ${errorText}`);
|
|
5817
|
+
}
|
|
5818
|
+
const data = await response.json();
|
|
5819
|
+
// Handle both array and paginated responses
|
|
5820
|
+
if (Array.isArray(data)) {
|
|
5821
|
+
return data;
|
|
5822
|
+
}
|
|
5823
|
+
if (data.results && Array.isArray(data.results)) {
|
|
5824
|
+
return data.results;
|
|
5825
|
+
}
|
|
5826
|
+
return [];
|
|
5827
|
+
}
|
|
5779
5828
|
/**
|
|
5780
5829
|
* Convert backend interaction format to Interaction[] format
|
|
5781
5830
|
*/
|
|
@@ -5926,6 +5975,48 @@ class ProboCodeGenerator {
|
|
|
5926
5975
|
scenarios: scenarioResults,
|
|
5927
5976
|
};
|
|
5928
5977
|
}
|
|
5978
|
+
/**
|
|
5979
|
+
* Generate code for all scenarios in a project
|
|
5980
|
+
* Returns map of scenario names to generated code
|
|
5981
|
+
*/
|
|
5982
|
+
static async generateCodeForProject(projectId, apiToken, apiUrl, options) {
|
|
5983
|
+
// Validate inputs
|
|
5984
|
+
if (!apiToken) {
|
|
5985
|
+
throw new Error('API token is required');
|
|
5986
|
+
}
|
|
5987
|
+
if (!apiUrl) {
|
|
5988
|
+
throw new Error('API URL is required');
|
|
5989
|
+
}
|
|
5990
|
+
// Fetch project data and scenarios
|
|
5991
|
+
const [projectData, scenarios] = await Promise.all([
|
|
5992
|
+
this.fetchProjectData(projectId, apiToken, apiUrl),
|
|
5993
|
+
this.fetchProjectScenarios(projectId, apiToken, apiUrl),
|
|
5994
|
+
]);
|
|
5995
|
+
if (scenarios.length === 0) {
|
|
5996
|
+
throw new Error(`No scenarios found in project "${projectData.name}" (ID: ${projectId})`);
|
|
5997
|
+
}
|
|
5998
|
+
// Generate code for each scenario
|
|
5999
|
+
const scenarioResults = await Promise.all(scenarios.map(async (scenario) => {
|
|
6000
|
+
try {
|
|
6001
|
+
const code = await this.generateCodeForScenario(scenario.id, apiToken, apiUrl, options);
|
|
6002
|
+
return {
|
|
6003
|
+
scenarioId: scenario.id,
|
|
6004
|
+
scenarioName: scenario.name,
|
|
6005
|
+
code: code,
|
|
6006
|
+
};
|
|
6007
|
+
}
|
|
6008
|
+
catch (error) {
|
|
6009
|
+
// Log error but continue with other scenarios
|
|
6010
|
+
console.error(`Failed to generate code for scenario ${scenario.id}: ${error.message}`);
|
|
6011
|
+
throw error; // Re-throw to fail fast for now
|
|
6012
|
+
}
|
|
6013
|
+
}));
|
|
6014
|
+
return {
|
|
6015
|
+
projectId: projectData.id,
|
|
6016
|
+
projectName: projectData.name,
|
|
6017
|
+
scenarios: scenarioResults,
|
|
6018
|
+
};
|
|
6019
|
+
}
|
|
5929
6020
|
}
|
|
5930
6021
|
|
|
5931
6022
|
const execAsync = promisify(exec);
|
|
@@ -6048,11 +6139,12 @@ async function updateRunStatus(runId, testSuiteId, updates, apiToken, apiUrl) {
|
|
|
6048
6139
|
*/
|
|
6049
6140
|
class TestSuiteRunner {
|
|
6050
6141
|
/**
|
|
6051
|
-
* Lookup
|
|
6142
|
+
* Lookup project ID by name
|
|
6052
6143
|
*/
|
|
6053
|
-
static async
|
|
6144
|
+
static async lookupProjectByName(projectName, apiToken, apiUrl) {
|
|
6054
6145
|
const baseUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
|
|
6055
|
-
|
|
6146
|
+
// Projects endpoint is at root level, not under /api/
|
|
6147
|
+
const url = `${baseUrl}/projects/?name=${encodeURIComponent(projectName)}`;
|
|
6056
6148
|
const response = await fetch$1(url, {
|
|
6057
6149
|
method: 'GET',
|
|
6058
6150
|
headers: {
|
|
@@ -6062,25 +6154,35 @@ class TestSuiteRunner {
|
|
|
6062
6154
|
});
|
|
6063
6155
|
if (!response.ok) {
|
|
6064
6156
|
const errorText = await response.text();
|
|
6065
|
-
throw new Error(`Failed to lookup
|
|
6157
|
+
throw new Error(`Failed to lookup project: ${response.status} ${errorText}`);
|
|
6066
6158
|
}
|
|
6067
6159
|
const data = await response.json();
|
|
6068
|
-
// TEMPORARY DEBUG: Output the full response
|
|
6069
|
-
console.log('๐ DEBUG: API Response URL:', url);
|
|
6070
|
-
console.log('๐ DEBUG: API Response Status:', response.status);
|
|
6071
|
-
console.log('๐ DEBUG: API Response Data:', JSON.stringify(data, null, 2));
|
|
6072
|
-
console.log('๐ DEBUG: Is Array?', Array.isArray(data));
|
|
6073
|
-
console.log('๐ DEBUG: Array length:', Array.isArray(data) ? data.length : 'N/A');
|
|
6074
|
-
console.log('๐ DEBUG: Has results?', !!data.results);
|
|
6075
|
-
console.log('๐ DEBUG: Results is Array?', Array.isArray(data.results));
|
|
6076
|
-
console.log('๐ DEBUG: Results length:', Array.isArray(data.results) ? data.results.length : 'N/A');
|
|
6077
6160
|
if (Array.isArray(data) && data.length > 0) {
|
|
6078
|
-
|
|
6161
|
+
return data[0].id;
|
|
6079
6162
|
}
|
|
6080
6163
|
if (data.results && Array.isArray(data.results) && data.results.length > 0) {
|
|
6081
|
-
|
|
6164
|
+
return data.results[0].id;
|
|
6082
6165
|
}
|
|
6083
|
-
|
|
6166
|
+
throw new Error(`Project "${projectName}" not found`);
|
|
6167
|
+
}
|
|
6168
|
+
/**
|
|
6169
|
+
* Lookup test suite ID by name and project
|
|
6170
|
+
*/
|
|
6171
|
+
static async lookupTestSuiteByName(testSuiteName, projectName, apiToken, apiUrl) {
|
|
6172
|
+
const baseUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
|
|
6173
|
+
const url = `${baseUrl}/test-suites/?name=${encodeURIComponent(testSuiteName)}&project=${encodeURIComponent(projectName)}`;
|
|
6174
|
+
const response = await fetch$1(url, {
|
|
6175
|
+
method: 'GET',
|
|
6176
|
+
headers: {
|
|
6177
|
+
'Authorization': `Token ${apiToken}`,
|
|
6178
|
+
'Content-Type': 'application/json',
|
|
6179
|
+
},
|
|
6180
|
+
});
|
|
6181
|
+
if (!response.ok) {
|
|
6182
|
+
const errorText = await response.text();
|
|
6183
|
+
throw new Error(`Failed to lookup test suite: ${response.status} ${errorText}`);
|
|
6184
|
+
}
|
|
6185
|
+
const data = await response.json();
|
|
6084
6186
|
if (Array.isArray(data) && data.length > 0) {
|
|
6085
6187
|
return data[0].id;
|
|
6086
6188
|
}
|
|
@@ -6198,7 +6300,10 @@ class TestSuiteRunner {
|
|
|
6198
6300
|
}
|
|
6199
6301
|
}
|
|
6200
6302
|
}
|
|
6201
|
-
|
|
6303
|
+
// Type guard: check for projectId to distinguish ProjectCodeGenResult from TestSuiteCodeGenResult
|
|
6304
|
+
const sanitizedName = 'projectId' in codeGenResult && 'projectName' in codeGenResult
|
|
6305
|
+
? slugify(codeGenResult.projectName)
|
|
6306
|
+
: slugify(codeGenResult.testSuiteName);
|
|
6202
6307
|
const packageJsonContent = generatePackageJson({
|
|
6203
6308
|
name: sanitizedName,
|
|
6204
6309
|
hasSecrets: hasSecrets,
|
|
@@ -6231,12 +6336,12 @@ class TestSuiteRunner {
|
|
|
6231
6336
|
* Generates files, installs dependencies, and executes tests
|
|
6232
6337
|
*/
|
|
6233
6338
|
static async runTestSuite(testSuiteId, apiToken, apiUrl, testSuiteName, runId, options = {}) {
|
|
6234
|
-
const { outputDir, includeReporter = true, playwrightArgs = [], onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
|
|
6339
|
+
const { outputDir, includeReporter = true, playwrightArgs = [], skipRunCreation = false, onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
|
|
6235
6340
|
const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
|
|
6236
6341
|
let currentRunId = runId;
|
|
6237
6342
|
try {
|
|
6238
|
-
// Create run record if not provided
|
|
6239
|
-
if (!currentRunId) {
|
|
6343
|
+
// Create run record if not provided and not skipping run creation
|
|
6344
|
+
if (!currentRunId && !skipRunCreation) {
|
|
6240
6345
|
try {
|
|
6241
6346
|
const createResponse = await fetch$1(`${apiUrl}/test-suites/${testSuiteId}/runs/`, {
|
|
6242
6347
|
method: 'POST',
|
|
@@ -6661,6 +6766,233 @@ class TestSuiteRunner {
|
|
|
6661
6766
|
};
|
|
6662
6767
|
}
|
|
6663
6768
|
}
|
|
6769
|
+
/**
|
|
6770
|
+
* Generate all files for a project (all scenarios)
|
|
6771
|
+
*/
|
|
6772
|
+
static async generateProjectFiles(projectId, apiToken, apiUrl, outputDir, projectName, includeReporter = true) {
|
|
6773
|
+
const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
|
|
6774
|
+
// Generate code for all scenarios
|
|
6775
|
+
const codeGenResult = await ProboCodeGenerator.generateCodeForProject(projectId, apiToken, apiUrl);
|
|
6776
|
+
if (codeGenResult.scenarios.length === 0) {
|
|
6777
|
+
throw new Error(`No scenarios found in project "${codeGenResult.projectName}" (ID: ${projectId}). Cannot generate test files.`);
|
|
6778
|
+
}
|
|
6779
|
+
// Delete everything in the project directory except node_modules
|
|
6780
|
+
if (fs.existsSync(projectDir)) {
|
|
6781
|
+
try {
|
|
6782
|
+
const nodeModulesPath = path.join(projectDir, 'node_modules');
|
|
6783
|
+
const hasNodeModules = fs.existsSync(nodeModulesPath);
|
|
6784
|
+
// Temporarily move node_modules out of the way if it exists
|
|
6785
|
+
let tempNodeModulesPath = null;
|
|
6786
|
+
if (hasNodeModules) {
|
|
6787
|
+
tempNodeModulesPath = path.join(projectDir, '..', `node_modules.temp.${projectId}`);
|
|
6788
|
+
// Remove temp directory if it exists from a previous failed run
|
|
6789
|
+
if (fs.existsSync(tempNodeModulesPath)) {
|
|
6790
|
+
fs.rmSync(tempNodeModulesPath, { recursive: true, force: true });
|
|
6791
|
+
}
|
|
6792
|
+
fs.renameSync(nodeModulesPath, tempNodeModulesPath);
|
|
6793
|
+
console.log(`๐ฆ Preserved node_modules temporarily`);
|
|
6794
|
+
}
|
|
6795
|
+
// Delete everything in the directory
|
|
6796
|
+
const entries = fs.readdirSync(projectDir, { withFileTypes: true });
|
|
6797
|
+
for (const entry of entries) {
|
|
6798
|
+
const entryPath = path.join(projectDir, entry.name);
|
|
6799
|
+
try {
|
|
6800
|
+
if (entry.isDirectory()) {
|
|
6801
|
+
fs.rmSync(entryPath, { recursive: true, force: true });
|
|
6802
|
+
}
|
|
6803
|
+
else {
|
|
6804
|
+
fs.unlinkSync(entryPath);
|
|
6805
|
+
}
|
|
6806
|
+
}
|
|
6807
|
+
catch (error) {
|
|
6808
|
+
console.warn(`โ ๏ธ Failed to delete ${entry.name}:`, error);
|
|
6809
|
+
}
|
|
6810
|
+
}
|
|
6811
|
+
console.log(`๐๏ธ Cleaned project directory (preserved node_modules)`);
|
|
6812
|
+
// Move node_modules back
|
|
6813
|
+
if (hasNodeModules && tempNodeModulesPath) {
|
|
6814
|
+
fs.renameSync(tempNodeModulesPath, nodeModulesPath);
|
|
6815
|
+
console.log(`๐ฆ Restored node_modules`);
|
|
6816
|
+
}
|
|
6817
|
+
}
|
|
6818
|
+
catch (error) {
|
|
6819
|
+
console.warn(`โ ๏ธ Failed to clean project directory ${projectDir}:`, error);
|
|
6820
|
+
// Continue anyway - we'll overwrite files as needed
|
|
6821
|
+
}
|
|
6822
|
+
}
|
|
6823
|
+
// Ensure directories exist (will recreate after deletion)
|
|
6824
|
+
ensureDirectoryExists(projectDir);
|
|
6825
|
+
const testsDir = path.join(projectDir, 'tests');
|
|
6826
|
+
ensureDirectoryExists(testsDir);
|
|
6827
|
+
// Save each scenario's code to a .spec.ts file
|
|
6828
|
+
for (const scenario of codeGenResult.scenarios) {
|
|
6829
|
+
const fileName = `${slugify(scenario.scenarioName)}.spec.ts`;
|
|
6830
|
+
const filePath = path.join(testsDir, fileName);
|
|
6831
|
+
fs.writeFileSync(filePath, scenario.code, 'utf-8');
|
|
6832
|
+
console.log(`โ
Generated test file: ${filePath}`);
|
|
6833
|
+
}
|
|
6834
|
+
// Generate package.json
|
|
6835
|
+
await this.generatePackageJson(projectDir, codeGenResult);
|
|
6836
|
+
// Generate playwright.config.ts (no runId for project runs)
|
|
6837
|
+
await this.generatePlaywrightConfig(projectDir, includeReporter, undefined);
|
|
6838
|
+
// Generate custom reporter file for live progress streaming (only if requested)
|
|
6839
|
+
if (includeReporter) {
|
|
6840
|
+
await this.generateProboReporter(projectDir);
|
|
6841
|
+
}
|
|
6842
|
+
}
|
|
6843
|
+
/**
|
|
6844
|
+
* Run all scenarios in a project
|
|
6845
|
+
* Generates files, installs dependencies, and executes tests
|
|
6846
|
+
* Does not create run records in the database (CLI mode)
|
|
6847
|
+
*/
|
|
6848
|
+
static async runProjectScenarios(projectId, apiToken, apiUrl, projectName, options = {}) {
|
|
6849
|
+
const { outputDir, includeReporter = false, // CLI mode - no custom reporter
|
|
6850
|
+
playwrightArgs = [], onStdout, onStderr, } = options;
|
|
6851
|
+
const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
|
|
6852
|
+
try {
|
|
6853
|
+
// Generate all files
|
|
6854
|
+
console.log(`๐ Generating test files for project ${projectId}...`);
|
|
6855
|
+
await this.generateProjectFiles(projectId, apiToken, apiUrl, projectDir, projectName, includeReporter);
|
|
6856
|
+
// Count total tests
|
|
6857
|
+
const testsTotal = countTotalTests(projectDir);
|
|
6858
|
+
if (testsTotal === 0) {
|
|
6859
|
+
throw new Error(`No test files generated for project ${projectId}`);
|
|
6860
|
+
}
|
|
6861
|
+
// Install dependencies
|
|
6862
|
+
console.log(`๐ฆ Installing dependencies in ${projectDir}...`);
|
|
6863
|
+
try {
|
|
6864
|
+
const { stdout: installStdout, stderr: installStderr } = await execAsync('npm install', { cwd: projectDir, timeout: 300000 } // 5 minute timeout for install
|
|
6865
|
+
);
|
|
6866
|
+
console.log('โ
Dependencies installed successfully');
|
|
6867
|
+
if (installStdout)
|
|
6868
|
+
console.log(installStdout);
|
|
6869
|
+
if (installStderr)
|
|
6870
|
+
console.warn(installStderr);
|
|
6871
|
+
}
|
|
6872
|
+
catch (installError) {
|
|
6873
|
+
console.error('โ Failed to install dependencies:', installError);
|
|
6874
|
+
const errorMsg = `Failed to install dependencies: ${installError.message}`;
|
|
6875
|
+
return {
|
|
6876
|
+
success: false,
|
|
6877
|
+
exitCode: installError.code || 1,
|
|
6878
|
+
stdout: installError.stdout || '',
|
|
6879
|
+
stderr: installError.stderr || installError.message || '',
|
|
6880
|
+
error: errorMsg,
|
|
6881
|
+
};
|
|
6882
|
+
}
|
|
6883
|
+
// Run Playwright tests with streaming output
|
|
6884
|
+
console.log(`๐ Running Playwright tests in ${projectDir}...`);
|
|
6885
|
+
if (playwrightArgs.length > 0) {
|
|
6886
|
+
console.log(`๐งช Playwright extra args: ${playwrightArgs.join(' ')}`);
|
|
6887
|
+
}
|
|
6888
|
+
return new Promise(async (resolve) => {
|
|
6889
|
+
var _a, _b;
|
|
6890
|
+
let stdout = '';
|
|
6891
|
+
let stderr = '';
|
|
6892
|
+
let hasResolved = false;
|
|
6893
|
+
let stdoutLineBuffer = '';
|
|
6894
|
+
// Use spawn for streaming output
|
|
6895
|
+
const testProcess = spawn('npx', ['playwright', 'test', ...playwrightArgs], {
|
|
6896
|
+
cwd: projectDir,
|
|
6897
|
+
shell: true,
|
|
6898
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
6899
|
+
});
|
|
6900
|
+
// Stream stdout line by line
|
|
6901
|
+
(_a = testProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', async (data) => {
|
|
6902
|
+
var _a;
|
|
6903
|
+
const chunk = data.toString();
|
|
6904
|
+
stdoutLineBuffer += chunk;
|
|
6905
|
+
// Process complete lines
|
|
6906
|
+
const parts = stdoutLineBuffer.split(/\n/);
|
|
6907
|
+
stdoutLineBuffer = (_a = parts.pop()) !== null && _a !== void 0 ? _a : '';
|
|
6908
|
+
for (const rawLine of parts) {
|
|
6909
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
6910
|
+
stdout += rawLine + '\n';
|
|
6911
|
+
// Call stdout callback if provided
|
|
6912
|
+
if (onStdout) {
|
|
6913
|
+
await onStdout(rawLine + '\n');
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
});
|
|
6917
|
+
// Stream stderr line by line
|
|
6918
|
+
(_b = testProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', async (data) => {
|
|
6919
|
+
const chunk = data.toString();
|
|
6920
|
+
stderr += chunk;
|
|
6921
|
+
// Call stderr callback if provided
|
|
6922
|
+
if (onStderr) {
|
|
6923
|
+
await onStderr(chunk);
|
|
6924
|
+
}
|
|
6925
|
+
});
|
|
6926
|
+
// Handle process completion
|
|
6927
|
+
testProcess.on('close', async (code) => {
|
|
6928
|
+
if (hasResolved)
|
|
6929
|
+
return;
|
|
6930
|
+
hasResolved = true;
|
|
6931
|
+
// Flush any remaining buffered stdout
|
|
6932
|
+
if (stdoutLineBuffer) {
|
|
6933
|
+
stdout += stdoutLineBuffer;
|
|
6934
|
+
if (onStdout) {
|
|
6935
|
+
await onStdout(stdoutLineBuffer);
|
|
6936
|
+
}
|
|
6937
|
+
}
|
|
6938
|
+
const exitCode = code !== null && code !== void 0 ? code : 1;
|
|
6939
|
+
const success = exitCode === 0;
|
|
6940
|
+
const isTestFailure = exitCode === 1 && stdout.length > 0;
|
|
6941
|
+
console.log(success ? 'โ
Tests completed successfully' : (isTestFailure ? 'โ ๏ธ Tests completed with failures' : 'โ Test execution failed'));
|
|
6942
|
+
resolve({
|
|
6943
|
+
success: success,
|
|
6944
|
+
exitCode: exitCode,
|
|
6945
|
+
stdout: stdout,
|
|
6946
|
+
stderr: stderr,
|
|
6947
|
+
error: success || isTestFailure ? undefined : `Test execution failed with exit code ${exitCode}`,
|
|
6948
|
+
});
|
|
6949
|
+
});
|
|
6950
|
+
// Handle process errors
|
|
6951
|
+
testProcess.on('error', async (error) => {
|
|
6952
|
+
if (hasResolved)
|
|
6953
|
+
return;
|
|
6954
|
+
hasResolved = true;
|
|
6955
|
+
const errorMessage = error.message || String(error);
|
|
6956
|
+
stderr += errorMessage;
|
|
6957
|
+
console.error('โ Test execution failed:', error);
|
|
6958
|
+
resolve({
|
|
6959
|
+
success: false,
|
|
6960
|
+
exitCode: 1,
|
|
6961
|
+
stdout: stdout,
|
|
6962
|
+
stderr: stderr,
|
|
6963
|
+
error: `Test execution failed: ${errorMessage}`,
|
|
6964
|
+
});
|
|
6965
|
+
});
|
|
6966
|
+
// Set timeout (1 hour)
|
|
6967
|
+
setTimeout(async () => {
|
|
6968
|
+
if (!hasResolved) {
|
|
6969
|
+
hasResolved = true;
|
|
6970
|
+
testProcess.kill();
|
|
6971
|
+
const errorMessage = 'Test execution timed out after 1 hour';
|
|
6972
|
+
stderr += errorMessage;
|
|
6973
|
+
resolve({
|
|
6974
|
+
success: false,
|
|
6975
|
+
exitCode: 1,
|
|
6976
|
+
stdout: stdout,
|
|
6977
|
+
stderr: stderr,
|
|
6978
|
+
error: errorMessage,
|
|
6979
|
+
});
|
|
6980
|
+
}
|
|
6981
|
+
}, 3600000); // 1 hour timeout
|
|
6982
|
+
});
|
|
6983
|
+
}
|
|
6984
|
+
catch (error) {
|
|
6985
|
+
console.error('โ Error running project scenarios:', error);
|
|
6986
|
+
const errorMsg = error.message || String(error);
|
|
6987
|
+
return {
|
|
6988
|
+
success: false,
|
|
6989
|
+
exitCode: 1,
|
|
6990
|
+
stdout: '',
|
|
6991
|
+
stderr: errorMsg,
|
|
6992
|
+
error: errorMsg,
|
|
6993
|
+
};
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6664
6996
|
/**
|
|
6665
6997
|
* Upload artifacts for a test suite run
|
|
6666
6998
|
*
|