@probolabs/playwright 1.4.0-rc.6 โ†’ 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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);
@@ -6047,6 +6138,33 @@ async function updateRunStatus(runId, testSuiteId, updates, apiToken, apiUrl) {
6047
6138
  * TestSuiteRunner - Handles test suite file generation and execution
6048
6139
  */
6049
6140
  class TestSuiteRunner {
6141
+ /**
6142
+ * Lookup project ID by name
6143
+ */
6144
+ static async lookupProjectByName(projectName, apiToken, apiUrl) {
6145
+ const baseUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
6146
+ // Projects endpoint is at root level, not under /api/
6147
+ const url = `${baseUrl}/projects/?name=${encodeURIComponent(projectName)}`;
6148
+ const response = await fetch$1(url, {
6149
+ method: 'GET',
6150
+ headers: {
6151
+ 'Authorization': `Token ${apiToken}`,
6152
+ 'Content-Type': 'application/json',
6153
+ },
6154
+ });
6155
+ if (!response.ok) {
6156
+ const errorText = await response.text();
6157
+ throw new Error(`Failed to lookup project: ${response.status} ${errorText}`);
6158
+ }
6159
+ const data = await response.json();
6160
+ if (Array.isArray(data) && data.length > 0) {
6161
+ return data[0].id;
6162
+ }
6163
+ if (data.results && Array.isArray(data.results) && data.results.length > 0) {
6164
+ return data.results[0].id;
6165
+ }
6166
+ throw new Error(`Project "${projectName}" not found`);
6167
+ }
6050
6168
  /**
6051
6169
  * Lookup test suite ID by name and project
6052
6170
  */
@@ -6182,7 +6300,10 @@ class TestSuiteRunner {
6182
6300
  }
6183
6301
  }
6184
6302
  }
6185
- const sanitizedName = slugify(codeGenResult.testSuiteName);
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);
6186
6307
  const packageJsonContent = generatePackageJson({
6187
6308
  name: sanitizedName,
6188
6309
  hasSecrets: hasSecrets,
@@ -6215,12 +6336,12 @@ class TestSuiteRunner {
6215
6336
  * Generates files, installs dependencies, and executes tests
6216
6337
  */
6217
6338
  static async runTestSuite(testSuiteId, apiToken, apiUrl, testSuiteName, runId, options = {}) {
6218
- const { outputDir, includeReporter = true, playwrightArgs = [], onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
6339
+ const { outputDir, includeReporter = true, playwrightArgs = [], skipRunCreation = false, onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
6219
6340
  const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
6220
6341
  let currentRunId = runId;
6221
6342
  try {
6222
- // Create run record if not provided (needed for run-specific report directories)
6223
- if (!currentRunId) {
6343
+ // Create run record if not provided and not skipping run creation
6344
+ if (!currentRunId && !skipRunCreation) {
6224
6345
  try {
6225
6346
  const createResponse = await fetch$1(`${apiUrl}/test-suites/${testSuiteId}/runs/`, {
6226
6347
  method: 'POST',
@@ -6645,6 +6766,233 @@ class TestSuiteRunner {
6645
6766
  };
6646
6767
  }
6647
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
+ }
6648
6996
  /**
6649
6997
  * Upload artifacts for a test suite run
6650
6998
  *