@probolabs/playwright 1.4.0-rc.6 โ†’ 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/dist/index.cjs CHANGED
@@ -5761,6 +5761,7 @@ export default class ProboReporter implements Reporter {
5761
5761
  */
5762
5762
  static async fetchScenarioData(scenarioId, apiToken, apiUrl) {
5763
5763
  const normalizedUrl = this.normalizeApiUrl(apiUrl);
5764
+ // Scenario interactions endpoint is under /api/ (special endpoint)
5764
5765
  const url = `${normalizedUrl}/api/scenarios/${scenarioId}/interactions`;
5765
5766
  const response = await fetch(url, {
5766
5767
  method: 'GET',
@@ -5795,6 +5796,54 @@ export default class ProboReporter implements Reporter {
5795
5796
  }
5796
5797
  return response.json();
5797
5798
  }
5799
+ /**
5800
+ * Fetch project data from the backend API
5801
+ */
5802
+ static async fetchProjectData(projectId, apiToken, apiUrl) {
5803
+ const normalizedUrl = this.normalizeApiUrl(apiUrl);
5804
+ // Projects endpoint is at root level, not under /api/
5805
+ const url = `${normalizedUrl}/projects/${projectId}/`;
5806
+ const response = await fetch(url, {
5807
+ method: 'GET',
5808
+ headers: {
5809
+ 'Authorization': `Token ${apiToken}`,
5810
+ 'Content-Type': 'application/json',
5811
+ },
5812
+ });
5813
+ if (!response.ok) {
5814
+ const errorText = await this.readResponseErrorText(response);
5815
+ throw new Error(`Failed to fetch project ${projectId}: ${response.status} ${errorText}`);
5816
+ }
5817
+ return response.json();
5818
+ }
5819
+ /**
5820
+ * Fetch all scenarios for a project from the backend API
5821
+ */
5822
+ static async fetchProjectScenarios(projectId, apiToken, apiUrl) {
5823
+ const normalizedUrl = this.normalizeApiUrl(apiUrl);
5824
+ // Scenarios endpoint is at root level, not under /api/
5825
+ const url = `${normalizedUrl}/scenarios/?project_id=${projectId}`;
5826
+ const response = await fetch(url, {
5827
+ method: 'GET',
5828
+ headers: {
5829
+ 'Authorization': `Token ${apiToken}`,
5830
+ 'Content-Type': 'application/json',
5831
+ },
5832
+ });
5833
+ if (!response.ok) {
5834
+ const errorText = await this.readResponseErrorText(response);
5835
+ throw new Error(`Failed to fetch scenarios for project ${projectId}: ${response.status} ${errorText}`);
5836
+ }
5837
+ const data = await response.json();
5838
+ // Handle both array and paginated responses
5839
+ if (Array.isArray(data)) {
5840
+ return data;
5841
+ }
5842
+ if (data.results && Array.isArray(data.results)) {
5843
+ return data.results;
5844
+ }
5845
+ return [];
5846
+ }
5798
5847
  /**
5799
5848
  * Convert backend interaction format to Interaction[] format
5800
5849
  */
@@ -5945,6 +5994,48 @@ export default class ProboReporter implements Reporter {
5945
5994
  scenarios: scenarioResults,
5946
5995
  };
5947
5996
  }
5997
+ /**
5998
+ * Generate code for all scenarios in a project
5999
+ * Returns map of scenario names to generated code
6000
+ */
6001
+ static async generateCodeForProject(projectId, apiToken, apiUrl, options) {
6002
+ // Validate inputs
6003
+ if (!apiToken) {
6004
+ throw new Error('API token is required');
6005
+ }
6006
+ if (!apiUrl) {
6007
+ throw new Error('API URL is required');
6008
+ }
6009
+ // Fetch project data and scenarios
6010
+ const [projectData, scenarios] = await Promise.all([
6011
+ this.fetchProjectData(projectId, apiToken, apiUrl),
6012
+ this.fetchProjectScenarios(projectId, apiToken, apiUrl),
6013
+ ]);
6014
+ if (scenarios.length === 0) {
6015
+ throw new Error(`No scenarios found in project "${projectData.name}" (ID: ${projectId})`);
6016
+ }
6017
+ // Generate code for each scenario
6018
+ const scenarioResults = await Promise.all(scenarios.map(async (scenario) => {
6019
+ try {
6020
+ const code = await this.generateCodeForScenario(scenario.id, apiToken, apiUrl, options);
6021
+ return {
6022
+ scenarioId: scenario.id,
6023
+ scenarioName: scenario.name,
6024
+ code: code,
6025
+ };
6026
+ }
6027
+ catch (error) {
6028
+ // Log error but continue with other scenarios
6029
+ console.error(`Failed to generate code for scenario ${scenario.id}: ${error.message}`);
6030
+ throw error; // Re-throw to fail fast for now
6031
+ }
6032
+ }));
6033
+ return {
6034
+ projectId: projectData.id,
6035
+ projectName: projectData.name,
6036
+ scenarios: scenarioResults,
6037
+ };
6038
+ }
5948
6039
  }
5949
6040
 
5950
6041
  const execAsync = util.promisify(child_process.exec);
@@ -6066,6 +6157,33 @@ export default class ProboReporter implements Reporter {
6066
6157
  * TestSuiteRunner - Handles test suite file generation and execution
6067
6158
  */
6068
6159
  class TestSuiteRunner {
6160
+ /**
6161
+ * Lookup project ID by name
6162
+ */
6163
+ static async lookupProjectByName(projectName, apiToken, apiUrl) {
6164
+ const baseUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
6165
+ // Projects endpoint is at root level, not under /api/
6166
+ const url = `${baseUrl}/projects/?name=${encodeURIComponent(projectName)}`;
6167
+ const response = await fetch$1(url, {
6168
+ method: 'GET',
6169
+ headers: {
6170
+ 'Authorization': `Token ${apiToken}`,
6171
+ 'Content-Type': 'application/json',
6172
+ },
6173
+ });
6174
+ if (!response.ok) {
6175
+ const errorText = await response.text();
6176
+ throw new Error(`Failed to lookup project: ${response.status} ${errorText}`);
6177
+ }
6178
+ const data = await response.json();
6179
+ if (Array.isArray(data) && data.length > 0) {
6180
+ return data[0].id;
6181
+ }
6182
+ if (data.results && Array.isArray(data.results) && data.results.length > 0) {
6183
+ return data.results[0].id;
6184
+ }
6185
+ throw new Error(`Project "${projectName}" not found`);
6186
+ }
6069
6187
  /**
6070
6188
  * Lookup test suite ID by name and project
6071
6189
  */
@@ -6201,7 +6319,10 @@ export default class ProboReporter implements Reporter {
6201
6319
  }
6202
6320
  }
6203
6321
  }
6204
- const sanitizedName = slugify(codeGenResult.testSuiteName);
6322
+ // Type guard: check for projectId to distinguish ProjectCodeGenResult from TestSuiteCodeGenResult
6323
+ const sanitizedName = 'projectId' in codeGenResult && 'projectName' in codeGenResult
6324
+ ? slugify(codeGenResult.projectName)
6325
+ : slugify(codeGenResult.testSuiteName);
6205
6326
  const packageJsonContent = generatePackageJson({
6206
6327
  name: sanitizedName,
6207
6328
  hasSecrets: hasSecrets,
@@ -6234,12 +6355,12 @@ export default class ProboReporter implements Reporter {
6234
6355
  * Generates files, installs dependencies, and executes tests
6235
6356
  */
6236
6357
  static async runTestSuite(testSuiteId, apiToken, apiUrl, testSuiteName, runId, options = {}) {
6237
- const { outputDir, includeReporter = true, playwrightArgs = [], onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
6358
+ const { outputDir, includeReporter = true, playwrightArgs = [], skipRunCreation = false, onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
6238
6359
  const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
6239
6360
  let currentRunId = runId;
6240
6361
  try {
6241
- // Create run record if not provided (needed for run-specific report directories)
6242
- if (!currentRunId) {
6362
+ // Create run record if not provided and not skipping run creation
6363
+ if (!currentRunId && !skipRunCreation) {
6243
6364
  try {
6244
6365
  const createResponse = await fetch$1(`${apiUrl}/test-suites/${testSuiteId}/runs/`, {
6245
6366
  method: 'POST',
@@ -6664,6 +6785,233 @@ export default class ProboReporter implements Reporter {
6664
6785
  };
6665
6786
  }
6666
6787
  }
6788
+ /**
6789
+ * Generate all files for a project (all scenarios)
6790
+ */
6791
+ static async generateProjectFiles(projectId, apiToken, apiUrl, outputDir, projectName, includeReporter = true) {
6792
+ const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
6793
+ // Generate code for all scenarios
6794
+ const codeGenResult = await ProboCodeGenerator.generateCodeForProject(projectId, apiToken, apiUrl);
6795
+ if (codeGenResult.scenarios.length === 0) {
6796
+ throw new Error(`No scenarios found in project "${codeGenResult.projectName}" (ID: ${projectId}). Cannot generate test files.`);
6797
+ }
6798
+ // Delete everything in the project directory except node_modules
6799
+ if (fs__namespace.existsSync(projectDir)) {
6800
+ try {
6801
+ const nodeModulesPath = path__namespace.join(projectDir, 'node_modules');
6802
+ const hasNodeModules = fs__namespace.existsSync(nodeModulesPath);
6803
+ // Temporarily move node_modules out of the way if it exists
6804
+ let tempNodeModulesPath = null;
6805
+ if (hasNodeModules) {
6806
+ tempNodeModulesPath = path__namespace.join(projectDir, '..', `node_modules.temp.${projectId}`);
6807
+ // Remove temp directory if it exists from a previous failed run
6808
+ if (fs__namespace.existsSync(tempNodeModulesPath)) {
6809
+ fs__namespace.rmSync(tempNodeModulesPath, { recursive: true, force: true });
6810
+ }
6811
+ fs__namespace.renameSync(nodeModulesPath, tempNodeModulesPath);
6812
+ console.log(`๐Ÿ“ฆ Preserved node_modules temporarily`);
6813
+ }
6814
+ // Delete everything in the directory
6815
+ const entries = fs__namespace.readdirSync(projectDir, { withFileTypes: true });
6816
+ for (const entry of entries) {
6817
+ const entryPath = path__namespace.join(projectDir, entry.name);
6818
+ try {
6819
+ if (entry.isDirectory()) {
6820
+ fs__namespace.rmSync(entryPath, { recursive: true, force: true });
6821
+ }
6822
+ else {
6823
+ fs__namespace.unlinkSync(entryPath);
6824
+ }
6825
+ }
6826
+ catch (error) {
6827
+ console.warn(`โš ๏ธ Failed to delete ${entry.name}:`, error);
6828
+ }
6829
+ }
6830
+ console.log(`๐Ÿ—‘๏ธ Cleaned project directory (preserved node_modules)`);
6831
+ // Move node_modules back
6832
+ if (hasNodeModules && tempNodeModulesPath) {
6833
+ fs__namespace.renameSync(tempNodeModulesPath, nodeModulesPath);
6834
+ console.log(`๐Ÿ“ฆ Restored node_modules`);
6835
+ }
6836
+ }
6837
+ catch (error) {
6838
+ console.warn(`โš ๏ธ Failed to clean project directory ${projectDir}:`, error);
6839
+ // Continue anyway - we'll overwrite files as needed
6840
+ }
6841
+ }
6842
+ // Ensure directories exist (will recreate after deletion)
6843
+ ensureDirectoryExists(projectDir);
6844
+ const testsDir = path__namespace.join(projectDir, 'tests');
6845
+ ensureDirectoryExists(testsDir);
6846
+ // Save each scenario's code to a .spec.ts file
6847
+ for (const scenario of codeGenResult.scenarios) {
6848
+ const fileName = `${slugify(scenario.scenarioName)}.spec.ts`;
6849
+ const filePath = path__namespace.join(testsDir, fileName);
6850
+ fs__namespace.writeFileSync(filePath, scenario.code, 'utf-8');
6851
+ console.log(`โœ… Generated test file: ${filePath}`);
6852
+ }
6853
+ // Generate package.json
6854
+ await this.generatePackageJson(projectDir, codeGenResult);
6855
+ // Generate playwright.config.ts (no runId for project runs)
6856
+ await this.generatePlaywrightConfig(projectDir, includeReporter, undefined);
6857
+ // Generate custom reporter file for live progress streaming (only if requested)
6858
+ if (includeReporter) {
6859
+ await this.generateProboReporter(projectDir);
6860
+ }
6861
+ }
6862
+ /**
6863
+ * Run all scenarios in a project
6864
+ * Generates files, installs dependencies, and executes tests
6865
+ * Does not create run records in the database (CLI mode)
6866
+ */
6867
+ static async runProjectScenarios(projectId, apiToken, apiUrl, projectName, options = {}) {
6868
+ const { outputDir, includeReporter = false, // CLI mode - no custom reporter
6869
+ playwrightArgs = [], onStdout, onStderr, } = options;
6870
+ const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
6871
+ try {
6872
+ // Generate all files
6873
+ console.log(`๐Ÿ“ Generating test files for project ${projectId}...`);
6874
+ await this.generateProjectFiles(projectId, apiToken, apiUrl, projectDir, projectName, includeReporter);
6875
+ // Count total tests
6876
+ const testsTotal = countTotalTests(projectDir);
6877
+ if (testsTotal === 0) {
6878
+ throw new Error(`No test files generated for project ${projectId}`);
6879
+ }
6880
+ // Install dependencies
6881
+ console.log(`๐Ÿ“ฆ Installing dependencies in ${projectDir}...`);
6882
+ try {
6883
+ const { stdout: installStdout, stderr: installStderr } = await execAsync('npm install', { cwd: projectDir, timeout: 300000 } // 5 minute timeout for install
6884
+ );
6885
+ console.log('โœ… Dependencies installed successfully');
6886
+ if (installStdout)
6887
+ console.log(installStdout);
6888
+ if (installStderr)
6889
+ console.warn(installStderr);
6890
+ }
6891
+ catch (installError) {
6892
+ console.error('โŒ Failed to install dependencies:', installError);
6893
+ const errorMsg = `Failed to install dependencies: ${installError.message}`;
6894
+ return {
6895
+ success: false,
6896
+ exitCode: installError.code || 1,
6897
+ stdout: installError.stdout || '',
6898
+ stderr: installError.stderr || installError.message || '',
6899
+ error: errorMsg,
6900
+ };
6901
+ }
6902
+ // Run Playwright tests with streaming output
6903
+ console.log(`๐Ÿš€ Running Playwright tests in ${projectDir}...`);
6904
+ if (playwrightArgs.length > 0) {
6905
+ console.log(`๐Ÿงช Playwright extra args: ${playwrightArgs.join(' ')}`);
6906
+ }
6907
+ return new Promise(async (resolve) => {
6908
+ var _a, _b;
6909
+ let stdout = '';
6910
+ let stderr = '';
6911
+ let hasResolved = false;
6912
+ let stdoutLineBuffer = '';
6913
+ // Use spawn for streaming output
6914
+ const testProcess = child_process.spawn('npx', ['playwright', 'test', ...playwrightArgs], {
6915
+ cwd: projectDir,
6916
+ shell: true,
6917
+ stdio: ['ignore', 'pipe', 'pipe'],
6918
+ });
6919
+ // Stream stdout line by line
6920
+ (_a = testProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', async (data) => {
6921
+ var _a;
6922
+ const chunk = data.toString();
6923
+ stdoutLineBuffer += chunk;
6924
+ // Process complete lines
6925
+ const parts = stdoutLineBuffer.split(/\n/);
6926
+ stdoutLineBuffer = (_a = parts.pop()) !== null && _a !== void 0 ? _a : '';
6927
+ for (const rawLine of parts) {
6928
+ const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
6929
+ stdout += rawLine + '\n';
6930
+ // Call stdout callback if provided
6931
+ if (onStdout) {
6932
+ await onStdout(rawLine + '\n');
6933
+ }
6934
+ }
6935
+ });
6936
+ // Stream stderr line by line
6937
+ (_b = testProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', async (data) => {
6938
+ const chunk = data.toString();
6939
+ stderr += chunk;
6940
+ // Call stderr callback if provided
6941
+ if (onStderr) {
6942
+ await onStderr(chunk);
6943
+ }
6944
+ });
6945
+ // Handle process completion
6946
+ testProcess.on('close', async (code) => {
6947
+ if (hasResolved)
6948
+ return;
6949
+ hasResolved = true;
6950
+ // Flush any remaining buffered stdout
6951
+ if (stdoutLineBuffer) {
6952
+ stdout += stdoutLineBuffer;
6953
+ if (onStdout) {
6954
+ await onStdout(stdoutLineBuffer);
6955
+ }
6956
+ }
6957
+ const exitCode = code !== null && code !== void 0 ? code : 1;
6958
+ const success = exitCode === 0;
6959
+ const isTestFailure = exitCode === 1 && stdout.length > 0;
6960
+ console.log(success ? 'โœ… Tests completed successfully' : (isTestFailure ? 'โš ๏ธ Tests completed with failures' : 'โŒ Test execution failed'));
6961
+ resolve({
6962
+ success: success,
6963
+ exitCode: exitCode,
6964
+ stdout: stdout,
6965
+ stderr: stderr,
6966
+ error: success || isTestFailure ? undefined : `Test execution failed with exit code ${exitCode}`,
6967
+ });
6968
+ });
6969
+ // Handle process errors
6970
+ testProcess.on('error', async (error) => {
6971
+ if (hasResolved)
6972
+ return;
6973
+ hasResolved = true;
6974
+ const errorMessage = error.message || String(error);
6975
+ stderr += errorMessage;
6976
+ console.error('โŒ Test execution failed:', error);
6977
+ resolve({
6978
+ success: false,
6979
+ exitCode: 1,
6980
+ stdout: stdout,
6981
+ stderr: stderr,
6982
+ error: `Test execution failed: ${errorMessage}`,
6983
+ });
6984
+ });
6985
+ // Set timeout (1 hour)
6986
+ setTimeout(async () => {
6987
+ if (!hasResolved) {
6988
+ hasResolved = true;
6989
+ testProcess.kill();
6990
+ const errorMessage = 'Test execution timed out after 1 hour';
6991
+ stderr += errorMessage;
6992
+ resolve({
6993
+ success: false,
6994
+ exitCode: 1,
6995
+ stdout: stdout,
6996
+ stderr: stderr,
6997
+ error: errorMessage,
6998
+ });
6999
+ }
7000
+ }, 3600000); // 1 hour timeout
7001
+ });
7002
+ }
7003
+ catch (error) {
7004
+ console.error('โŒ Error running project scenarios:', error);
7005
+ const errorMsg = error.message || String(error);
7006
+ return {
7007
+ success: false,
7008
+ exitCode: 1,
7009
+ stdout: '',
7010
+ stderr: errorMsg,
7011
+ error: errorMsg,
7012
+ };
7013
+ }
7014
+ }
6667
7015
  /**
6668
7016
  * Upload artifacts for a test suite run
6669
7017
  *