testchimp-runner-core 0.0.86 → 0.0.88
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/execution-service.d.ts +10 -7
- package/dist/execution-service.d.ts.map +1 -1
- package/dist/execution-service.js +399 -100
- package/dist/execution-service.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -1
- package/dist/index.js.map +1 -1
- package/dist/progress-reporter.d.ts +28 -0
- package/dist/progress-reporter.d.ts.map +1 -1
- package/dist/types.d.ts +41 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +0 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/test-file-parser.d.ts +94 -0
- package/dist/utils/test-file-parser.d.ts.map +1 -0
- package/dist/utils/test-file-parser.js +604 -0
- package/dist/utils/test-file-parser.js.map +1 -0
- package/package.json +1 -1
|
@@ -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
|
|
88
|
-
* Parses
|
|
89
|
-
*
|
|
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
|
-
|
|
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":"
|
|
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;IAgaxF;;;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)
|
|
@@ -889,7 +891,8 @@ class ExecutionService {
|
|
|
889
891
|
code: step.code,
|
|
890
892
|
isVariableDeclaration,
|
|
891
893
|
intentComment: step.description !== step.code ? step.description : undefined,
|
|
892
|
-
screenStateAnnotation: undefined // Will be in description for pre-parsed steps
|
|
894
|
+
screenStateAnnotation: undefined, // Will be in description for pre-parsed steps
|
|
895
|
+
stepId: step.id // Preserve block index ID (e.g., "0", "1", "2")
|
|
893
896
|
};
|
|
894
897
|
});
|
|
895
898
|
}
|
|
@@ -934,8 +937,9 @@ class ExecutionService {
|
|
|
934
937
|
// LIFECYCLE: beforeStepStart (call before execution)
|
|
935
938
|
if (this.progressReporter?.beforeStepStart && options.jobId) {
|
|
936
939
|
const description = stmt.intentComment || ''; // Don't use code as fallback
|
|
940
|
+
const stepId = stmt.stepId || `step-${stepNumber}`; // Use preserved ID, fallback to sequential
|
|
937
941
|
await this.progressReporter.beforeStepStart({
|
|
938
|
-
stepId
|
|
942
|
+
stepId,
|
|
939
943
|
stepNumber,
|
|
940
944
|
description,
|
|
941
945
|
code: stmt.code
|
|
@@ -948,13 +952,14 @@ class ExecutionService {
|
|
|
948
952
|
// Only report non-variable statements as steps
|
|
949
953
|
if (!isVariable) {
|
|
950
954
|
const description = stmt.intentComment || ''; // Don't use code as fallback
|
|
955
|
+
const stepId = stmt.stepId || `step-${stepNumber}`; // Use preserved ID, fallback to sequential
|
|
951
956
|
this.log(` ✓ Step ${stepNumber} succeeded: ${description || stmt.code}`);
|
|
952
957
|
executedStepDescriptions.push(description);
|
|
953
958
|
// LIFECYCLE: onStepProgress (success)
|
|
954
959
|
if (this.progressReporter?.onStepProgress && options.jobId) {
|
|
955
960
|
await this.progressReporter.onStepProgress({
|
|
956
961
|
jobId: options.jobId,
|
|
957
|
-
stepId
|
|
962
|
+
stepId,
|
|
958
963
|
stepNumber,
|
|
959
964
|
description,
|
|
960
965
|
code: stmt.code,
|
|
@@ -974,12 +979,13 @@ class ExecutionService {
|
|
|
974
979
|
if (!isVariable) {
|
|
975
980
|
// stepNumber already incremented in beforeStepStart
|
|
976
981
|
const description = stmt.intentComment || ''; // Don't use code as fallback
|
|
982
|
+
const stepId = stmt.stepId || `step-${stepNumber}`; // Use preserved ID, fallback to sequential
|
|
977
983
|
this.log(` ✗ Step ${stepNumber} failed: ${errorMsg}`);
|
|
978
984
|
// LIFECYCLE: onStepProgress (failure)
|
|
979
985
|
if (this.progressReporter?.onStepProgress && options.jobId) {
|
|
980
986
|
await this.progressReporter.onStepProgress({
|
|
981
987
|
jobId: options.jobId,
|
|
982
|
-
stepId
|
|
988
|
+
stepId,
|
|
983
989
|
stepNumber,
|
|
984
990
|
description,
|
|
985
991
|
code: stmt.code,
|
|
@@ -1003,20 +1009,25 @@ class ExecutionService {
|
|
|
1003
1009
|
// RUN_WITH_AI_REPAIR: Attempt repair (only for non-variable statements)
|
|
1004
1010
|
if (!isVariable) {
|
|
1005
1011
|
// Convert statement to step format for repair
|
|
1012
|
+
const stepId = stmt.stepId || `step-${stepNumber}`;
|
|
1006
1013
|
const stepForRepair = {
|
|
1007
|
-
id:
|
|
1014
|
+
id: stepId,
|
|
1008
1015
|
description: stmt.intentComment || '', // Don't use code as fallback
|
|
1009
1016
|
code: stmt.code
|
|
1010
1017
|
};
|
|
1011
1018
|
// Create temporary steps array for repair context
|
|
1012
1019
|
const stepsForRepair = allStatements
|
|
1013
1020
|
.filter(s => !s.isVariableDeclaration)
|
|
1014
|
-
.map((s, idx) =>
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1021
|
+
.map((s, idx) => {
|
|
1022
|
+
// Find original index in allStatements to get correct stepId
|
|
1023
|
+
const originalIndex = allStatements.indexOf(s);
|
|
1024
|
+
return {
|
|
1025
|
+
id: s.stepId || `step-${idx + 1}`, // Use preserved block index ID
|
|
1026
|
+
description: s.intentComment || '', // Don't use code as fallback
|
|
1027
|
+
code: s.code,
|
|
1028
|
+
success: originalIndex < i // Previous steps succeeded (use original index)
|
|
1029
|
+
};
|
|
1030
|
+
});
|
|
1020
1031
|
const repairResult = await this.attemptStepRepair(stepNumber - 1, // 0-based index
|
|
1021
1032
|
stepForRepair, stepsForRepair, executedStepDescriptions, page, persistentContext, options.jobId || '', options.model || model_constants_1.DEFAULT_MODEL);
|
|
1022
1033
|
if (repairResult.success) {
|
|
@@ -1462,108 +1473,396 @@ class ExecutionService {
|
|
|
1462
1473
|
return (0, browser_utils_1.initializeBrowser)(playwrightConfig, headless, playwrightConfigFilePath, this.logger);
|
|
1463
1474
|
}
|
|
1464
1475
|
/**
|
|
1465
|
-
* Execute
|
|
1466
|
-
* Parses
|
|
1467
|
-
*
|
|
1468
|
-
* Returns successful commands, the failing command, remaining commands, and error
|
|
1469
|
-
* This allows us to preserve successful commands and continue with remaining ones after repair
|
|
1476
|
+
* Execute a test file with hooks support
|
|
1477
|
+
* Parses the test file, extracts hooks and tests, executes them in correct order
|
|
1478
|
+
* Each test is reported separately with its own jobId
|
|
1470
1479
|
*/
|
|
1471
|
-
async
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
const declarations = []; // const/let/var declarations
|
|
1480
|
-
const commands = []; // executable statements (await/page/expect/ai/control flow)
|
|
1481
|
-
for (const statement of statements) {
|
|
1482
|
-
const trimmed = statement.trim();
|
|
1483
|
-
if (trimmed.length === 0) {
|
|
1484
|
-
continue;
|
|
1485
|
-
}
|
|
1486
|
-
// Skip comment-only statements
|
|
1487
|
-
const withoutComments = trimmed.replace(/^\s*\/\/.*$/gm, '').trim();
|
|
1488
|
-
if (withoutComments.length === 0) {
|
|
1489
|
-
continue;
|
|
1490
|
-
}
|
|
1491
|
-
const isDeclaration = /^(const|let|var)\b/.test(trimmed) && !/\bawait\b/.test(trimmed);
|
|
1492
|
-
const isExecutable = /\bawait\b/.test(statement) ||
|
|
1493
|
-
/\bpage\./.test(statement) ||
|
|
1494
|
-
/\bexpect\(/.test(statement) ||
|
|
1495
|
-
/\bai\./.test(statement) ||
|
|
1496
|
-
/^(if|for|while|switch|try|do)\b/.test(trimmed);
|
|
1497
|
-
if (isDeclaration) {
|
|
1498
|
-
declarations.push(statement);
|
|
1480
|
+
async runTestFile(request) {
|
|
1481
|
+
const startTime = Date.now();
|
|
1482
|
+
const testResults = [];
|
|
1483
|
+
try {
|
|
1484
|
+
// Read script content if scriptFilePath is provided
|
|
1485
|
+
let script;
|
|
1486
|
+
if (request.script) {
|
|
1487
|
+
script = request.script;
|
|
1499
1488
|
}
|
|
1500
|
-
else if (
|
|
1501
|
-
|
|
1489
|
+
else if (request.scriptFilePath) {
|
|
1490
|
+
try {
|
|
1491
|
+
script = fs.readFileSync(request.scriptFilePath, 'utf8');
|
|
1492
|
+
this.log(`Read test file from: ${request.scriptFilePath}`);
|
|
1493
|
+
}
|
|
1494
|
+
catch (error) {
|
|
1495
|
+
throw new Error(`Failed to read test file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1496
|
+
}
|
|
1502
1497
|
}
|
|
1503
1498
|
else {
|
|
1504
|
-
|
|
1505
|
-
commands.push(statement);
|
|
1499
|
+
throw new Error('Either script or scriptFilePath must be provided');
|
|
1506
1500
|
}
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
try {
|
|
1511
|
-
const fullCode = priorStepsContext ? priorStepsContext + '\n' + stepCode : stepCode;
|
|
1512
|
-
await this.executeStepCode(fullCode, page);
|
|
1513
|
-
return { successfulCommands: [stepCode], failingCommand: undefined, remainingCommands: [], error: undefined };
|
|
1501
|
+
// Validate tempDir is provided
|
|
1502
|
+
if (!request.tempDir) {
|
|
1503
|
+
throw new Error('tempDir is required for test file execution');
|
|
1514
1504
|
}
|
|
1515
|
-
|
|
1505
|
+
// Auto-discover playwright.config.js if not provided
|
|
1506
|
+
let playwrightConfig = request.playwrightConfig;
|
|
1507
|
+
let playwrightConfigFilePath = request.playwrightConfigFilePath;
|
|
1508
|
+
if (!playwrightConfig && !playwrightConfigFilePath) {
|
|
1509
|
+
const configExtensions = ['.js', '.ts', '.mjs'];
|
|
1510
|
+
for (const ext of configExtensions) {
|
|
1511
|
+
const configPath = path.join(request.tempDir, `playwright.config${ext}`);
|
|
1512
|
+
if (fs.existsSync(configPath)) {
|
|
1513
|
+
playwrightConfigFilePath = configPath;
|
|
1514
|
+
this.log(`Auto-discovered Playwright config: ${configPath}`);
|
|
1515
|
+
break;
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
// Extract imports from the original script once (they'll be needed for hooks and tests)
|
|
1520
|
+
// Store the full original script for passing to test executions
|
|
1521
|
+
const originalScript = script;
|
|
1522
|
+
// Parse test file to extract hooks and tests
|
|
1523
|
+
this.log('Parsing test file to extract hooks and tests...');
|
|
1524
|
+
const parsed = test_file_parser_1.TestFileParser.parseTestFile(script, request.testNames);
|
|
1525
|
+
// Flatten structure for execution
|
|
1526
|
+
const flattened = test_file_parser_1.TestFileParser.flattenForExecution(parsed);
|
|
1527
|
+
this.log(`Found ${flattened.fileLevelHooks.beforeAll.length} file-level beforeAll hooks, ${flattened.fileLevelHooks.afterAll.length} file-level afterAll hooks`);
|
|
1528
|
+
this.log(`Found ${flattened.fileLevelHooks.beforeEach.length} file-level beforeEach hooks, ${flattened.fileLevelHooks.afterEach.length} file-level afterEach hooks`);
|
|
1529
|
+
this.log(`Found ${flattened.suites.length} suite(s) with suite-level hooks`);
|
|
1530
|
+
this.log(`Found ${flattened.tests.length} test(s)`);
|
|
1531
|
+
if (flattened.tests.length === 0) {
|
|
1532
|
+
this.log('No tests found in file', 'warn');
|
|
1516
1533
|
return {
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
error: step_execution_utils_1.StepExecutionUtils.safeSerializeError(err)
|
|
1534
|
+
success: true,
|
|
1535
|
+
testResults: [],
|
|
1536
|
+
executionTime: Date.now() - startTime
|
|
1521
1537
|
};
|
|
1522
1538
|
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1539
|
+
// Initialize browser/context/page (shared across all hooks and tests)
|
|
1540
|
+
const useExistingBrowser = !!(request.existingBrowser && request.existingContext && request.existingPage);
|
|
1541
|
+
let browser;
|
|
1542
|
+
let context;
|
|
1543
|
+
let page;
|
|
1544
|
+
let browserCreated = false;
|
|
1545
|
+
if (useExistingBrowser) {
|
|
1546
|
+
this.log('Using existing browser/page provided by caller');
|
|
1547
|
+
browser = request.existingBrowser;
|
|
1548
|
+
context = request.existingContext;
|
|
1549
|
+
page = request.existingPage;
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
this.log('Initializing browser for test file execution...');
|
|
1553
|
+
const browserInstance = await this.initializeBrowser(playwrightConfig, request.headless !== false, playwrightConfigFilePath);
|
|
1554
|
+
browser = browserInstance.browser;
|
|
1555
|
+
context = browserInstance.context;
|
|
1556
|
+
page = browserInstance.page;
|
|
1557
|
+
browserCreated = true;
|
|
1558
|
+
}
|
|
1559
|
+
// Apply file upload overrides if tempDir is provided
|
|
1560
|
+
if (request.tempDir) {
|
|
1561
|
+
applyFileUploadOverrides(page, request.tempDir, request.testFolderPath, (msg, level) => this.log(msg, level));
|
|
1562
|
+
this.log(`Applied file upload overrides with tempDir: ${request.tempDir}`);
|
|
1563
|
+
}
|
|
1564
|
+
// Execute file-level beforeAll hooks
|
|
1565
|
+
if (flattened.fileLevelHooks.beforeAll.length > 0) {
|
|
1566
|
+
this.log(`Executing ${flattened.fileLevelHooks.beforeAll.length} file-level beforeAll hook(s)...`);
|
|
1567
|
+
try {
|
|
1568
|
+
for (const hook of flattened.fileLevelHooks.beforeAll) {
|
|
1569
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1570
|
+
}
|
|
1571
|
+
this.log('File-level beforeAll hooks executed successfully');
|
|
1572
|
+
}
|
|
1573
|
+
catch (error) {
|
|
1574
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1575
|
+
this.log(`File-level beforeAll hook failed: ${errorMsg}`, 'error');
|
|
1576
|
+
// Cleanup browser if we created it
|
|
1577
|
+
if (browserCreated && browser) {
|
|
1578
|
+
try {
|
|
1579
|
+
await browser.close();
|
|
1580
|
+
}
|
|
1581
|
+
catch (closeError) {
|
|
1582
|
+
// Ignore close errors
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
success: false,
|
|
1587
|
+
testResults: [],
|
|
1588
|
+
executionTime: Date.now() - startTime,
|
|
1589
|
+
error: `File-level beforeAll hook failed: ${errorMsg}`
|
|
1590
|
+
};
|
|
1549
1591
|
}
|
|
1550
|
-
this.log(` ✓ Command succeeded: ${cmd.substring(0, 60)}...`);
|
|
1551
1592
|
}
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1593
|
+
// Helper function to create a unique key from suite path (avoids collisions if suite names contain '__')
|
|
1594
|
+
const getSuiteKey = (suitePath) => {
|
|
1595
|
+
// Use JSON.stringify to safely serialize the array, avoiding collisions
|
|
1596
|
+
return JSON.stringify(suitePath);
|
|
1597
|
+
};
|
|
1598
|
+
// Build a Map of suites by key for O(1) lookups (instead of O(n) .find() calls)
|
|
1599
|
+
const suiteMap = new Map();
|
|
1600
|
+
for (const suite of flattened.suites) {
|
|
1601
|
+
const suiteKey = getSuiteKey(suite.suitePath);
|
|
1602
|
+
suiteMap.set(suiteKey, suite);
|
|
1603
|
+
}
|
|
1604
|
+
// Track suite execution state (which suites have had beforeAll executed)
|
|
1605
|
+
const suiteBeforeAllExecuted = new Set();
|
|
1606
|
+
// Track test counts per suite (for determining last test in suite)
|
|
1607
|
+
const suiteTestCounts = new Map();
|
|
1608
|
+
const suiteTotalTests = new Map();
|
|
1609
|
+
// Initialize suite test counts
|
|
1610
|
+
for (const suite of flattened.suites) {
|
|
1611
|
+
const suiteKey = getSuiteKey(suite.suitePath);
|
|
1612
|
+
suiteTotalTests.set(suiteKey, suite.testIndices.length);
|
|
1613
|
+
suiteTestCounts.set(suiteKey, 0);
|
|
1614
|
+
}
|
|
1615
|
+
// Execute each test
|
|
1616
|
+
for (let testIndex = 0; testIndex < flattened.tests.length; testIndex++) {
|
|
1617
|
+
const testData = flattened.tests[testIndex];
|
|
1618
|
+
const test = testData.test;
|
|
1619
|
+
const testStartTime = Date.now();
|
|
1620
|
+
const jobId = crypto.randomUUID();
|
|
1621
|
+
let testStatus = 'passed';
|
|
1622
|
+
let testError;
|
|
1623
|
+
try {
|
|
1624
|
+
// Before test execution: Execute suite-level beforeAll hooks (if first test in suite)
|
|
1625
|
+
if (testData.suitePath.length > 0) {
|
|
1626
|
+
// Execute beforeAll for each suite in the path (in order: parent → child)
|
|
1627
|
+
for (let i = 0; i < testData.suitePath.length; i++) {
|
|
1628
|
+
const suitePath = testData.suitePath.slice(0, i + 1);
|
|
1629
|
+
const suiteKey = getSuiteKey(suitePath);
|
|
1630
|
+
// Look up suite in Map (O(1) instead of O(n) .find())
|
|
1631
|
+
const suite = suiteMap.get(suiteKey);
|
|
1632
|
+
if (suite && suite.beforeAll.length > 0 && !suiteBeforeAllExecuted.has(suiteKey)) {
|
|
1633
|
+
// Use human-readable suite name for logging
|
|
1634
|
+
const suiteDisplayName = suitePath.join('__');
|
|
1635
|
+
this.log(`Executing suite-level beforeAll hooks for suite: ${suiteDisplayName}`);
|
|
1636
|
+
try {
|
|
1637
|
+
for (const hook of suite.beforeAll) {
|
|
1638
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1639
|
+
}
|
|
1640
|
+
suiteBeforeAllExecuted.add(suiteKey);
|
|
1641
|
+
this.log(`Suite-level beforeAll hooks executed for suite: ${suiteDisplayName}`);
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1645
|
+
this.log(`Suite-level beforeAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'error');
|
|
1646
|
+
testStatus = 'failed';
|
|
1647
|
+
testError = `Suite-level beforeAll hook failed: ${errorMsg}`;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
// Call onStartTest callback (use fullName)
|
|
1653
|
+
if (this.progressReporter?.onStartTest) {
|
|
1654
|
+
await this.progressReporter.onStartTest(test.fullName, jobId, page, browser, context);
|
|
1655
|
+
}
|
|
1656
|
+
// Execute file-level beforeEach hooks
|
|
1657
|
+
if (flattened.fileLevelHooks.beforeEach.length > 0 && testStatus === 'passed') {
|
|
1658
|
+
this.log(`Executing ${flattened.fileLevelHooks.beforeEach.length} file-level beforeEach hook(s) for test: ${test.fullName}`);
|
|
1659
|
+
try {
|
|
1660
|
+
for (const hook of flattened.fileLevelHooks.beforeEach) {
|
|
1661
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
catch (error) {
|
|
1665
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1666
|
+
this.log(`File-level beforeEach hook failed for test ${test.fullName}: ${errorMsg}`, 'error');
|
|
1667
|
+
testStatus = 'failed';
|
|
1668
|
+
testError = `File-level beforeEach hook failed: ${errorMsg}`;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
// Execute suite-level beforeEach hooks (from all parent suites, in order: parent → child)
|
|
1672
|
+
if (testData.suiteBeforeEachHooks.length > 0 && testStatus === 'passed') {
|
|
1673
|
+
this.log(`Executing ${testData.suiteBeforeEachHooks.length} suite-level beforeEach hook(s) for test: ${test.fullName}`);
|
|
1674
|
+
try {
|
|
1675
|
+
for (const hook of testData.suiteBeforeEachHooks) {
|
|
1676
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
catch (error) {
|
|
1680
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1681
|
+
this.log(`Suite-level beforeEach hook failed for test ${test.fullName}: ${errorMsg}`, 'error');
|
|
1682
|
+
testStatus = 'failed';
|
|
1683
|
+
testError = `Suite-level beforeEach hook failed: ${errorMsg}`;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
// Run the test if hooks didn't fail
|
|
1687
|
+
if (testStatus === 'passed') {
|
|
1688
|
+
try {
|
|
1689
|
+
// Execute test using existing infrastructure
|
|
1690
|
+
// Construct a script with imports + this test using AST (more robust than string interpolation)
|
|
1691
|
+
const testScript = test_file_parser_1.TestFileParser.constructTestScriptWithImports(originalScript, test.fullName, // Use fullName for test execution
|
|
1692
|
+
test.code);
|
|
1693
|
+
const result = await this.executeStepsInPersistentContext([], // No pre-parsed steps - AST will extract from test body
|
|
1694
|
+
page, browser, context, {
|
|
1695
|
+
mode: request.mode || types_1.ExecutionMode.RUN_EXACTLY,
|
|
1696
|
+
jobId: jobId,
|
|
1697
|
+
tempDir: request.tempDir,
|
|
1698
|
+
originalScript: testScript // Full script with imports + test
|
|
1699
|
+
});
|
|
1700
|
+
if (!result.success) {
|
|
1701
|
+
testStatus = 'failed';
|
|
1702
|
+
testError = result.error || 'Test execution failed';
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
catch (error) {
|
|
1706
|
+
testStatus = 'failed';
|
|
1707
|
+
testError = error instanceof Error ? error.message : String(error);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
// Execute suite-level afterEach hooks (from all parent suites, in reverse order: child → parent)
|
|
1711
|
+
if (testData.suiteAfterEachHooks.length > 0) {
|
|
1712
|
+
this.log(`Executing ${testData.suiteAfterEachHooks.length} suite-level afterEach hook(s) for test: ${test.fullName}`);
|
|
1713
|
+
try {
|
|
1714
|
+
for (const hook of testData.suiteAfterEachHooks) {
|
|
1715
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
catch (error) {
|
|
1719
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1720
|
+
this.log(`Suite-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
|
|
1721
|
+
// Log but continue - don't fail the test because of afterEach failure
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
// Execute file-level afterEach hooks (even if test failed)
|
|
1725
|
+
if (flattened.fileLevelHooks.afterEach.length > 0) {
|
|
1726
|
+
this.log(`Executing ${flattened.fileLevelHooks.afterEach.length} file-level afterEach hook(s) for test: ${test.fullName}`);
|
|
1727
|
+
try {
|
|
1728
|
+
for (const hook of flattened.fileLevelHooks.afterEach) {
|
|
1729
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
catch (error) {
|
|
1733
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1734
|
+
this.log(`File-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
|
|
1735
|
+
// Log but continue - don't fail the test because of afterEach failure
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
// After test execution: Increment test counts and execute suite-level afterAll hooks (if last test in suite)
|
|
1739
|
+
if (testData.suitePath.length > 0) {
|
|
1740
|
+
// Increment test count for each suite in the path
|
|
1741
|
+
for (let i = 0; i < testData.suitePath.length; i++) {
|
|
1742
|
+
const suitePath = testData.suitePath.slice(0, i + 1);
|
|
1743
|
+
const suiteKey = getSuiteKey(suitePath);
|
|
1744
|
+
const currentCount = suiteTestCounts.get(suiteKey) || 0;
|
|
1745
|
+
suiteTestCounts.set(suiteKey, currentCount + 1);
|
|
1746
|
+
}
|
|
1747
|
+
// Execute afterAll for each suite in the path (in reverse order: child → parent)
|
|
1748
|
+
for (let i = testData.suitePath.length - 1; i >= 0; i--) {
|
|
1749
|
+
const suitePath = testData.suitePath.slice(0, i + 1);
|
|
1750
|
+
const suiteKey = getSuiteKey(suitePath);
|
|
1751
|
+
// Look up suite in Map (O(1) instead of O(n) .find())
|
|
1752
|
+
const suite = suiteMap.get(suiteKey);
|
|
1753
|
+
if (suite && suite.afterAll.length > 0) {
|
|
1754
|
+
const totalTests = suiteTotalTests.get(suiteKey) || 0;
|
|
1755
|
+
const currentCount = suiteTestCounts.get(suiteKey) || 0;
|
|
1756
|
+
// If this is the last test in the suite, execute afterAll
|
|
1757
|
+
if (currentCount >= totalTests) {
|
|
1758
|
+
// Use human-readable suite name for logging
|
|
1759
|
+
const suiteDisplayName = suitePath.join('__');
|
|
1760
|
+
this.log(`Executing suite-level afterAll hooks for suite: ${suiteDisplayName}`);
|
|
1761
|
+
try {
|
|
1762
|
+
for (const hook of suite.afterAll) {
|
|
1763
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1764
|
+
}
|
|
1765
|
+
this.log(`Suite-level afterAll hooks executed for suite: ${suiteDisplayName}`);
|
|
1766
|
+
}
|
|
1767
|
+
catch (error) {
|
|
1768
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1769
|
+
this.log(`Suite-level afterAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'warn');
|
|
1770
|
+
// Log but continue - don't fail the test because of afterAll failure
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
// Call onEndTest callback (use fullName)
|
|
1777
|
+
if (this.progressReporter?.onEndTest) {
|
|
1778
|
+
await this.progressReporter.onEndTest(test.fullName, jobId, testStatus, testError, page, browser, context);
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
catch (error) {
|
|
1782
|
+
// Unexpected error during test execution
|
|
1783
|
+
testStatus = 'failed';
|
|
1784
|
+
testError = error instanceof Error ? error.message : String(error);
|
|
1785
|
+
this.log(`Unexpected error during test ${test.name}: ${testError}`, 'error');
|
|
1786
|
+
// Still call onEndTest if possible
|
|
1787
|
+
if (this.progressReporter?.onEndTest) {
|
|
1788
|
+
try {
|
|
1789
|
+
await this.progressReporter.onEndTest(test.fullName, jobId, testStatus, testError, page, browser, context);
|
|
1790
|
+
}
|
|
1791
|
+
catch (callbackError) {
|
|
1792
|
+
this.log(`onEndTest callback failed: ${callbackError}`, 'warn');
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
// Record test result (use fullName)
|
|
1797
|
+
testResults.push({
|
|
1798
|
+
testName: test.fullName,
|
|
1799
|
+
jobId: jobId,
|
|
1800
|
+
status: testStatus,
|
|
1801
|
+
error: testError,
|
|
1802
|
+
executionTime: Date.now() - testStartTime
|
|
1803
|
+
});
|
|
1804
|
+
this.log(`Test ${test.fullName} completed with status: ${testStatus}`);
|
|
1805
|
+
}
|
|
1806
|
+
// Execute file-level afterAll hooks
|
|
1807
|
+
if (flattened.fileLevelHooks.afterAll.length > 0) {
|
|
1808
|
+
this.log(`Executing ${flattened.fileLevelHooks.afterAll.length} file-level afterAll hook(s)...`);
|
|
1809
|
+
try {
|
|
1810
|
+
for (const hook of flattened.fileLevelHooks.afterAll) {
|
|
1811
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir);
|
|
1812
|
+
}
|
|
1813
|
+
this.log('File-level afterAll hooks executed successfully');
|
|
1814
|
+
}
|
|
1815
|
+
catch (error) {
|
|
1816
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1817
|
+
this.log(`File-level afterAll hook failed: ${errorMsg}`, 'warn');
|
|
1818
|
+
// Log but don't fail overall execution
|
|
1562
1819
|
}
|
|
1563
|
-
break; // Stop at first failure
|
|
1564
1820
|
}
|
|
1821
|
+
// Cleanup browser if we created it
|
|
1822
|
+
if (browserCreated && browser) {
|
|
1823
|
+
try {
|
|
1824
|
+
await browser.close();
|
|
1825
|
+
this.log('Browser closed');
|
|
1826
|
+
}
|
|
1827
|
+
catch (closeError) {
|
|
1828
|
+
this.log(`Error closing browser: ${closeError}`, 'warn');
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
// Determine overall success (all tests passed)
|
|
1832
|
+
const allPassed = testResults.every(result => result.status === 'passed');
|
|
1833
|
+
return {
|
|
1834
|
+
success: allPassed,
|
|
1835
|
+
testResults: testResults,
|
|
1836
|
+
executionTime: Date.now() - startTime
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
catch (error) {
|
|
1840
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
1841
|
+
this.log(`Test file execution failed: ${errorMsg}`, 'error');
|
|
1842
|
+
return {
|
|
1843
|
+
success: false,
|
|
1844
|
+
testResults: testResults,
|
|
1845
|
+
executionTime: Date.now() - startTime,
|
|
1846
|
+
error: errorMsg
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
/**
|
|
1851
|
+
* Execute a hook (beforeAll, afterAll, beforeEach, afterEach)
|
|
1852
|
+
* Uses the same execution context as tests
|
|
1853
|
+
*/
|
|
1854
|
+
async executeHook(hookCode, page, context, browser, tempDir) {
|
|
1855
|
+
const { expect, test } = require('@playwright/test');
|
|
1856
|
+
const { ai } = require('ai-wright');
|
|
1857
|
+
// Create execution context for the hook
|
|
1858
|
+
const hookContext = new PersistentExecutionContext(page, expect, test, ai, browser, context, tempDir);
|
|
1859
|
+
try {
|
|
1860
|
+
// Execute hook code
|
|
1861
|
+
await hookContext.executeCode(hookCode);
|
|
1862
|
+
}
|
|
1863
|
+
finally {
|
|
1864
|
+
hookContext.dispose();
|
|
1565
1865
|
}
|
|
1566
|
-
return { successfulCommands, failingCommand, remainingCommands, error };
|
|
1567
1866
|
}
|
|
1568
1867
|
}
|
|
1569
1868
|
exports.ExecutionService = ExecutionService;
|