testchimp-runner-core 0.0.87 → 0.0.89

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.
@@ -1,4 +1,4 @@
1
- import { PlaywrightExecutionRequest, PlaywrightExecutionResponse, ScriptExecutionRequest, ScriptExecutionResponse } from './types';
1
+ import { PlaywrightExecutionRequest, PlaywrightExecutionResponse, ScriptExecutionRequest, ScriptExecutionResponse, TestFileExecutionRequest, TestFileExecutionResponse } from './types';
2
2
  import { AuthConfig } from './auth-config';
3
3
  import { LLMProvider } from './llm-provider';
4
4
  import { ProgressReporter } from './progress-reporter';
@@ -84,12 +84,15 @@ export declare class ExecutionService {
84
84
  */
85
85
  private initializeBrowser;
86
86
  /**
87
- * Execute step with command-level tracking
88
- * Parses step code into individual commands and executes them one-by-one
89
- * Maintains execution context so variables defined in earlier commands/steps are accessible to later ones
90
- * Returns successful commands, the failing command, remaining commands, and error
91
- * This allows us to preserve successful commands and continue with remaining ones after repair
87
+ * Execute a test file with hooks support
88
+ * Parses the test file, extracts hooks and tests, executes them in correct order
89
+ * Each test is reported separately with its own jobId
92
90
  */
93
- private executeStepWithTracking;
91
+ runTestFile(request: TestFileExecutionRequest): Promise<TestFileExecutionResponse>;
92
+ /**
93
+ * Execute a hook (beforeAll, afterAll, beforeEach, afterEach)
94
+ * Uses the same execution context as tests
95
+ */
96
+ private executeHook;
94
97
  }
95
98
  //# sourceMappingURL=execution-service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"execution-service.d.ts","sourceRoot":"","sources":["../src/execution-service.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAE3B,sBAAsB,EACtB,uBAAuB,EAKxB,MAAM,SAAS,CAAC;AAKjB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAW3C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AA8ZvD;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,MAAM,CAAC,CAA8D;IAC7E,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,cAAc,CAAkB;IAExC;;OAEG;IACH,aAAa;gBAKX,UAAU,CAAC,EAAE,UAAU,EACvB,UAAU,CAAC,EAAE,MAAM,EACnB,uBAAuB,GAAE,MAAW,EACpC,WAAW,CAAC,EAAE,WAAW,EACzB,gBAAgB,CAAC,EAAE,gBAAgB;IA8BrC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,GAAG,IAAI;IAKpF;;OAEG;IACH,OAAO,CAAC,GAAG;IAOX,OAAO,CAAC,WAAW;IAanB;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;OAGG;IACH,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAQ3C;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAkBtF;;OAEG;YACW,qBAAqB;IAmBnC;;OAEG;IACG,gBAAgB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAgCjG;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAsC7B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;;;OAIG;YACW,+BAA+B;IA2iB7C;;;OAGG;YACW,iBAAiB;YAiGjB,UAAU;YAkLV,eAAe;YAoKf,oBAAoB;YAiBpB,eAAe;IAuC7B,OAAO,CAAC,qBAAqB;IAK7B;;OAEG;YACW,iBAAiB;IAI/B;;;;;;OAMG;YACW,uBAAuB;CAmHtC"}
1
+ {"version":3,"file":"execution-service.d.ts","sourceRoot":"","sources":["../src/execution-service.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,EAE3B,sBAAsB,EACtB,uBAAuB,EAKvB,wBAAwB,EACxB,yBAAyB,EAE1B,MAAM,SAAS,CAAC;AAKjB,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAW3C,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AA+ZvD;;GAEG;AACH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAAc;IACjC,OAAO,CAAC,gBAAgB,CAAC,CAAmB;IAC5C,OAAO,CAAC,kBAAkB,CAAqB;IAC/C,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,gBAAgB,CAAgC;IACxD,OAAO,CAAC,MAAM,CAAC,CAA8D;IAC7E,OAAO,CAAC,iBAAiB,CAAoB;IAC7C,OAAO,CAAC,cAAc,CAAkB;IAExC;;OAEG;IACH,aAAa;gBAKX,UAAU,CAAC,EAAE,UAAU,EACvB,UAAU,CAAC,EAAE,MAAM,EACnB,uBAAuB,GAAE,MAAW,EACpC,WAAW,CAAC,EAAE,WAAW,EACzB,gBAAgB,CAAC,EAAE,gBAAgB;IA8BrC;;OAEG;IACH,SAAS,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,OAAO,GAAG,MAAM,KAAK,IAAI,GAAG,IAAI;IAKpF;;OAEG;IACH,OAAO,CAAC,GAAG;IAOX,OAAO,CAAC,WAAW;IAanB;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAIjC;;;OAGG;IACH,aAAa,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI;IAQ3C;;OAEG;IACG,aAAa,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,uBAAuB,CAAC;IAkBtF;;OAEG;YACW,qBAAqB;IAmBnC;;OAEG;IACG,gBAAgB,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,2BAA2B,CAAC;IAgCjG;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAsC7B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAI5B;;OAEG;IACH,OAAO,IAAI,OAAO;IAIlB;;;;OAIG;YACW,+BAA+B;IA2iB7C;;;OAGG;YACW,iBAAiB;YAiGjB,UAAU;YAkLV,eAAe;YAoKf,oBAAoB;YAiBpB,eAAe;IAuC7B,OAAO,CAAC,qBAAqB;IAK7B;;OAEG;YACW,iBAAiB;IAI/B;;;;OAIG;IACG,WAAW,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,yBAAyB,CAAC;IAgbxF;;;OAGG;YACW,WAAW;CAuB1B"}
@@ -36,6 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ExecutionService = void 0;
37
37
  const vm = __importStar(require("vm"));
38
38
  const path = __importStar(require("path"));
39
+ const crypto = __importStar(require("crypto"));
40
+ const fs = __importStar(require("fs"));
39
41
  const async_hooks_1 = require("async_hooks");
40
42
  const playwright_mcp_service_1 = require("./playwright-mcp-service");
41
43
  const types_1 = require("./types");
@@ -48,11 +50,11 @@ const playwright_runner_1 = require("./utils/playwright-runner");
48
50
  const import_utils_1 = require("./utils/import-utils");
49
51
  const initialization_code_utils_1 = require("./utils/initialization-code-utils");
50
52
  const script_parser_utils_1 = require("./utils/script-parser-utils");
51
- const step_execution_utils_1 = require("./utils/step-execution-utils");
52
53
  const script_generator_utils_1 = require("./utils/script-generator-utils");
53
54
  const backend_proxy_llm_provider_1 = require("./providers/backend-proxy-llm-provider");
54
55
  const build_info_1 = require("./build-info");
55
56
  const orchestrator_1 = require("./orchestrator");
57
+ const test_file_parser_1 = require("./utils/test-file-parser");
56
58
  class ModuleResolutionManager {
57
59
  /**
58
60
  * Install the global hook (only once, shared by all instances)
@@ -880,11 +882,12 @@ class ExecutionService {
880
882
  if (steps && steps.length > 0) {
881
883
  this.log(`Using ${steps.length} pre-parsed steps from consumer`);
882
884
  allStatements = steps.map(step => {
883
- // Detect if this is a variable declaration
884
- const trimmedCode = step.code.trim();
885
- const isVariableDeclaration = trimmedCode.startsWith('const ') ||
886
- trimmedCode.startsWith('let ') ||
887
- trimmedCode.startsWith('var ');
885
+ // Use AST-based detection if provided, otherwise fall back to string-based detection
886
+ const isVariableDeclaration = step.isVariableDeclaration !== undefined
887
+ ? step.isVariableDeclaration
888
+ : (step.code.trim().startsWith('const ') ||
889
+ step.code.trim().startsWith('let ') ||
890
+ step.code.trim().startsWith('var '));
888
891
  return {
889
892
  code: step.code,
890
893
  isVariableDeclaration,
@@ -1471,108 +1474,410 @@ class ExecutionService {
1471
1474
  return (0, browser_utils_1.initializeBrowser)(playwrightConfig, headless, playwrightConfigFilePath, this.logger);
1472
1475
  }
1473
1476
  /**
1474
- * Execute step with command-level tracking
1475
- * Parses step code into individual commands and executes them one-by-one
1476
- * Maintains execution context so variables defined in earlier commands/steps are accessible to later ones
1477
- * Returns successful commands, the failing command, remaining commands, and error
1478
- * This allows us to preserve successful commands and continue with remaining ones after repair
1477
+ * Execute a test file with hooks support
1478
+ * Parses the test file, extracts hooks and tests, executes them in correct order
1479
+ * Each test is reported separately with its own jobId
1479
1480
  */
1480
- async executeStepWithTracking(stepCode, page, priorStepsContext = '') {
1481
- const successfulCommands = [];
1482
- let failingCommand;
1483
- let remainingCommands = [];
1484
- let error;
1485
- // Parse step code into individual statements (handles multi-line await blocks)
1486
- const statements = step_execution_utils_1.StepExecutionUtils.splitIntoExecutableStatements(stepCode);
1487
- // Separate variable declarations and Playwright commands
1488
- const declarations = []; // const/let/var declarations
1489
- const commands = []; // executable statements (await/page/expect/ai/control flow)
1490
- for (const statement of statements) {
1491
- const trimmed = statement.trim();
1492
- if (trimmed.length === 0) {
1493
- continue;
1494
- }
1495
- // Skip comment-only statements
1496
- const withoutComments = trimmed.replace(/^\s*\/\/.*$/gm, '').trim();
1497
- if (withoutComments.length === 0) {
1498
- continue;
1499
- }
1500
- const isDeclaration = /^(const|let|var)\b/.test(trimmed) && !/\bawait\b/.test(trimmed);
1501
- const isExecutable = /\bawait\b/.test(statement) ||
1502
- /\bpage\./.test(statement) ||
1503
- /\bexpect\(/.test(statement) ||
1504
- /\bai\./.test(statement) ||
1505
- /^(if|for|while|switch|try|do)\b/.test(trimmed);
1506
- if (isDeclaration) {
1507
- declarations.push(statement);
1481
+ async runTestFile(request) {
1482
+ const startTime = Date.now();
1483
+ const testResults = [];
1484
+ try {
1485
+ // Read script content if scriptFilePath is provided
1486
+ let script;
1487
+ if (request.script) {
1488
+ script = request.script;
1508
1489
  }
1509
- else if (isExecutable) {
1510
- commands.push(statement);
1490
+ else if (request.scriptFilePath) {
1491
+ try {
1492
+ script = fs.readFileSync(request.scriptFilePath, 'utf8');
1493
+ this.log(`Read test file from: ${request.scriptFilePath}`);
1494
+ }
1495
+ catch (error) {
1496
+ throw new Error(`Failed to read test file: ${error instanceof Error ? error.message : String(error)}`);
1497
+ }
1511
1498
  }
1512
1499
  else {
1513
- // Default to executing the statement to preserve behaviour
1514
- commands.push(statement);
1500
+ throw new Error('Either script or scriptFilePath must be provided');
1515
1501
  }
1516
- }
1517
- if (commands.length === 0 && declarations.length === 0) {
1518
- // No parseable commands - try to execute whole code as-is with prior context
1519
- try {
1520
- const fullCode = priorStepsContext ? priorStepsContext + '\n' + stepCode : stepCode;
1521
- await this.executeStepCode(fullCode, page);
1522
- return { successfulCommands: [stepCode], failingCommand: undefined, remainingCommands: [], error: undefined };
1502
+ // Validate tempDir is provided
1503
+ if (!request.tempDir) {
1504
+ throw new Error('tempDir is required for test file execution');
1523
1505
  }
1524
- catch (err) {
1506
+ // Auto-discover playwright.config.js if not provided
1507
+ let playwrightConfig = request.playwrightConfig;
1508
+ let playwrightConfigFilePath = request.playwrightConfigFilePath;
1509
+ if (!playwrightConfig && !playwrightConfigFilePath) {
1510
+ const configExtensions = ['.js', '.ts', '.mjs'];
1511
+ for (const ext of configExtensions) {
1512
+ const configPath = path.join(request.tempDir, `playwright.config${ext}`);
1513
+ if (fs.existsSync(configPath)) {
1514
+ playwrightConfigFilePath = configPath;
1515
+ this.log(`Auto-discovered Playwright config: ${configPath}`);
1516
+ break;
1517
+ }
1518
+ }
1519
+ }
1520
+ // Extract imports from the original script once (they'll be needed for hooks and tests)
1521
+ // Store the full original script for passing to test executions
1522
+ const originalScript = script;
1523
+ // Parse test file to extract hooks and tests
1524
+ this.log('Parsing test file to extract hooks and tests...');
1525
+ const parsed = test_file_parser_1.TestFileParser.parseTestFile(script, request.testNames);
1526
+ // Flatten structure for execution
1527
+ const flattened = test_file_parser_1.TestFileParser.flattenForExecution(parsed);
1528
+ this.log(`Found ${flattened.fileLevelHooks.beforeAll.length} file-level beforeAll hooks, ${flattened.fileLevelHooks.afterAll.length} file-level afterAll hooks`);
1529
+ this.log(`Found ${flattened.fileLevelHooks.beforeEach.length} file-level beforeEach hooks, ${flattened.fileLevelHooks.afterEach.length} file-level afterEach hooks`);
1530
+ this.log(`Found ${flattened.suites.length} suite(s) with suite-level hooks`);
1531
+ this.log(`Found ${flattened.tests.length} test(s)`);
1532
+ if (flattened.tests.length === 0) {
1533
+ this.log('No tests found in file', 'warn');
1525
1534
  return {
1526
- successfulCommands: [],
1527
- failingCommand: stepCode,
1528
- remainingCommands: [],
1529
- error: step_execution_utils_1.StepExecutionUtils.safeSerializeError(err)
1535
+ success: true,
1536
+ testResults: [],
1537
+ executionTime: Date.now() - startTime
1530
1538
  };
1531
1539
  }
1532
- }
1533
- // Build accumulated context: start with prior steps context, then add current declarations
1534
- const accumulatedContext = [];
1535
- if (priorStepsContext) {
1536
- accumulatedContext.push(priorStepsContext);
1537
- }
1538
- accumulatedContext.push(...declarations);
1539
- successfulCommands.push(...declarations); // Declarations are part of successful commands
1540
- // Execute commands one by one, maintaining variable context only
1541
- // We accumulate executed commands separately to avoid re-executing them
1542
- const executedCommands = [];
1543
- for (let cmdIdx = 0; cmdIdx < commands.length; cmdIdx++) {
1544
- const cmd = commands[cmdIdx];
1545
- try {
1546
- // Execute ONLY this command (not re-executing prior ones)
1547
- // Include accumulated context for variable access only
1548
- const codeToExecute = accumulatedContext.length > 0
1549
- ? [...accumulatedContext, cmd].join('\n')
1550
- : cmd;
1551
- await this.executeStepCode(codeToExecute, page);
1552
- // Success - track the command
1553
- executedCommands.push(cmd);
1554
- successfulCommands.push(cmd);
1555
- // Only add declarations to accumulated context (not action commands)
1556
- if (cmd.startsWith('const ') || cmd.startsWith('let ') || cmd.startsWith('var ')) {
1557
- accumulatedContext.push(cmd);
1540
+ // Initialize browser/context/page (shared across all hooks and tests)
1541
+ const useExistingBrowser = !!(request.existingBrowser && request.existingContext && request.existingPage);
1542
+ let browser;
1543
+ let context;
1544
+ let page;
1545
+ let browserCreated = false;
1546
+ if (useExistingBrowser) {
1547
+ this.log('Using existing browser/page provided by caller');
1548
+ browser = request.existingBrowser;
1549
+ context = request.existingContext;
1550
+ page = request.existingPage;
1551
+ }
1552
+ else {
1553
+ this.log('Initializing browser for test file execution...');
1554
+ const browserInstance = await this.initializeBrowser(playwrightConfig, request.headless !== false, playwrightConfigFilePath);
1555
+ browser = browserInstance.browser;
1556
+ context = browserInstance.context;
1557
+ page = browserInstance.page;
1558
+ browserCreated = true;
1559
+ }
1560
+ // Apply file upload overrides if tempDir is provided
1561
+ if (request.tempDir) {
1562
+ applyFileUploadOverrides(page, request.tempDir, request.testFolderPath, (msg, level) => this.log(msg, level));
1563
+ this.log(`Applied file upload overrides with tempDir: ${request.tempDir}`);
1564
+ }
1565
+ // Execute file-level beforeAll hooks
1566
+ if (flattened.fileLevelHooks.beforeAll.length > 0) {
1567
+ this.log(`Executing ${flattened.fileLevelHooks.beforeAll.length} file-level beforeAll hook(s)...`);
1568
+ try {
1569
+ for (const hook of flattened.fileLevelHooks.beforeAll) {
1570
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1571
+ }
1572
+ this.log('File-level beforeAll hooks executed successfully');
1573
+ }
1574
+ catch (error) {
1575
+ const errorMsg = error instanceof Error ? error.message : String(error);
1576
+ this.log(`File-level beforeAll hook failed: ${errorMsg}`, 'error');
1577
+ // Cleanup browser if we created it
1578
+ if (browserCreated && browser) {
1579
+ try {
1580
+ await browser.close();
1581
+ }
1582
+ catch (closeError) {
1583
+ // Ignore close errors
1584
+ }
1585
+ }
1586
+ return {
1587
+ success: false,
1588
+ testResults: [],
1589
+ executionTime: Date.now() - startTime,
1590
+ error: `File-level beforeAll hook failed: ${errorMsg}`
1591
+ };
1558
1592
  }
1559
- this.log(` ✓ Command succeeded: ${cmd.substring(0, 60)}...`);
1560
1593
  }
1561
- catch (err) {
1562
- // This command failed - it's the failing one
1563
- failingCommand = cmd;
1564
- error = step_execution_utils_1.StepExecutionUtils.safeSerializeError(err);
1565
- // Capture remaining commands that were not executed
1566
- remainingCommands = commands.slice(cmdIdx + 1);
1567
- this.log(` ✗ Command failed: ${cmd.substring(0, 60)}...`);
1568
- this.log(` Error: ${error}`);
1569
- if (remainingCommands.length > 0) {
1570
- this.log(` ⏭ ${remainingCommands.length} remaining commands not executed`);
1594
+ // Helper function to create a unique key from suite path (avoids collisions if suite names contain '__')
1595
+ const getSuiteKey = (suitePath) => {
1596
+ // Use JSON.stringify to safely serialize the array, avoiding collisions
1597
+ return JSON.stringify(suitePath);
1598
+ };
1599
+ // Build a Map of suites by key for O(1) lookups (instead of O(n) .find() calls)
1600
+ const suiteMap = new Map();
1601
+ for (const suite of flattened.suites) {
1602
+ const suiteKey = getSuiteKey(suite.suitePath);
1603
+ suiteMap.set(suiteKey, suite);
1604
+ }
1605
+ // Track suite execution state (which suites have had beforeAll executed)
1606
+ const suiteBeforeAllExecuted = new Set();
1607
+ // Track test counts per suite (for determining last test in suite)
1608
+ const suiteTestCounts = new Map();
1609
+ const suiteTotalTests = new Map();
1610
+ // Initialize suite test counts
1611
+ for (const suite of flattened.suites) {
1612
+ const suiteKey = getSuiteKey(suite.suitePath);
1613
+ suiteTotalTests.set(suiteKey, suite.testIndices.length);
1614
+ suiteTestCounts.set(suiteKey, 0);
1615
+ }
1616
+ // Execute each test
1617
+ for (let testIndex = 0; testIndex < flattened.tests.length; testIndex++) {
1618
+ const testData = flattened.tests[testIndex];
1619
+ const test = testData.test;
1620
+ const testStartTime = Date.now();
1621
+ const jobId = crypto.randomUUID();
1622
+ let testStatus = 'passed';
1623
+ let testError;
1624
+ try {
1625
+ // Before test execution: Execute suite-level beforeAll hooks (if first test in suite)
1626
+ if (testData.suitePath.length > 0) {
1627
+ // Execute beforeAll for each suite in the path (in order: parent → child)
1628
+ for (let i = 0; i < testData.suitePath.length; i++) {
1629
+ const suitePath = testData.suitePath.slice(0, i + 1);
1630
+ const suiteKey = getSuiteKey(suitePath);
1631
+ // Look up suite in Map (O(1) instead of O(n) .find())
1632
+ const suite = suiteMap.get(suiteKey);
1633
+ if (suite && suite.beforeAll.length > 0 && !suiteBeforeAllExecuted.has(suiteKey)) {
1634
+ // Use human-readable suite name for logging
1635
+ const suiteDisplayName = suitePath.join('__');
1636
+ this.log(`Executing suite-level beforeAll hooks for suite: ${suiteDisplayName}`);
1637
+ try {
1638
+ for (const hook of suite.beforeAll) {
1639
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1640
+ }
1641
+ suiteBeforeAllExecuted.add(suiteKey);
1642
+ this.log(`Suite-level beforeAll hooks executed for suite: ${suiteDisplayName}`);
1643
+ }
1644
+ catch (error) {
1645
+ const errorMsg = error instanceof Error ? error.message : String(error);
1646
+ this.log(`Suite-level beforeAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'error');
1647
+ testStatus = 'failed';
1648
+ testError = `Suite-level beforeAll hook failed: ${errorMsg}`;
1649
+ }
1650
+ }
1651
+ }
1652
+ }
1653
+ // Call onStartTest callback (use fullName)
1654
+ if (this.progressReporter?.onStartTest) {
1655
+ await this.progressReporter.onStartTest(test.fullName, jobId, page, browser, context);
1656
+ }
1657
+ // Execute file-level beforeEach hooks
1658
+ if (flattened.fileLevelHooks.beforeEach.length > 0 && testStatus === 'passed') {
1659
+ this.log(`Executing ${flattened.fileLevelHooks.beforeEach.length} file-level beforeEach hook(s) for test: ${test.fullName}`);
1660
+ try {
1661
+ for (const hook of flattened.fileLevelHooks.beforeEach) {
1662
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1663
+ }
1664
+ }
1665
+ catch (error) {
1666
+ const errorMsg = error instanceof Error ? error.message : String(error);
1667
+ this.log(`File-level beforeEach hook failed for test ${test.fullName}: ${errorMsg}`, 'error');
1668
+ testStatus = 'failed';
1669
+ testError = `File-level beforeEach hook failed: ${errorMsg}`;
1670
+ }
1671
+ }
1672
+ // Execute suite-level beforeEach hooks (from all parent suites, in order: parent → child)
1673
+ if (testData.suiteBeforeEachHooks.length > 0 && testStatus === 'passed') {
1674
+ this.log(`Executing ${testData.suiteBeforeEachHooks.length} suite-level beforeEach hook(s) for test: ${test.fullName}`);
1675
+ try {
1676
+ for (const hook of testData.suiteBeforeEachHooks) {
1677
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1678
+ }
1679
+ }
1680
+ catch (error) {
1681
+ const errorMsg = error instanceof Error ? error.message : String(error);
1682
+ this.log(`Suite-level beforeEach hook failed for test ${test.fullName}: ${errorMsg}`, 'error');
1683
+ testStatus = 'failed';
1684
+ testError = `Suite-level beforeEach hook failed: ${errorMsg}`;
1685
+ }
1686
+ }
1687
+ // Run the test if hooks didn't fail
1688
+ if (testStatus === 'passed') {
1689
+ try {
1690
+ // Construct a script with imports + this test (needed for module resolution)
1691
+ const testScript = test_file_parser_1.TestFileParser.constructTestScriptWithImports(originalScript, test.fullName, test.code);
1692
+ // Use pre-parsed statements from initial file parse
1693
+ // These were extracted directly from the original AST, ensuring accuracy
1694
+ this.log(`Using ${test.statements.length} pre-parsed statements for test: ${test.fullName}`);
1695
+ const steps = test.statements.map((stmt, index) => ({
1696
+ id: `step-${index}`,
1697
+ code: stmt.code,
1698
+ description: stmt.intentComment || stmt.code.trim().substring(0, 100) || '',
1699
+ isVariableDeclaration: stmt.isVariableDeclaration // Pass AST-based detection flag
1700
+ }));
1701
+ // Execute using the existing step-wise execution infrastructure
1702
+ // executeStepsInPersistentContext will:
1703
+ // 1. Execute imports from testScript (module resolution)
1704
+ // 2. Execute ALL statements sequentially (variables + actions)
1705
+ // 3. Variables execute silently and persist in context
1706
+ // 4. Non-variable statements trigger callbacks (onStepProgress)
1707
+ const result = await this.executeStepsInPersistentContext(steps, // Pre-parsed steps from initial parse with AST-based flags
1708
+ page, browser, context, {
1709
+ mode: request.mode || types_1.ExecutionMode.RUN_EXACTLY,
1710
+ jobId: jobId,
1711
+ tempDir: request.tempDir,
1712
+ originalScript: testScript, // For module resolution (imports)
1713
+ model: request.model
1714
+ });
1715
+ if (!result.success) {
1716
+ testStatus = 'failed';
1717
+ testError = result.error || 'Test execution failed';
1718
+ }
1719
+ }
1720
+ catch (error) {
1721
+ testStatus = 'failed';
1722
+ testError = error instanceof Error ? error.message : String(error);
1723
+ }
1724
+ }
1725
+ // Execute suite-level afterEach hooks (from all parent suites, in reverse order: child → parent)
1726
+ if (testData.suiteAfterEachHooks.length > 0) {
1727
+ this.log(`Executing ${testData.suiteAfterEachHooks.length} suite-level afterEach hook(s) for test: ${test.fullName}`);
1728
+ try {
1729
+ for (const hook of testData.suiteAfterEachHooks) {
1730
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1731
+ }
1732
+ }
1733
+ catch (error) {
1734
+ const errorMsg = error instanceof Error ? error.message : String(error);
1735
+ this.log(`Suite-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
1736
+ // Log but continue - don't fail the test because of afterEach failure
1737
+ }
1738
+ }
1739
+ // Execute file-level afterEach hooks (even if test failed)
1740
+ if (flattened.fileLevelHooks.afterEach.length > 0) {
1741
+ this.log(`Executing ${flattened.fileLevelHooks.afterEach.length} file-level afterEach hook(s) for test: ${test.fullName}`);
1742
+ try {
1743
+ for (const hook of flattened.fileLevelHooks.afterEach) {
1744
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1745
+ }
1746
+ }
1747
+ catch (error) {
1748
+ const errorMsg = error instanceof Error ? error.message : String(error);
1749
+ this.log(`File-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
1750
+ // Log but continue - don't fail the test because of afterEach failure
1751
+ }
1752
+ }
1753
+ // After test execution: Increment test counts and execute suite-level afterAll hooks (if last test in suite)
1754
+ if (testData.suitePath.length > 0) {
1755
+ // Increment test count for each suite in the path
1756
+ for (let i = 0; i < testData.suitePath.length; i++) {
1757
+ const suitePath = testData.suitePath.slice(0, i + 1);
1758
+ const suiteKey = getSuiteKey(suitePath);
1759
+ const currentCount = suiteTestCounts.get(suiteKey) || 0;
1760
+ suiteTestCounts.set(suiteKey, currentCount + 1);
1761
+ }
1762
+ // Execute afterAll for each suite in the path (in reverse order: child → parent)
1763
+ for (let i = testData.suitePath.length - 1; i >= 0; i--) {
1764
+ const suitePath = testData.suitePath.slice(0, i + 1);
1765
+ const suiteKey = getSuiteKey(suitePath);
1766
+ // Look up suite in Map (O(1) instead of O(n) .find())
1767
+ const suite = suiteMap.get(suiteKey);
1768
+ if (suite && suite.afterAll.length > 0) {
1769
+ const totalTests = suiteTotalTests.get(suiteKey) || 0;
1770
+ const currentCount = suiteTestCounts.get(suiteKey) || 0;
1771
+ // If this is the last test in the suite, execute afterAll
1772
+ if (currentCount >= totalTests) {
1773
+ // Use human-readable suite name for logging
1774
+ const suiteDisplayName = suitePath.join('__');
1775
+ this.log(`Executing suite-level afterAll hooks for suite: ${suiteDisplayName}`);
1776
+ try {
1777
+ for (const hook of suite.afterAll) {
1778
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1779
+ }
1780
+ this.log(`Suite-level afterAll hooks executed for suite: ${suiteDisplayName}`);
1781
+ }
1782
+ catch (error) {
1783
+ const errorMsg = error instanceof Error ? error.message : String(error);
1784
+ this.log(`Suite-level afterAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'warn');
1785
+ // Log but continue - don't fail the test because of afterAll failure
1786
+ }
1787
+ }
1788
+ }
1789
+ }
1790
+ }
1791
+ // Call onEndTest callback (use fullName)
1792
+ if (this.progressReporter?.onEndTest) {
1793
+ await this.progressReporter.onEndTest(test.fullName, jobId, testStatus, testError, page, browser, context);
1794
+ }
1795
+ }
1796
+ catch (error) {
1797
+ // Unexpected error during test execution
1798
+ testStatus = 'failed';
1799
+ testError = error instanceof Error ? error.message : String(error);
1800
+ this.log(`Unexpected error during test ${test.name}: ${testError}`, 'error');
1801
+ // Still call onEndTest if possible
1802
+ if (this.progressReporter?.onEndTest) {
1803
+ try {
1804
+ await this.progressReporter.onEndTest(test.fullName, jobId, testStatus, testError, page, browser, context);
1805
+ }
1806
+ catch (callbackError) {
1807
+ this.log(`onEndTest callback failed: ${callbackError}`, 'warn');
1808
+ }
1809
+ }
1810
+ }
1811
+ // Record test result (use fullName)
1812
+ testResults.push({
1813
+ testName: test.fullName,
1814
+ jobId: jobId,
1815
+ status: testStatus,
1816
+ error: testError,
1817
+ executionTime: Date.now() - testStartTime
1818
+ });
1819
+ this.log(`Test ${test.fullName} completed with status: ${testStatus}`);
1820
+ }
1821
+ // Execute file-level afterAll hooks
1822
+ if (flattened.fileLevelHooks.afterAll.length > 0) {
1823
+ this.log(`Executing ${flattened.fileLevelHooks.afterAll.length} file-level afterAll hook(s)...`);
1824
+ try {
1825
+ for (const hook of flattened.fileLevelHooks.afterAll) {
1826
+ await this.executeHook(hook.code, page, context, browser, request.tempDir);
1827
+ }
1828
+ this.log('File-level afterAll hooks executed successfully');
1829
+ }
1830
+ catch (error) {
1831
+ const errorMsg = error instanceof Error ? error.message : String(error);
1832
+ this.log(`File-level afterAll hook failed: ${errorMsg}`, 'warn');
1833
+ // Log but don't fail overall execution
1571
1834
  }
1572
- break; // Stop at first failure
1573
1835
  }
1836
+ // Cleanup browser if we created it
1837
+ if (browserCreated && browser) {
1838
+ try {
1839
+ await browser.close();
1840
+ this.log('Browser closed');
1841
+ }
1842
+ catch (closeError) {
1843
+ this.log(`Error closing browser: ${closeError}`, 'warn');
1844
+ }
1845
+ }
1846
+ // Determine overall success (all tests passed)
1847
+ const allPassed = testResults.every(result => result.status === 'passed');
1848
+ return {
1849
+ success: allPassed,
1850
+ testResults: testResults,
1851
+ executionTime: Date.now() - startTime
1852
+ };
1853
+ }
1854
+ catch (error) {
1855
+ const errorMsg = error instanceof Error ? error.message : String(error);
1856
+ this.log(`Test file execution failed: ${errorMsg}`, 'error');
1857
+ return {
1858
+ success: false,
1859
+ testResults: testResults,
1860
+ executionTime: Date.now() - startTime,
1861
+ error: errorMsg
1862
+ };
1863
+ }
1864
+ }
1865
+ /**
1866
+ * Execute a hook (beforeAll, afterAll, beforeEach, afterEach)
1867
+ * Uses the same execution context as tests
1868
+ */
1869
+ async executeHook(hookCode, page, context, browser, tempDir) {
1870
+ const { expect, test } = require('@playwright/test');
1871
+ const { ai } = require('ai-wright');
1872
+ // Create execution context for the hook
1873
+ const hookContext = new PersistentExecutionContext(page, expect, test, ai, browser, context, tempDir);
1874
+ try {
1875
+ // Execute hook code
1876
+ await hookContext.executeCode(hookCode);
1877
+ }
1878
+ finally {
1879
+ hookContext.dispose();
1574
1880
  }
1575
- return { successfulCommands, failingCommand, remainingCommands, error };
1576
1881
  }
1577
1882
  }
1578
1883
  exports.ExecutionService = ExecutionService;