@probolabs/playwright 1.5.0-rc.8 โ†’ 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
  }
@@ -6111,12 +6128,12 @@ class ProboCodeGenerator {
6111
6128
  return response.json();
6112
6129
  }
6113
6130
  /**
6114
- * 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)
6115
6132
  */
6116
6133
  static async fetchProjectScenarios(projectId, apiToken, apiUrl) {
6117
6134
  const normalizedUrl = this.normalizeApiUrl(apiUrl);
6118
- // Scenarios endpoint is at root level, not under /api/
6119
- 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}`;
6120
6137
  const response = await fetch(url, {
6121
6138
  method: 'GET',
6122
6139
  headers: {
@@ -6254,6 +6271,7 @@ class ProboCodeGenerator {
6254
6271
  scenarioName: scenarioData.name,
6255
6272
  interactions: interactions,
6256
6273
  rows: rows,
6274
+ ...((options === null || options === void 0 ? void 0 : options.testSuiteTags) && options.testSuiteTags.length > 0 ? { testSuiteTags: options.testSuiteTags } : {}),
6257
6275
  };
6258
6276
  return generateCode(codeOptions, settings, viewPort);
6259
6277
  }
@@ -6313,14 +6331,15 @@ class ProboCodeGenerator {
6313
6331
  if (scenarios.length === 0) {
6314
6332
  throw new Error(`No scenarios found in project "${projectData.name}" (ID: ${projectId})`);
6315
6333
  }
6316
- // Generate code for each scenario
6334
+ // Generate code for each scenario (with test_suite_tags for Playwright tagging)
6317
6335
  const scenarioResults = await Promise.all(scenarios.map(async (scenario) => {
6318
6336
  try {
6319
- 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 });
6320
6338
  return {
6321
6339
  scenarioId: scenario.id,
6322
6340
  scenarioName: scenario.name,
6323
6341
  code: code,
6342
+ testSuiteTags: scenario.test_suite_tags,
6324
6343
  };
6325
6344
  }
6326
6345
  catch (error) {
@@ -6347,15 +6366,13 @@ function ensureDirectoryExists(dir) {
6347
6366
  }
6348
6367
  }
6349
6368
  /**
6350
- * 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}/
6351
6371
  */
6352
- function getDefaultTestSuiteDir(testSuiteId, testSuiteName) {
6353
- const baseDir = path.join(os.homedir(), '.probium', 'test-suites');
6354
- if (testSuiteName) {
6355
- const sanitizedName = slugify(testSuiteName);
6356
- return path.join(baseDir, `${testSuiteId}-${sanitizedName}`);
6357
- }
6358
- 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}`);
6359
6376
  }
6360
6377
  /**
6361
6378
  * Count total tests by scanning spec files
@@ -6550,36 +6567,67 @@ class TestSuiteRunner {
6550
6567
  throw new Error(`Test suite "${testSuiteName}" not found in project "${projectName}"`);
6551
6568
  }
6552
6569
  /**
6553
- * 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)
6554
6597
  */
6555
6598
  static async generateTestSuiteFiles(testSuiteId, apiToken, apiUrl, outputDir, testSuiteName, includeReporter = true, runId) {
6556
- const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
6557
- // 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);
6558
6610
  const codeGenResult = await ProboCodeGenerator.generateCodeForTestSuite(testSuiteId, apiToken, apiUrl);
6559
6611
  if (codeGenResult.scenarios.length === 0) {
6560
- 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.`);
6561
6613
  }
6562
- // Delete everything in the test suite directory except node_modules
6563
- // This ensures a clean state while preserving dependencies for faster subsequent runs
6564
- if (fs.existsSync(testSuiteDir)) {
6614
+ const projectDir = outputDir;
6615
+ if (fs.existsSync(projectDir)) {
6565
6616
  try {
6566
- const nodeModulesPath = path.join(testSuiteDir, 'node_modules');
6617
+ const nodeModulesPath = path.join(projectDir, 'node_modules');
6567
6618
  const hasNodeModules = fs.existsSync(nodeModulesPath);
6568
- // Temporarily move node_modules out of the way if it exists
6569
6619
  let tempNodeModulesPath = null;
6570
6620
  if (hasNodeModules) {
6571
- tempNodeModulesPath = path.join(testSuiteDir, '..', `node_modules.temp.${testSuiteId}`);
6572
- // Remove temp directory if it exists from a previous failed run
6621
+ tempNodeModulesPath = path.join(projectDir, '..', `node_modules.temp.${projectId}`);
6573
6622
  if (fs.existsSync(tempNodeModulesPath)) {
6574
6623
  fs.rmSync(tempNodeModulesPath, { recursive: true, force: true });
6575
6624
  }
6576
6625
  fs.renameSync(nodeModulesPath, tempNodeModulesPath);
6577
6626
  console.log(`๐Ÿ“ฆ Preserved node_modules temporarily`);
6578
6627
  }
6579
- // Delete everything in the directory
6580
- const entries = fs.readdirSync(testSuiteDir, { withFileTypes: true });
6628
+ const entries = fs.readdirSync(projectDir, { withFileTypes: true });
6581
6629
  for (const entry of entries) {
6582
- const entryPath = path.join(testSuiteDir, entry.name);
6630
+ const entryPath = path.join(projectDir, entry.name);
6583
6631
  try {
6584
6632
  if (entry.isDirectory()) {
6585
6633
  fs.rmSync(entryPath, { recursive: true, force: true });
@@ -6592,36 +6640,33 @@ class TestSuiteRunner {
6592
6640
  console.warn(`โš ๏ธ Failed to delete ${entry.name}:`, error);
6593
6641
  }
6594
6642
  }
6595
- console.log(`๐Ÿ—‘๏ธ Cleaned test suite directory (preserved node_modules)`);
6596
- // Move node_modules back
6643
+ console.log(`๐Ÿ—‘๏ธ Cleaned project directory (preserved node_modules)`);
6597
6644
  if (hasNodeModules && tempNodeModulesPath) {
6598
6645
  fs.renameSync(tempNodeModulesPath, nodeModulesPath);
6599
6646
  console.log(`๐Ÿ“ฆ Restored node_modules`);
6600
6647
  }
6601
6648
  }
6602
6649
  catch (error) {
6603
- console.warn(`โš ๏ธ Failed to clean test suite directory ${testSuiteDir}:`, error);
6604
- // Continue anyway - we'll overwrite files as needed
6650
+ console.warn(`โš ๏ธ Failed to clean project directory ${projectDir}:`, error);
6605
6651
  }
6606
6652
  }
6607
- // Ensure directories exist (will recreate after deletion)
6608
- ensureDirectoryExists(testSuiteDir);
6609
- const testsDir = path.join(testSuiteDir, 'tests');
6653
+ ensureDirectoryExists(projectDir);
6654
+ const testsDir = path.join(projectDir, 'tests');
6610
6655
  ensureDirectoryExists(testsDir);
6611
- // Save each scenario's code to a .spec.ts file
6656
+ const seenSlugs = new Map();
6612
6657
  for (const scenario of codeGenResult.scenarios) {
6613
- 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`;
6614
6662
  const filePath = path.join(testsDir, fileName);
6615
6663
  fs.writeFileSync(filePath, scenario.code, 'utf-8');
6616
6664
  console.log(`โœ… Generated test file: ${filePath}`);
6617
6665
  }
6618
- // Generate package.json
6619
- await this.generatePackageJson(testSuiteDir, codeGenResult);
6620
- // Generate playwright.config.ts with runId if available
6621
- await this.generatePlaywrightConfig(testSuiteDir, includeReporter, runId);
6622
- // Generate custom reporter file for live progress streaming (only if requested)
6666
+ await this.generatePackageJson(projectDir, codeGenResult);
6667
+ await this.generatePlaywrightConfig(projectDir, includeReporter, runId);
6623
6668
  if (includeReporter) {
6624
- await this.generateProboReporter(testSuiteDir);
6669
+ await this.generateProboReporter(projectDir);
6625
6670
  }
6626
6671
  }
6627
6672
  /**
@@ -6665,7 +6710,8 @@ class TestSuiteRunner {
6665
6710
  const packageJsonContent = generatePackageJson({
6666
6711
  name: sanitizedName,
6667
6712
  hasSecrets: hasSecrets,
6668
- testScript: 'npx playwright test'
6713
+ testScript: 'npx playwright test',
6714
+ probolabsPlaywrightVersion: process.env.PROBO_PLAYWRIGHT_VERSION || 'latest',
6669
6715
  });
6670
6716
  const filePath = path.join(outputDir, 'package.json');
6671
6717
  fs.writeFileSync(filePath, packageJsonContent, 'utf-8');
@@ -6695,7 +6741,8 @@ class TestSuiteRunner {
6695
6741
  */
6696
6742
  static async runTestSuite(testSuiteId, apiToken, apiUrl, testSuiteName, runId, options = {}) {
6697
6743
  const { outputDir, includeReporter = true, playwrightArgs = [], skipRunCreation = false, onStatusUpdate, onStdout, onStderr, onReporterEvent, } = options;
6698
- const testSuiteDir = outputDir || getDefaultTestSuiteDir(testSuiteId, testSuiteName);
6744
+ const details = await this.fetchTestSuiteDetails(testSuiteId, apiToken, apiUrl);
6745
+ const projectDir = outputDir || getProjectDir(details.projectId, details.projectName);
6699
6746
  let currentRunId = runId;
6700
6747
  try {
6701
6748
  // Create run record if not provided and not skipping run creation
@@ -6723,9 +6770,9 @@ class TestSuiteRunner {
6723
6770
  }
6724
6771
  // Generate all files (with runId if available for run-specific report directories)
6725
6772
  console.log(`๐Ÿ“ Generating test suite files for test suite ${testSuiteId}...`);
6726
- await this.generateTestSuiteFiles(testSuiteId, apiToken, apiUrl, testSuiteDir, testSuiteName, includeReporter, currentRunId);
6773
+ await this.generateTestSuiteFiles(testSuiteId, apiToken, apiUrl, projectDir, testSuiteName, includeReporter, currentRunId);
6727
6774
  // Count total tests
6728
- const testsTotal = countTotalTests(testSuiteDir);
6775
+ const testsTotal = countTotalTests(projectDir);
6729
6776
  if (currentRunId && testsTotal > 0) {
6730
6777
  await updateRunStatus(currentRunId, testSuiteId, { tests_total: testsTotal }, apiToken, apiUrl);
6731
6778
  if (onStatusUpdate) {
@@ -6733,10 +6780,10 @@ class TestSuiteRunner {
6733
6780
  }
6734
6781
  }
6735
6782
  // Install dependencies
6736
- console.log(`๐Ÿ“ฆ Installing dependencies in ${testSuiteDir}...`);
6783
+ console.log(`๐Ÿ“ฆ Installing dependencies in ${projectDir}...`);
6737
6784
  try {
6738
6785
  const npmExecutable = findNpmExecutable();
6739
- 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
6740
6787
  );
6741
6788
  console.log('โœ… Dependencies installed successfully');
6742
6789
  if (installStdout)
@@ -6776,7 +6823,7 @@ class TestSuiteRunner {
6776
6823
  console.log(`๐ŸŒ Installing Playwright Chromium browser...`);
6777
6824
  try {
6778
6825
  const npxExecutable = findNpxExecutable();
6779
- 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
6780
6827
  );
6781
6828
  console.log('โœ… Playwright Chromium browser installed successfully');
6782
6829
  if (browserStdout)
@@ -6790,7 +6837,7 @@ class TestSuiteRunner {
6790
6837
  // or the error will be caught when trying to run tests
6791
6838
  }
6792
6839
  // Run Playwright tests with streaming output
6793
- console.log(`๐Ÿš€ Running Playwright tests in ${testSuiteDir}...`);
6840
+ console.log(`๐Ÿš€ Running Playwright tests in ${projectDir}...`);
6794
6841
  if (playwrightArgs.length > 0) {
6795
6842
  console.log(`๐Ÿงช Playwright extra args: ${playwrightArgs.join(' ')}`);
6796
6843
  }
@@ -6812,7 +6859,7 @@ class TestSuiteRunner {
6812
6859
  // Use spawn for streaming output
6813
6860
  const npxExecutable = findNpxExecutable();
6814
6861
  const testProcess = spawn(npxExecutable, ['playwright', 'test', ...playwrightArgs], {
6815
- cwd: testSuiteDir,
6862
+ cwd: projectDir,
6816
6863
  shell: true,
6817
6864
  stdio: ['ignore', 'pipe', 'pipe'],
6818
6865
  env: {
@@ -6998,8 +7045,8 @@ class TestSuiteRunner {
6998
7045
  console.log(success ? 'โœ… Tests completed successfully' : (isTestFailure ? 'โš ๏ธ Tests completed with failures' : 'โŒ Test execution failed'));
6999
7046
  // Parse final statistics from metadata/stdout as fallback
7000
7047
  let parsedStats = null;
7001
- if (fs.existsSync(path.join(testSuiteDir, 'playwright-report'))) {
7002
- parsedStats = parseStatisticsFromMetadata(testSuiteDir);
7048
+ if (fs.existsSync(path.join(projectDir, 'playwright-report'))) {
7049
+ parsedStats = parseStatisticsFromMetadata(projectDir);
7003
7050
  }
7004
7051
  if (!parsedStats) {
7005
7052
  parsedStats = parsePlaywrightStatistics(stdout);
@@ -7201,7 +7248,8 @@ class TestSuiteRunner {
7201
7248
  * Generate all files for a project (all scenarios)
7202
7249
  */
7203
7250
  static async generateProjectFiles(projectId, apiToken, apiUrl, outputDir, projectName, includeReporter = true) {
7204
- const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
7251
+ var _a;
7252
+ const projectDir = outputDir || getProjectDir(projectId, projectName || `project-${projectId}`);
7205
7253
  // Generate code for all scenarios
7206
7254
  const codeGenResult = await ProboCodeGenerator.generateCodeForProject(projectId, apiToken, apiUrl);
7207
7255
  if (codeGenResult.scenarios.length === 0) {
@@ -7255,9 +7303,12 @@ class TestSuiteRunner {
7255
7303
  ensureDirectoryExists(projectDir);
7256
7304
  const testsDir = path.join(projectDir, 'tests');
7257
7305
  ensureDirectoryExists(testsDir);
7258
- // Save each scenario's code to a .spec.ts file
7306
+ const seenSlugs = new Map();
7259
7307
  for (const scenario of codeGenResult.scenarios) {
7260
- 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`;
7261
7312
  const filePath = path.join(testsDir, fileName);
7262
7313
  fs.writeFileSync(filePath, scenario.code, 'utf-8');
7263
7314
  console.log(`โœ… Generated test file: ${filePath}`);
@@ -7271,6 +7322,18 @@ class TestSuiteRunner {
7271
7322
  await this.generateProboReporter(projectDir);
7272
7323
  }
7273
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
+ }
7274
7337
  /**
7275
7338
  * Run all scenarios in a project
7276
7339
  * Generates files, installs dependencies, and executes tests
@@ -7279,7 +7342,7 @@ class TestSuiteRunner {
7279
7342
  static async runProjectScenarios(projectId, apiToken, apiUrl, projectName, options = {}) {
7280
7343
  const { outputDir, includeReporter = false, // CLI mode - no custom reporter
7281
7344
  playwrightArgs = [], onStdout, onStderr, } = options;
7282
- const projectDir = outputDir || getDefaultTestSuiteDir(projectId, projectName || `project-${projectId}`);
7345
+ const projectDir = outputDir || getProjectDir(projectId, projectName || `project-${projectId}`);
7283
7346
  try {
7284
7347
  // Generate all files
7285
7348
  console.log(`๐Ÿ“ Generating test files for project ${projectId}...`);
@@ -7859,5 +7922,5 @@ class Probo {
7859
7922
  }
7860
7923
  }
7861
7924
 
7862
- 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 };
7863
7926
  //# sourceMappingURL=index.js.map