@probolabs/playwright 1.5.0-rc.7 โ†’ 1.5.0-rc.9

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
@@ -432,6 +432,8 @@ interface CodeGenOptions {
432
432
  width: number;
433
433
  height: number;
434
434
  };
435
+ /** Test suite names for Playwright tags (from api/scenarios/light/) */
436
+ testSuiteTags?: string[];
435
437
  }
436
438
  /**
437
439
  * Result of generating code for a test suite
@@ -455,6 +457,7 @@ interface ProjectCodeGenResult {
455
457
  scenarioId: number;
456
458
  scenarioName: string;
457
459
  code: string;
460
+ testSuiteTags?: string[];
458
461
  }>;
459
462
  }
460
463
  /**
@@ -479,7 +482,7 @@ declare class ProboCodeGenerator {
479
482
  */
480
483
  private static fetchProjectData;
481
484
  /**
482
- * Fetch all scenarios for a project from the backend API
485
+ * Fetch all scenarios for a project from the backend API (light endpoint includes test_suite_tags)
483
486
  */
484
487
  private static fetchProjectScenarios;
485
488
  /**
@@ -558,6 +561,11 @@ interface TestSuiteRunOptions {
558
561
  onStderr?: (chunk: string) => void | Promise<void>;
559
562
  onReporterEvent?: (event: any) => void | Promise<void>;
560
563
  }
564
+ /**
565
+ * Gets the project directory path (unified for test suite runs and export).
566
+ * Path: ~/.probium/projects/{projectId}-{projectName-slugified}/
567
+ */
568
+ declare function getProjectDir(projectId: number, projectName?: string): string;
561
569
  /**
562
570
  * TestSuiteRunner - Handles test suite file generation and execution
563
571
  */
@@ -571,9 +579,18 @@ declare class TestSuiteRunner {
571
579
  */
572
580
  static lookupTestSuiteByName(testSuiteName: string, projectName: string, apiToken: string, apiUrl: string): Promise<number>;
573
581
  /**
574
- * Generate all files for a test suite
582
+ * Fetch test suite details (project id, project name, test suite name)
583
+ */
584
+ private static fetchTestSuiteDetails;
585
+ /**
586
+ * Generate all files for a test suite (writes to project-level directory)
575
587
  */
576
588
  static generateTestSuiteFiles(testSuiteId: number, apiToken: string, apiUrl: string, outputDir?: string, testSuiteName?: string, includeReporter?: boolean, runId?: number): Promise<void>;
589
+ /**
590
+ * Generate files only for the scenarios that participate in the test suite.
591
+ * Used by the recorder app test-suite runner so only suite specs are written; then we run npx playwright test normally.
592
+ */
593
+ static generateTestSuiteOnlyFiles(testSuiteId: number, apiToken: string, apiUrl: string, outputDir: string, testSuiteName?: string, includeReporter?: boolean, runId?: number): Promise<void>;
577
594
  /**
578
595
  * Generate package.json file
579
596
  */
@@ -595,6 +612,12 @@ declare class TestSuiteRunner {
595
612
  * Generate all files for a project (all scenarios)
596
613
  */
597
614
  static generateProjectFiles(projectId: number, apiToken: string, apiUrl: string, outputDir?: string, projectName?: string, includeReporter?: boolean): Promise<void>;
615
+ /**
616
+ * Export project code to a directory.
617
+ * If outputDir is provided, creates a subfolder {outputDir}/{projectId}-{projectNameSlug}/ and writes there.
618
+ * Otherwise writes to the default project dir (~/.probium/projects/...).
619
+ */
620
+ static exportProjectCode(projectId: number, apiToken: string, apiUrl: string, outputDir?: string, projectName?: string, includeReporter?: boolean): Promise<string>;
598
621
  /**
599
622
  * Run all scenarios in a project
600
623
  * Generates files, installs dependencies, and executes tests
@@ -674,5 +697,5 @@ declare class Probo {
674
697
  askAIHelper(page: Page, question: string, options: AskAIOptions, assertAnswer?: string): Promise<ServerResponse>;
675
698
  }
676
699
 
677
- export { Highlighter, NavTracker, OTP, Probo, ProboCodeGenerator, ProboPlaywright, TestSuiteRunner, buildInterpolationContext, findClosestVisibleElement, interpolateWithParams };
700
+ export { Highlighter, NavTracker, OTP, Probo, ProboCodeGenerator, ProboPlaywright, TestSuiteRunner, buildInterpolationContext, findClosestVisibleElement, getProjectDir, interpolateWithParams };
678
701
  export type { AskAIOptions, CodeGenOptions, ElementTagType, MailinatorMessage, RunStepOptions, TestStatistics, TestSuiteCodeGenResult, TestSuiteRunOptions, TestSuiteRunResult };
package/dist/index.js CHANGED
@@ -1296,6 +1296,16 @@ const DEFAULT_RECORDER_SETTINGS = {
1296
1296
  };
1297
1297
 
1298
1298
  // --- Code generation utilities for Probo Labs Playwright scripts ---
1299
+ /**
1300
+ * Converts a test suite name to a Playwright tag (slugify + @ prefix).
1301
+ * Playwright tags must start with @.
1302
+ */
1303
+ function slugifyTag(tag) {
1304
+ if (!tag || typeof tag !== 'string')
1305
+ return '@untagged';
1306
+ const slug = slugify(tag);
1307
+ return slug ? `@${slug}` : '@untagged';
1308
+ }
1299
1309
  /**
1300
1310
  * Extracts environment variable names from parameter table rows
1301
1311
  * by parsing ${process.env.VAR_NAME} patterns from parameter values
@@ -1604,6 +1614,13 @@ function scriptTemplate(options, settings, viewPort) {
1604
1614
  }).join(',\n ')}
1605
1615
  ];
1606
1616
  ` : '';
1617
+ // Playwright tags for test suite filtering (e.g. npx playwright test --grep @sanity)
1618
+ const playwrightTags = options.testSuiteTags && options.testSuiteTags.length > 0
1619
+ ? options.testSuiteTags.map(slugifyTag).filter((t) => t !== '@untagged')
1620
+ : [];
1621
+ const describeTagOption = playwrightTags.length > 0
1622
+ ? `{ tag: [${playwrightTags.map((t) => `'${t}'`).join(', ')}] }, `
1623
+ : '';
1607
1624
  const proboConstructor = hasAiInteractions ? `
1608
1625
  const probo = new Probo({
1609
1626
  scenarioName: '${options.scenarioName}',
@@ -1680,7 +1697,7 @@ ${hasSecrets ? `import { config } from 'dotenv';
1680
1697
  }
1681
1698
  });
1682
1699
 
1683
- test.describe('${options.scenarioName}', () => {
1700
+ test.describe('${options.scenarioName}', ${describeTagOption}() => {
1684
1701
  test.beforeEach(async ({ page }) => {
1685
1702
  // set the ProboPlaywright instance to the current page
1686
1703
  ppw.setPage(page);
@@ -1756,7 +1773,7 @@ const DEFAULT_PLAYWRIGHT_TEST_VERSION = '^1.57.0';
1756
1773
  * Generate package.json content for a Playwright test project
1757
1774
  */
1758
1775
  function generatePackageJson(options) {
1759
- const { name, hasSecrets = false, testScript = 'npx playwright test', playwrightTestVersion = DEFAULT_PLAYWRIGHT_TEST_VERSION } = options;
1776
+ const { name, hasSecrets = false, testScript = 'npx playwright test', playwrightTestVersion = DEFAULT_PLAYWRIGHT_TEST_VERSION, probolabsPlaywrightVersion = 'latest' } = options;
1760
1777
  return `{
1761
1778
  "name": "${name}",
1762
1779
  "version": "1.0.0",
@@ -1767,7 +1784,7 @@ function generatePackageJson(options) {
1767
1784
  },
1768
1785
  "dependencies": {
1769
1786
  "@playwright/test": "${playwrightTestVersion}",
1770
- "@probolabs/playwright": "latest"${hasSecrets ? ',\n "dotenv": "latest"' : ''}
1787
+ "@probolabs/playwright": "${probolabsPlaywrightVersion}"${hasSecrets ? ',\n "dotenv": "latest"' : ''}
1771
1788
  }
1772
1789
  }`;
1773
1790
  }
@@ -5106,16 +5123,21 @@ class ProboPlaywright {
5106
5123
  this.nextTabIndex = 1;
5107
5124
  this.params = {};
5108
5125
  this.isCanceled = null;
5126
+ proboLogger.setLogLevel(debugLevel);
5109
5127
  this.enableSmartSelectors = enableSmartSelectors;
5110
5128
  this.timeoutConfig = mergeTimeoutConfigWithEnv({
5111
5129
  ...DEFAULT_PLAYWRIGHT_TIMEOUT_CONFIG,
5112
5130
  ...timeoutConfig
5113
5131
  });
5132
+ proboLogger.debug(`ProboPlaywright timeout config: playwrightActionTimeout=${this.timeoutConfig.playwrightActionTimeout}ms, ` +
5133
+ `playwrightNavigationTimeout=${this.timeoutConfig.playwrightNavigationTimeout}ms, ` +
5134
+ `playwrightLocatorTimeout=${this.timeoutConfig.playwrightLocatorTimeout}ms, ` +
5135
+ `waitForStabilityQuietTimeout=${this.timeoutConfig.waitForStabilityQuietTimeout}ms, ` +
5136
+ `waitForStabilityGlobalTimeout=${this.timeoutConfig.waitForStabilityGlobalTimeout}ms`);
5114
5137
  this.isCanceled = isCanceled || null;
5115
5138
  this.highlighter = new Highlighter(enableSmartSelectors, false, debugLevel);
5116
5139
  this.context = context !== null && context !== void 0 ? context : null;
5117
5140
  this.setPage(page);
5118
- proboLogger.setLogLevel(debugLevel);
5119
5141
  }
5120
5142
  /**
5121
5143
  * Sets the Playwright page instance for this ProboPlaywright instance.
@@ -6106,12 +6128,12 @@ class ProboCodeGenerator {
6106
6128
  return response.json();
6107
6129
  }
6108
6130
  /**
6109
- * Fetch all scenarios for a project from the backend API
6131
+ * Fetch all scenarios for a project from the backend API (light endpoint includes test_suite_tags)
6110
6132
  */
6111
6133
  static async fetchProjectScenarios(projectId, apiToken, apiUrl) {
6112
6134
  const normalizedUrl = this.normalizeApiUrl(apiUrl);
6113
- // Scenarios endpoint is at root level, not under /api/
6114
- const url = `${normalizedUrl}/scenarios/?project_id=${projectId}`;
6135
+ // Use api/scenarios/light/ to get test_suite_tags for Playwright tagging
6136
+ const url = `${normalizedUrl}/api/scenarios/light/?project_id=${projectId}`;
6115
6137
  const response = await fetch(url, {
6116
6138
  method: 'GET',
6117
6139
  headers: {
@@ -6249,6 +6271,7 @@ class ProboCodeGenerator {
6249
6271
  scenarioName: scenarioData.name,
6250
6272
  interactions: interactions,
6251
6273
  rows: rows,
6274
+ ...((options === null || options === void 0 ? void 0 : options.testSuiteTags) && options.testSuiteTags.length > 0 ? { testSuiteTags: options.testSuiteTags } : {}),
6252
6275
  };
6253
6276
  return generateCode(codeOptions, settings, viewPort);
6254
6277
  }
@@ -6308,14 +6331,15 @@ class ProboCodeGenerator {
6308
6331
  if (scenarios.length === 0) {
6309
6332
  throw new Error(`No scenarios found in project "${projectData.name}" (ID: ${projectId})`);
6310
6333
  }
6311
- // Generate code for each scenario
6334
+ // Generate code for each scenario (with test_suite_tags for Playwright tagging)
6312
6335
  const scenarioResults = await Promise.all(scenarios.map(async (scenario) => {
6313
6336
  try {
6314
- const code = await this.generateCodeForScenario(scenario.id, apiToken, apiUrl, options);
6337
+ const code = await this.generateCodeForScenario(scenario.id, apiToken, apiUrl, { ...options, testSuiteTags: scenario.test_suite_tags });
6315
6338
  return {
6316
6339
  scenarioId: scenario.id,
6317
6340
  scenarioName: scenario.name,
6318
6341
  code: code,
6342
+ testSuiteTags: scenario.test_suite_tags,
6319
6343
  };
6320
6344
  }
6321
6345
  catch (error) {
@@ -6342,15 +6366,13 @@ function ensureDirectoryExists(dir) {
6342
6366
  }
6343
6367
  }
6344
6368
  /**
6345
- * Gets the default test suite directory path
6369
+ * Gets the project directory path (unified for test suite runs and export).
6370
+ * Path: ~/.probium/projects/{projectId}-{projectName-slugified}/
6346
6371
  */
6347
- function getDefaultTestSuiteDir(testSuiteId, testSuiteName) {
6348
- const baseDir = path.join(os.homedir(), '.probium', 'test-suites');
6349
- if (testSuiteName) {
6350
- const sanitizedName = slugify(testSuiteName);
6351
- return path.join(baseDir, `${testSuiteId}-${sanitizedName}`);
6352
- }
6353
- return path.join(baseDir, testSuiteId.toString());
6372
+ function getProjectDir(projectId, projectName) {
6373
+ const baseDir = path.join(os.homedir(), '.probium', 'projects');
6374
+ const sanitizedName = projectName ? slugify(projectName) : `project-${projectId}`;
6375
+ return path.join(baseDir, `${projectId}-${sanitizedName}`);
6354
6376
  }
6355
6377
  /**
6356
6378
  * Count total tests by scanning spec files
@@ -6545,36 +6567,67 @@ class TestSuiteRunner {
6545
6567
  throw new Error(`Test suite "${testSuiteName}" not found in project "${projectName}"`);
6546
6568
  }
6547
6569
  /**
6548
- * Generate all files for a test suite
6570
+ * Fetch test suite details (project id, project name, test suite name)
6571
+ */
6572
+ static async fetchTestSuiteDetails(testSuiteId, apiToken, apiUrl) {
6573
+ var _a, _b, _c;
6574
+ const baseUrl = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl;
6575
+ const response = await fetch$1(`${baseUrl}/test-suites/${testSuiteId}/`, {
6576
+ method: 'GET',
6577
+ headers: {
6578
+ 'Authorization': `Token ${apiToken}`,
6579
+ 'Content-Type': 'application/json',
6580
+ },
6581
+ });
6582
+ if (!response.ok) {
6583
+ const errorText = await response.text();
6584
+ throw new Error(`Failed to fetch test suite ${testSuiteId}: ${response.status} ${errorText}`);
6585
+ }
6586
+ const data = await response.json();
6587
+ const projectId = (_a = data.project) !== null && _a !== void 0 ? _a : data.project_id;
6588
+ const projectName = (_b = data.project_name) !== null && _b !== void 0 ? _b : `project-${projectId}`;
6589
+ const testSuiteName = (_c = data.name) !== null && _c !== void 0 ? _c : `suite-${testSuiteId}`;
6590
+ if (!projectId) {
6591
+ throw new Error(`Test suite ${testSuiteId} has no project`);
6592
+ }
6593
+ return { projectId, projectName, testSuiteName };
6594
+ }
6595
+ /**
6596
+ * Generate all files for a test suite (writes to project-level directory)
6549
6597
  */
6550
6598
  static async generateTestSuiteFiles(testSuiteId, apiToken, apiUrl, outputDir, testSuiteName, includeReporter = true, runId) {
6551
- const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
6552
- // Generate code for all scenarios
6599
+ const { projectId, projectName, testSuiteName: resolvedSuiteName } = await this.fetchTestSuiteDetails(testSuiteId, apiToken, apiUrl);
6600
+ const projectDir = outputDir || getProjectDir(projectId, projectName);
6601
+ await this.generateTestSuiteOnlyFiles(testSuiteId, apiToken, apiUrl, projectDir, testSuiteName !== null && testSuiteName !== void 0 ? testSuiteName : resolvedSuiteName, includeReporter, runId);
6602
+ }
6603
+ /**
6604
+ * Generate files only for the scenarios that participate in the test suite.
6605
+ * Used by the recorder app test-suite runner so only suite specs are written; then we run npx playwright test normally.
6606
+ */
6607
+ static async generateTestSuiteOnlyFiles(testSuiteId, apiToken, apiUrl, outputDir, testSuiteName, includeReporter = true, runId) {
6608
+ var _a;
6609
+ const { projectId } = await this.fetchTestSuiteDetails(testSuiteId, apiToken, apiUrl);
6553
6610
  const codeGenResult = await ProboCodeGenerator.generateCodeForTestSuite(testSuiteId, apiToken, apiUrl);
6554
6611
  if (codeGenResult.scenarios.length === 0) {
6555
- throw new Error(`No scenarios found in test suite ${testSuiteId}. Cannot generate test files.`);
6612
+ throw new Error(`No scenarios found in test suite "${codeGenResult.testSuiteName}" (ID: ${testSuiteId}). Cannot generate test files.`);
6556
6613
  }
6557
- // Delete everything in the test suite directory except node_modules
6558
- // This ensures a clean state while preserving dependencies for faster subsequent runs
6559
- if (fs.existsSync(testSuiteDir)) {
6614
+ const projectDir = outputDir;
6615
+ if (fs.existsSync(projectDir)) {
6560
6616
  try {
6561
- const nodeModulesPath = path.join(testSuiteDir, 'node_modules');
6617
+ const nodeModulesPath = path.join(projectDir, 'node_modules');
6562
6618
  const hasNodeModules = fs.existsSync(nodeModulesPath);
6563
- // Temporarily move node_modules out of the way if it exists
6564
6619
  let tempNodeModulesPath = null;
6565
6620
  if (hasNodeModules) {
6566
- tempNodeModulesPath = path.join(testSuiteDir, '..', `node_modules.temp.${testSuiteId}`);
6567
- // Remove temp directory if it exists from a previous failed run
6621
+ tempNodeModulesPath = path.join(projectDir, '..', `node_modules.temp.${projectId}`);
6568
6622
  if (fs.existsSync(tempNodeModulesPath)) {
6569
6623
  fs.rmSync(tempNodeModulesPath, { recursive: true, force: true });
6570
6624
  }
6571
6625
  fs.renameSync(nodeModulesPath, tempNodeModulesPath);
6572
6626
  console.log(`๐Ÿ“ฆ Preserved node_modules temporarily`);
6573
6627
  }
6574
- // Delete everything in the directory
6575
- const entries = fs.readdirSync(testSuiteDir, { withFileTypes: true });
6628
+ const entries = fs.readdirSync(projectDir, { withFileTypes: true });
6576
6629
  for (const entry of entries) {
6577
- const entryPath = path.join(testSuiteDir, entry.name);
6630
+ const entryPath = path.join(projectDir, entry.name);
6578
6631
  try {
6579
6632
  if (entry.isDirectory()) {
6580
6633
  fs.rmSync(entryPath, { recursive: true, force: true });
@@ -6587,36 +6640,33 @@ class TestSuiteRunner {
6587
6640
  console.warn(`โš ๏ธ Failed to delete ${entry.name}:`, error);
6588
6641
  }
6589
6642
  }
6590
- console.log(`๐Ÿ—‘๏ธ Cleaned test suite directory (preserved node_modules)`);
6591
- // Move node_modules back
6643
+ console.log(`๐Ÿ—‘๏ธ Cleaned project directory (preserved node_modules)`);
6592
6644
  if (hasNodeModules && tempNodeModulesPath) {
6593
6645
  fs.renameSync(tempNodeModulesPath, nodeModulesPath);
6594
6646
  console.log(`๐Ÿ“ฆ Restored node_modules`);
6595
6647
  }
6596
6648
  }
6597
6649
  catch (error) {
6598
- console.warn(`โš ๏ธ Failed to clean test suite directory ${testSuiteDir}:`, error);
6599
- // Continue anyway - we'll overwrite files as needed
6650
+ console.warn(`โš ๏ธ Failed to clean project directory ${projectDir}:`, error);
6600
6651
  }
6601
6652
  }
6602
- // Ensure directories exist (will recreate after deletion)
6603
- ensureDirectoryExists(testSuiteDir);
6604
- const testsDir = path.join(testSuiteDir, 'tests');
6653
+ ensureDirectoryExists(projectDir);
6654
+ const testsDir = path.join(projectDir, 'tests');
6605
6655
  ensureDirectoryExists(testsDir);
6606
- // Save each scenario's code to a .spec.ts file
6656
+ const seenSlugs = new Map();
6607
6657
  for (const scenario of codeGenResult.scenarios) {
6608
- const fileName = `${slugify(scenario.scenarioName)}.spec.ts`;
6658
+ const slug = slugify(scenario.scenarioName);
6659
+ const count = (_a = seenSlugs.get(slug)) !== null && _a !== void 0 ? _a : 0;
6660
+ seenSlugs.set(slug, count + 1);
6661
+ const fileName = count > 0 ? `${slug}-${scenario.scenarioId}.spec.ts` : `${slug}.spec.ts`;
6609
6662
  const filePath = path.join(testsDir, fileName);
6610
6663
  fs.writeFileSync(filePath, scenario.code, 'utf-8');
6611
6664
  console.log(`โœ… Generated test file: ${filePath}`);
6612
6665
  }
6613
- // Generate package.json
6614
- await this.generatePackageJson(testSuiteDir, codeGenResult);
6615
- // Generate playwright.config.ts with runId if available
6616
- await this.generatePlaywrightConfig(testSuiteDir, includeReporter, runId);
6617
- // Generate custom reporter file for live progress streaming (only if requested)
6666
+ await this.generatePackageJson(projectDir, codeGenResult);
6667
+ await this.generatePlaywrightConfig(projectDir, includeReporter, runId);
6618
6668
  if (includeReporter) {
6619
- await this.generateProboReporter(testSuiteDir);
6669
+ await this.generateProboReporter(projectDir);
6620
6670
  }
6621
6671
  }
6622
6672
  /**
@@ -6660,7 +6710,8 @@ class TestSuiteRunner {
6660
6710
  const packageJsonContent = generatePackageJson({
6661
6711
  name: sanitizedName,
6662
6712
  hasSecrets: hasSecrets,
6663
- testScript: 'npx playwright test'
6713
+ testScript: 'npx playwright test',
6714
+ probolabsPlaywrightVersion: process.env.PROBO_PLAYWRIGHT_VERSION || 'latest',
6664
6715
  });
6665
6716
  const filePath = path.join(outputDir, 'package.json');
6666
6717
  fs.writeFileSync(filePath, packageJsonContent, 'utf-8');
@@ -6690,7 +6741,8 @@ class TestSuiteRunner {
6690
6741
  */
6691
6742
  static async runTestSuite(testSuiteId, apiToken, apiUrl, testSuiteName, runId, options = {}) {
6692
6743
  const { outputDir, includeReporter = true, playwrightArgs = [], skipRunCreation = false, onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
6693
- const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
6744
+ const details = await this.fetchTestSuiteDetails(testSuiteId, apiToken, apiUrl);
6745
+ const projectDir = outputDir || getProjectDir(details.projectId, details.projectName);
6694
6746
  let currentRunId = runId;
6695
6747
  try {
6696
6748
  // Create run record if not provided and not skipping run creation
@@ -6718,9 +6770,9 @@ class TestSuiteRunner {
6718
6770
  }
6719
6771
  // Generate all files (with runId if available for run-specific report directories)
6720
6772
  console.log(`๐Ÿ“ Generating test suite files for test suite ${testSuiteId}...`);
6721
- await this.generateTestSuiteFiles(testSuiteId, apiToken, apiUrl, testSuiteDir, testSuiteName, includeReporter, currentRunId);
6773
+ await this.generateTestSuiteFiles(testSuiteId, apiToken, apiUrl, projectDir, testSuiteName, includeReporter, currentRunId);
6722
6774
  // Count total tests
6723
- const testsTotal = countTotalTests(testSuiteDir);
6775
+ const testsTotal = countTotalTests(projectDir);
6724
6776
  if (currentRunId && testsTotal > 0) {
6725
6777
  await updateRunStatus(currentRunId, testSuiteId, { tests_total: testsTotal }, apiToken, apiUrl);
6726
6778
  if (onStatusUpdate) {
@@ -6728,10 +6780,10 @@ class TestSuiteRunner {
6728
6780
  }
6729
6781
  }
6730
6782
  // Install dependencies
6731
- console.log(`๐Ÿ“ฆ Installing dependencies in ${testSuiteDir}...`);
6783
+ console.log(`๐Ÿ“ฆ Installing dependencies in ${projectDir}...`);
6732
6784
  try {
6733
6785
  const npmExecutable = findNpmExecutable();
6734
- const { stdout: installStdout, stderr: installStderr } = await execAsync(`${npmExecutable} install`, { cwd: testSuiteDir, timeout: 300000 } // 5 minute timeout for install
6786
+ const { stdout: installStdout, stderr: installStderr } = await execAsync(`${npmExecutable} install`, { cwd: projectDir, timeout: 300000 } // 5 minute timeout for install
6735
6787
  );
6736
6788
  console.log('โœ… Dependencies installed successfully');
6737
6789
  if (installStdout)
@@ -6771,7 +6823,7 @@ class TestSuiteRunner {
6771
6823
  console.log(`๐ŸŒ Installing Playwright Chromium browser...`);
6772
6824
  try {
6773
6825
  const npxExecutable = findNpxExecutable();
6774
- const { stdout: browserStdout, stderr: browserStderr } = await execAsync(`${npxExecutable} playwright install chromium`, { cwd: testSuiteDir, timeout: 300000 } // 5 minute timeout for browser install
6826
+ const { stdout: browserStdout, stderr: browserStderr } = await execAsync(`${npxExecutable} playwright install chromium`, { cwd: projectDir, timeout: 300000 } // 5 minute timeout for browser install
6775
6827
  );
6776
6828
  console.log('โœ… Playwright Chromium browser installed successfully');
6777
6829
  if (browserStdout)
@@ -6785,7 +6837,7 @@ class TestSuiteRunner {
6785
6837
  // or the error will be caught when trying to run tests
6786
6838
  }
6787
6839
  // Run Playwright tests with streaming output
6788
- console.log(`๐Ÿš€ Running Playwright tests in ${testSuiteDir}...`);
6840
+ console.log(`๐Ÿš€ Running Playwright tests in ${projectDir}...`);
6789
6841
  if (playwrightArgs.length > 0) {
6790
6842
  console.log(`๐Ÿงช Playwright extra args: ${playwrightArgs.join(' ')}`);
6791
6843
  }
@@ -6807,7 +6859,7 @@ class TestSuiteRunner {
6807
6859
  // Use spawn for streaming output
6808
6860
  const npxExecutable = findNpxExecutable();
6809
6861
  const testProcess = spawn(npxExecutable, ['playwright', 'test', ...playwrightArgs], {
6810
- cwd: testSuiteDir,
6862
+ cwd: projectDir,
6811
6863
  shell: true,
6812
6864
  stdio: ['ignore', 'pipe', 'pipe'],
6813
6865
  env: {
@@ -6993,8 +7045,8 @@ class TestSuiteRunner {
6993
7045
  console.log(success ? 'โœ… Tests completed successfully' : (isTestFailure ? 'โš ๏ธ Tests completed with failures' : 'โŒ Test execution failed'));
6994
7046
  // Parse final statistics from metadata/stdout as fallback
6995
7047
  let parsedStats = null;
6996
- if (fs.existsSync(path.join(testSuiteDir, 'playwright-report'))) {
6997
- parsedStats = parseStatisticsFromMetadata(testSuiteDir);
7048
+ if (fs.existsSync(path.join(projectDir, 'playwright-report'))) {
7049
+ parsedStats = parseStatisticsFromMetadata(projectDir);
6998
7050
  }
6999
7051
  if (!parsedStats) {
7000
7052
  parsedStats = parsePlaywrightStatistics(stdout);
@@ -7196,7 +7248,8 @@ class TestSuiteRunner {
7196
7248
  * Generate all files for a project (all scenarios)
7197
7249
  */
7198
7250
  static async generateProjectFiles(projectId, apiToken, apiUrl, outputDir, projectName, includeReporter = true) {
7199
- const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
7251
+ var _a;
7252
+ const projectDir = outputDir || getProjectDir(projectId, projectName || `project-${projectId}`);
7200
7253
  // Generate code for all scenarios
7201
7254
  const codeGenResult = await ProboCodeGenerator.generateCodeForProject(projectId, apiToken, apiUrl);
7202
7255
  if (codeGenResult.scenarios.length === 0) {
@@ -7250,9 +7303,12 @@ class TestSuiteRunner {
7250
7303
  ensureDirectoryExists(projectDir);
7251
7304
  const testsDir = path.join(projectDir, 'tests');
7252
7305
  ensureDirectoryExists(testsDir);
7253
- // Save each scenario's code to a .spec.ts file
7306
+ const seenSlugs = new Map();
7254
7307
  for (const scenario of codeGenResult.scenarios) {
7255
- const fileName = `${slugify(scenario.scenarioName)}.spec.ts`;
7308
+ const slug = slugify(scenario.scenarioName);
7309
+ const count = (_a = seenSlugs.get(slug)) !== null && _a !== void 0 ? _a : 0;
7310
+ seenSlugs.set(slug, count + 1);
7311
+ const fileName = count > 0 ? `${slug}-${scenario.scenarioId}.spec.ts` : `${slug}.spec.ts`;
7256
7312
  const filePath = path.join(testsDir, fileName);
7257
7313
  fs.writeFileSync(filePath, scenario.code, 'utf-8');
7258
7314
  console.log(`โœ… Generated test file: ${filePath}`);
@@ -7266,6 +7322,18 @@ class TestSuiteRunner {
7266
7322
  await this.generateProboReporter(projectDir);
7267
7323
  }
7268
7324
  }
7325
+ /**
7326
+ * Export project code to a directory.
7327
+ * If outputDir is provided, creates a subfolder {outputDir}/{projectId}-{projectNameSlug}/ and writes there.
7328
+ * Otherwise writes to the default project dir (~/.probium/projects/...).
7329
+ */
7330
+ static async exportProjectCode(projectId, apiToken, apiUrl, outputDir, projectName, includeReporter = false) {
7331
+ const targetDir = outputDir
7332
+ ? path.join(outputDir, `${projectId}-${slugify(projectName || `project-${projectId}`)}`)
7333
+ : getProjectDir(projectId, projectName);
7334
+ await this.generateProjectFiles(projectId, apiToken, apiUrl, targetDir, projectName, includeReporter);
7335
+ return targetDir;
7336
+ }
7269
7337
  /**
7270
7338
  * Run all scenarios in a project
7271
7339
  * Generates files, installs dependencies, and executes tests
@@ -7274,7 +7342,7 @@ class TestSuiteRunner {
7274
7342
  static async runProjectScenarios(projectId, apiToken, apiUrl, projectName, options = {}) {
7275
7343
  const { outputDir, includeReporter = false, // CLI mode - no custom reporter
7276
7344
  playwrightArgs = [], onStdout, onStderr, } = options;
7277
- const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
7345
+ const projectDir = outputDir || getProjectDir(projectId, projectName || `project-${projectId}`);
7278
7346
  try {
7279
7347
  // Generate all files
7280
7348
  console.log(`๐Ÿ“ Generating test files for project ${projectId}...`);
@@ -7513,6 +7581,11 @@ class Probo {
7513
7581
  this.timeoutConfig = mergeTimeoutConfigWithEnv({ ...DEFAULT_PLAYWRIGHT_TIMEOUT_CONFIG, ...timeoutConfig });
7514
7582
  // set the log level for the api client
7515
7583
  apiLogger.setLogLevel(this.debugLevel);
7584
+ proboLogger.debug(`Timeout config: playwrightActionTimeout=${this.timeoutConfig.playwrightActionTimeout}ms, ` +
7585
+ `playwrightNavigationTimeout=${this.timeoutConfig.playwrightNavigationTimeout}ms, ` +
7586
+ `playwrightLocatorTimeout=${this.timeoutConfig.playwrightLocatorTimeout}ms, ` +
7587
+ `waitForStabilityQuietTimeout=${this.timeoutConfig.waitForStabilityQuietTimeout}ms, ` +
7588
+ `waitForStabilityGlobalTimeout=${this.timeoutConfig.waitForStabilityGlobalTimeout}ms`);
7516
7589
  proboLogger.info(`Initializing: scenario=${scenarioName}, apiUrl=${apiEndPoint}, ` +
7517
7590
  `enableConsoleLogs=${enableConsoleLogs}, debugLevel=${debugLevel}, aiModel=${aiModel}`);
7518
7591
  }
@@ -7849,5 +7922,5 @@ class Probo {
7849
7922
  }
7850
7923
  }
7851
7924
 
7852
- export { AIModel, Highlighter, NavTracker, OTP, PlaywrightAction, Probo, ProboCodeGenerator, ProboLogLevel, ProboPlaywright, TestSuiteRunner, buildInterpolationContext, createUrlSlug, extractRequiredEnvVars, findClosestVisibleElement, generateCode, getRequiredEnvVars, interpolateWithParams };
7925
+ export { AIModel, Highlighter, NavTracker, OTP, PlaywrightAction, Probo, ProboCodeGenerator, ProboLogLevel, ProboPlaywright, TestSuiteRunner, buildInterpolationContext, createUrlSlug, extractRequiredEnvVars, findClosestVisibleElement, generateCode, getProjectDir, getRequiredEnvVars, interpolateWithParams };
7853
7926
  //# sourceMappingURL=index.js.map