testchimp-runner-core 0.1.32 → 0.1.34
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.
|
@@ -59,6 +59,9 @@ const orchestrator_1 = require("./orchestrator");
|
|
|
59
59
|
const test_file_parser_1 = require("./utils/test-file-parser");
|
|
60
60
|
const dotenv = __importStar(require("dotenv"));
|
|
61
61
|
class ModuleResolutionManager {
|
|
62
|
+
static setFallbackContext(ctx) {
|
|
63
|
+
this.fallbackContext = ctx;
|
|
64
|
+
}
|
|
62
65
|
/**
|
|
63
66
|
* Install the global hook (only once, shared by all instances)
|
|
64
67
|
*/
|
|
@@ -88,8 +91,9 @@ class ModuleResolutionManager {
|
|
|
88
91
|
}
|
|
89
92
|
// Install hook that uses AsyncLocalStorage to get the current tempDir and testFileDir
|
|
90
93
|
Module._resolveFilename = function (request, parent, isMain, options) {
|
|
91
|
-
// Get resolution context for
|
|
92
|
-
const context = ModuleResolutionManager.asyncLocalStorage.getStore()
|
|
94
|
+
// Get resolution context: AsyncLocalStorage first, then fallback (for require() from inside vm.runInContext)
|
|
95
|
+
const context = ModuleResolutionManager.asyncLocalStorage.getStore()
|
|
96
|
+
?? ModuleResolutionManager.fallbackContext;
|
|
93
97
|
// If no context, use original resolution (for requires outside our execution context)
|
|
94
98
|
if (!context) {
|
|
95
99
|
return ModuleResolutionManager.originalResolveFilename.call(this, request, parent, isMain, options);
|
|
@@ -222,6 +226,8 @@ class ModuleResolutionManager {
|
|
|
222
226
|
ModuleResolutionManager.asyncLocalStorage = new async_hooks_1.AsyncLocalStorage();
|
|
223
227
|
ModuleResolutionManager.hookInstalled = false;
|
|
224
228
|
ModuleResolutionManager.scriptserviceNodeModules = null;
|
|
229
|
+
/** Fallback when require runs inside vm.runInContext and AsyncLocalStorage is not visible */
|
|
230
|
+
ModuleResolutionManager.fallbackContext = null;
|
|
225
231
|
/**
|
|
226
232
|
* Persistent execution context using vm.createContext
|
|
227
233
|
* Maintains variable scope across multiple step executions
|
|
@@ -356,9 +362,16 @@ class PersistentExecutionContext {
|
|
|
356
362
|
if (!this.context) {
|
|
357
363
|
throw new Error('Context has been disposed');
|
|
358
364
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
// Ensure require() from inside VM sees context even if AsyncLocalStorage is lost there
|
|
366
|
+
ModuleResolutionManager.setFallbackContext(context);
|
|
367
|
+
try {
|
|
368
|
+
const script = new vm.Script(wrappedCode);
|
|
369
|
+
const result = script.runInContext(this.context);
|
|
370
|
+
await result;
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
ModuleResolutionManager.setFallbackContext(null);
|
|
374
|
+
}
|
|
362
375
|
});
|
|
363
376
|
}
|
|
364
377
|
else {
|
|
@@ -1993,525 +2006,601 @@ class ExecutionService {
|
|
|
1993
2006
|
applyFileUploadOverrides(page, request.tempDir, request.testFolderPath, (msg, level) => this.log(msg, level));
|
|
1994
2007
|
this.log(`Applied file upload overrides with tempDir: ${request.tempDir}`);
|
|
1995
2008
|
}
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
if (request.tempDir) {
|
|
2018
|
-
await suiteContextData.context.executeCode(getEnvBootstrapCode());
|
|
2019
|
-
}
|
|
2020
|
-
// Execute imports from originalScript (for module resolution)
|
|
2021
|
-
if (originalScript && request.tempDir) {
|
|
2022
|
-
const importStatements = import_utils_1.ImportUtils.extractImportStatements(originalScript, (msg) => this.log(msg));
|
|
2023
|
-
if (importStatements.length > 0) {
|
|
2024
|
-
const importsCode = import_utils_1.ImportUtils.convertImportsToRequires(importStatements, (msg) => this.log(msg));
|
|
2025
|
-
if (importsCode) {
|
|
2026
|
-
await suiteContextData.context.executeCode(importsCode);
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
}
|
|
2030
|
-
// Initialize file variables (parent scope) - order: file variables → suite variables
|
|
2031
|
-
if (fileVariables.length > 0) {
|
|
2032
|
-
const fileVarsCode = fileVariables.join('\n');
|
|
2033
|
-
await suiteContextData.context.executeCode(fileVarsCode);
|
|
2034
|
-
}
|
|
2035
|
-
// Initialize suite variables (child scope)
|
|
2036
|
-
if (suiteVariables.length > 0) {
|
|
2037
|
-
const suiteVarsCode = suiteVariables.join('\n');
|
|
2038
|
-
await suiteContextData.context.executeCode(suiteVarsCode);
|
|
2039
|
-
}
|
|
2040
|
-
suiteContextData.variablesInitialized = true;
|
|
2041
|
-
}
|
|
2042
|
-
catch (error) {
|
|
2043
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2044
|
-
this.log(`Variable initialization failed for suite ${suitePath.join('__')}: ${errorMsg}`, 'warn');
|
|
2045
|
-
// Continue - variables may be optional
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
return suiteContextData.context;
|
|
2049
|
-
};
|
|
2050
|
-
// Helper to dispose suite context
|
|
2051
|
-
const disposeSuiteContext = (suitePath) => {
|
|
2052
|
-
const suiteKey = JSON.stringify(suitePath);
|
|
2053
|
-
const suiteContextData = suiteContexts.get(suiteKey);
|
|
2054
|
-
if (suiteContextData) {
|
|
2055
|
-
suiteContextData.context.dispose();
|
|
2056
|
-
suiteContexts.delete(suiteKey);
|
|
2009
|
+
const testFileDir = request.testFolderPath && request.testFolderPath.length > 0
|
|
2010
|
+
? path.join(request.tempDir, ...request.testFolderPath)
|
|
2011
|
+
: path.join(request.tempDir, 'tests');
|
|
2012
|
+
// Clear require cache so page/test files are always loaded fresh (avoids stale POM/page cache across runs).
|
|
2013
|
+
// Clear (1) current run's tempDir in both normalized and realpath form, and (2) ALL workspace dirs under
|
|
2014
|
+
// /tmp/testchimp (and /private/tmp/testchimp on macOS) so no prior run can serve a cached module.
|
|
2015
|
+
const tempDirNorm = path.normalize(request.tempDir) + path.sep;
|
|
2016
|
+
let tempDirRealNorm = null;
|
|
2017
|
+
try {
|
|
2018
|
+
const rp = fs.realpathSync(request.tempDir);
|
|
2019
|
+
tempDirRealNorm = path.normalize(rp) + path.sep;
|
|
2020
|
+
}
|
|
2021
|
+
catch (_) {
|
|
2022
|
+
// ignore
|
|
2023
|
+
}
|
|
2024
|
+
const testchimpPrefixes = [];
|
|
2025
|
+
try {
|
|
2026
|
+
testchimpPrefixes.push(path.normalize('/tmp/testchimp') + path.sep);
|
|
2027
|
+
const rp = fs.realpathSync('/tmp/testchimp');
|
|
2028
|
+
if (rp !== '/tmp/testchimp') {
|
|
2029
|
+
testchimpPrefixes.push(path.normalize(rp) + path.sep);
|
|
2057
2030
|
}
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2031
|
+
}
|
|
2032
|
+
catch (_) {
|
|
2033
|
+
// /tmp/testchimp may not exist yet
|
|
2034
|
+
}
|
|
2035
|
+
const cacheKeys = Object.keys(require.cache);
|
|
2036
|
+
let cleared = 0;
|
|
2037
|
+
for (const id of cacheKeys) {
|
|
2038
|
+
try {
|
|
2039
|
+
const normalized = path.normalize(id);
|
|
2040
|
+
let idRealNorm = null;
|
|
2066
2041
|
try {
|
|
2067
|
-
|
|
2068
|
-
// Use testFolderPath from request if available, otherwise default to tempDir/tests
|
|
2069
|
-
const testFileDir = request.testFolderPath && request.testFolderPath.length > 0
|
|
2070
|
-
? path.join(request.tempDir, ...request.testFolderPath)
|
|
2071
|
-
: path.join(request.tempDir, 'tests');
|
|
2072
|
-
fileLevelContext.setTestFileDir(testFileDir);
|
|
2073
|
-
// Load .env inside VM before any user code so process.env is available in test and POM code
|
|
2074
|
-
if (request.tempDir) {
|
|
2075
|
-
await fileLevelContext.executeCode(getEnvBootstrapCode());
|
|
2076
|
-
}
|
|
2077
|
-
// Execute imports from originalScript (for module resolution)
|
|
2078
|
-
if (originalScript && request.tempDir) {
|
|
2079
|
-
const importStatements = import_utils_1.ImportUtils.extractImportStatements(originalScript, (msg) => this.log(msg));
|
|
2080
|
-
if (importStatements.length > 0) {
|
|
2081
|
-
const importsCode = import_utils_1.ImportUtils.convertImportsToRequires(importStatements, (msg) => this.log(msg));
|
|
2082
|
-
if (importsCode) {
|
|
2083
|
-
await fileLevelContext.executeCode(importsCode);
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
// Initialize file-level variables
|
|
2088
|
-
if (flattened.fileVariables.length > 0) {
|
|
2089
|
-
const fileVarsCode = flattened.fileVariables.join('\n');
|
|
2090
|
-
await fileLevelContext.executeCode(fileVarsCode);
|
|
2091
|
-
}
|
|
2042
|
+
idRealNorm = path.normalize(fs.realpathSync(id));
|
|
2092
2043
|
}
|
|
2093
|
-
catch (
|
|
2094
|
-
|
|
2095
|
-
this.log(`File-level variable initialization failed: ${errorMsg}`, 'warn');
|
|
2096
|
-
// Continue - variables may be optional
|
|
2044
|
+
catch (_) {
|
|
2045
|
+
// file may not exist
|
|
2097
2046
|
}
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook);
|
|
2047
|
+
const matchCurrent = normalized.startsWith(tempDirNorm) ||
|
|
2048
|
+
normalized === path.normalize(request.tempDir) ||
|
|
2049
|
+
(tempDirRealNorm != null && (normalized.startsWith(tempDirRealNorm) || normalized === tempDirRealNorm.slice(0, -1))) ||
|
|
2050
|
+
(idRealNorm != null && tempDirRealNorm != null && idRealNorm.startsWith(tempDirRealNorm));
|
|
2051
|
+
const matchAnyWorkspace = testchimpPrefixes.some((prefix) => normalized.startsWith(prefix) ||
|
|
2052
|
+
(idRealNorm != null && idRealNorm.startsWith(prefix)));
|
|
2053
|
+
if (matchCurrent || matchAnyWorkspace) {
|
|
2054
|
+
delete require.cache[id];
|
|
2055
|
+
cleared++;
|
|
2108
2056
|
}
|
|
2109
|
-
this.log('File-level beforeAll hooks executed successfully');
|
|
2110
2057
|
}
|
|
2111
|
-
catch (
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2058
|
+
catch (_) {
|
|
2059
|
+
// ignore
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
if (cleared > 0) {
|
|
2063
|
+
this.log(`Cleared ${cleared} require.cache entries for workspace(s) to avoid stale page/module cache`);
|
|
2064
|
+
}
|
|
2065
|
+
// Clear Node's Module._pathCache so resolution is not short-circuited by a cached path from a previous run
|
|
2066
|
+
const Mod = require('module');
|
|
2067
|
+
if (typeof Mod._pathCache === 'object' && Mod._pathCache !== null) {
|
|
2068
|
+
const pathCache = Mod._pathCache;
|
|
2069
|
+
const pathCacheKeys = Object.keys(pathCache);
|
|
2070
|
+
let pathCacheCleared = 0;
|
|
2071
|
+
for (const key of pathCacheKeys) {
|
|
2072
|
+
const keyStr = String(key);
|
|
2073
|
+
if (keyStr.includes(path.normalize('/tmp/testchimp')) ||
|
|
2074
|
+
(tempDirRealNorm != null && keyStr.includes(tempDirRealNorm.slice(0, -1)))) {
|
|
2075
|
+
delete pathCache[key];
|
|
2076
|
+
pathCacheCleared++;
|
|
2122
2077
|
}
|
|
2123
|
-
return {
|
|
2124
|
-
success: false,
|
|
2125
|
-
testResults: [],
|
|
2126
|
-
executionTime: Date.now() - startTime,
|
|
2127
|
-
error: `File-level beforeAll hook failed: ${errorMsg}`
|
|
2128
|
-
};
|
|
2129
2078
|
}
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
if (testData.suitePath.length > 0) {
|
|
2167
|
-
// Execute beforeAll for each suite in the path (in order: parent → child)
|
|
2168
|
-
for (let i = 0; i < testData.suitePath.length; i++) {
|
|
2169
|
-
const suitePath = testData.suitePath.slice(0, i + 1);
|
|
2170
|
-
const suiteKey = getSuiteKey(suitePath);
|
|
2171
|
-
// Look up suite in Map (O(1) instead of O(n) .find())
|
|
2172
|
-
const suite = suiteMap.get(suiteKey);
|
|
2173
|
-
if (suite && suite.beforeAll.length > 0 && !suiteBeforeAllExecuted.has(suiteKey)) {
|
|
2174
|
-
// Use human-readable suite name for logging
|
|
2175
|
-
const suiteDisplayName = suitePath.join('__');
|
|
2176
|
-
this.log(`Executing suite-level beforeAll hooks for suite: ${suiteDisplayName}`);
|
|
2177
|
-
try {
|
|
2178
|
-
// Get or create suite context (includes file variables + suite variables)
|
|
2179
|
-
const suiteContext = await getOrCreateSuiteContext(suitePath, suite.suiteVariables, flattened.fileVariables);
|
|
2180
|
-
for (const hook of suite.beforeAll) {
|
|
2181
|
-
await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook);
|
|
2079
|
+
if (pathCacheCleared > 0) {
|
|
2080
|
+
this.log(`Cleared ${pathCacheCleared} Module._pathCache entries for workspace(s)`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return await ModuleResolutionManager.runWithContext({ tempDir: request.tempDir, testFileDir }, async () => {
|
|
2084
|
+
// Create suite context manager - one context per suite, shared across all hooks and tests
|
|
2085
|
+
const suiteContexts = new Map();
|
|
2086
|
+
// Helper to get or create suite context
|
|
2087
|
+
const getOrCreateSuiteContext = async (suitePath, suiteVariables, fileVariables) => {
|
|
2088
|
+
const suiteKey = JSON.stringify(suitePath);
|
|
2089
|
+
if (!suiteContexts.has(suiteKey)) {
|
|
2090
|
+
const { expect, test } = require('@playwright/test');
|
|
2091
|
+
const { ai } = require('ai-wright');
|
|
2092
|
+
const suiteContext = new PersistentExecutionContext(page, expect, test, ai, browser, context, request.tempDir);
|
|
2093
|
+
suiteContexts.set(suiteKey, { context: suiteContext, variablesInitialized: false });
|
|
2094
|
+
}
|
|
2095
|
+
const suiteContextData = suiteContexts.get(suiteKey);
|
|
2096
|
+
if (!suiteContextData.variablesInitialized) {
|
|
2097
|
+
try {
|
|
2098
|
+
// Set testFileDir on context for module resolution (needed before executing requires)
|
|
2099
|
+
// Use testFolderPath from request if available, otherwise default to tempDir/tests
|
|
2100
|
+
const testFileDir = request.testFolderPath && request.testFolderPath.length > 0
|
|
2101
|
+
? path.join(request.tempDir, ...request.testFolderPath)
|
|
2102
|
+
: path.join(request.tempDir, 'tests');
|
|
2103
|
+
suiteContextData.context.setTestFileDir(testFileDir);
|
|
2104
|
+
// Load .env inside VM before any user code so process.env is available in test and POM code
|
|
2105
|
+
if (request.tempDir) {
|
|
2106
|
+
await suiteContextData.context.executeCode(getEnvBootstrapCode());
|
|
2107
|
+
}
|
|
2108
|
+
// Execute imports from originalScript (for module resolution)
|
|
2109
|
+
if (originalScript && request.tempDir) {
|
|
2110
|
+
const importStatements = import_utils_1.ImportUtils.extractImportStatements(originalScript, (msg) => this.log(msg));
|
|
2111
|
+
if (importStatements.length > 0) {
|
|
2112
|
+
const importsCode = import_utils_1.ImportUtils.convertImportsToRequires(importStatements, (msg) => this.log(msg));
|
|
2113
|
+
if (importsCode) {
|
|
2114
|
+
await suiteContextData.context.executeCode(importsCode);
|
|
2182
2115
|
}
|
|
2183
|
-
suiteBeforeAllExecuted.add(suiteKey);
|
|
2184
|
-
this.log(`Suite-level beforeAll hooks executed for suite: ${suiteDisplayName}`);
|
|
2185
|
-
}
|
|
2186
|
-
catch (error) {
|
|
2187
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2188
|
-
this.log(`Suite-level beforeAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'error');
|
|
2189
|
-
testStatus = 'failed';
|
|
2190
|
-
testError = `Suite-level beforeAll hook failed: ${errorMsg}`;
|
|
2191
2116
|
}
|
|
2192
2117
|
}
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
try {
|
|
2198
|
-
await this.progressReporter.onStartTest({ suitePath: test.suitePath || [], testName: test.name }, jobId, page, browser, context);
|
|
2199
|
-
}
|
|
2200
|
-
catch (callbackError) {
|
|
2201
|
-
this.log(`onStartTest callback failed: ${callbackError instanceof Error ? callbackError.message : String(callbackError)}`, 'warn');
|
|
2202
|
-
// Don't fail the test if callback has issues - runner-core is a library
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
// Track step counter for sequential numeric step IDs across hooks and test
|
|
2206
|
-
let testStepCounter = 0;
|
|
2207
|
-
// Execute file-level beforeEach hooks
|
|
2208
|
-
if (flattened.fileLevelHooks.beforeEach.length > 0 && testStatus === 'passed') {
|
|
2209
|
-
this.log(`Executing ${flattened.fileLevelHooks.beforeEach.length} file-level beforeEach hook(s) for test: ${test.fullName}`);
|
|
2210
|
-
try {
|
|
2211
|
-
for (const hook of flattened.fileLevelHooks.beforeEach) {
|
|
2212
|
-
const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook, jobId, testStepCounter);
|
|
2213
|
-
testStepCounter += stepsExecuted;
|
|
2118
|
+
// Initialize file variables (parent scope) - order: file variables → suite variables
|
|
2119
|
+
if (fileVariables.length > 0) {
|
|
2120
|
+
const fileVarsCode = fileVariables.join('\n');
|
|
2121
|
+
await suiteContextData.context.executeCode(fileVarsCode);
|
|
2214
2122
|
}
|
|
2123
|
+
// Initialize suite variables (child scope)
|
|
2124
|
+
if (suiteVariables.length > 0) {
|
|
2125
|
+
const suiteVarsCode = suiteVariables.join('\n');
|
|
2126
|
+
await suiteContextData.context.executeCode(suiteVarsCode);
|
|
2127
|
+
}
|
|
2128
|
+
suiteContextData.variablesInitialized = true;
|
|
2215
2129
|
}
|
|
2216
2130
|
catch (error) {
|
|
2217
2131
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2218
|
-
this.log(`
|
|
2219
|
-
|
|
2220
|
-
testError = `File-level beforeEach hook failed: ${errorMsg}`;
|
|
2132
|
+
this.log(`Variable initialization failed for suite ${suitePath.join('__')}: ${errorMsg}`, 'warn');
|
|
2133
|
+
// Continue - variables may be optional
|
|
2221
2134
|
}
|
|
2222
2135
|
}
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2136
|
+
return suiteContextData.context;
|
|
2137
|
+
};
|
|
2138
|
+
// Helper to dispose suite context
|
|
2139
|
+
const disposeSuiteContext = (suitePath) => {
|
|
2140
|
+
const suiteKey = JSON.stringify(suitePath);
|
|
2141
|
+
const suiteContextData = suiteContexts.get(suiteKey);
|
|
2142
|
+
if (suiteContextData) {
|
|
2143
|
+
suiteContextData.context.dispose();
|
|
2144
|
+
suiteContexts.delete(suiteKey);
|
|
2145
|
+
}
|
|
2146
|
+
};
|
|
2147
|
+
// Create file-level shared context for file-level hooks
|
|
2148
|
+
let fileLevelContext = null;
|
|
2149
|
+
const initializeFileLevelContext = async () => {
|
|
2150
|
+
if (!fileLevelContext && (flattened.fileVariables.length > 0 || flattened.fileLevelHooks.beforeAll.length > 0)) {
|
|
2151
|
+
const { expect, test } = require('@playwright/test');
|
|
2152
|
+
const { ai } = require('ai-wright');
|
|
2153
|
+
fileLevelContext = new PersistentExecutionContext(page, expect, test, ai, browser, context, request.tempDir);
|
|
2226
2154
|
try {
|
|
2227
|
-
//
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2155
|
+
// Set testFileDir on context for module resolution (needed before executing requires)
|
|
2156
|
+
// Use testFolderPath from request if available, otherwise default to tempDir/tests
|
|
2157
|
+
const testFileDir = request.testFolderPath && request.testFolderPath.length > 0
|
|
2158
|
+
? path.join(request.tempDir, ...request.testFolderPath)
|
|
2159
|
+
: path.join(request.tempDir, 'tests');
|
|
2160
|
+
fileLevelContext.setTestFileDir(testFileDir);
|
|
2161
|
+
// Load .env inside VM before any user code so process.env is available in test and POM code
|
|
2162
|
+
if (request.tempDir) {
|
|
2163
|
+
await fileLevelContext.executeCode(getEnvBootstrapCode());
|
|
2164
|
+
}
|
|
2165
|
+
// Execute imports from originalScript (for module resolution)
|
|
2166
|
+
if (originalScript && request.tempDir) {
|
|
2167
|
+
const importStatements = import_utils_1.ImportUtils.extractImportStatements(originalScript, (msg) => this.log(msg));
|
|
2168
|
+
if (importStatements.length > 0) {
|
|
2169
|
+
const importsCode = import_utils_1.ImportUtils.convertImportsToRequires(importStatements, (msg) => this.log(msg));
|
|
2170
|
+
if (importsCode) {
|
|
2171
|
+
await fileLevelContext.executeCode(importsCode);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
// Initialize file-level variables
|
|
2176
|
+
if (flattened.fileVariables.length > 0) {
|
|
2177
|
+
const fileVarsCode = flattened.fileVariables.join('\n');
|
|
2178
|
+
await fileLevelContext.executeCode(fileVarsCode);
|
|
2232
2179
|
}
|
|
2233
2180
|
}
|
|
2234
2181
|
catch (error) {
|
|
2235
2182
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2236
|
-
this.log(`
|
|
2237
|
-
|
|
2238
|
-
testError = `Suite-level beforeEach hook failed: ${errorMsg}`;
|
|
2183
|
+
this.log(`File-level variable initialization failed: ${errorMsg}`, 'warn');
|
|
2184
|
+
// Continue - variables may be optional
|
|
2239
2185
|
}
|
|
2240
2186
|
}
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
: (stmt.stepId || String(testStepCounter + stepIndex++));
|
|
2261
|
-
// Log stepId assignment for debugging
|
|
2262
|
-
if (!stmt.isVariableDeclaration) {
|
|
2263
|
-
if (stmt.stepId) {
|
|
2264
|
-
this.log(`[stepId] Using hash-based stepId from parser: ${stmt.stepId.substring(0, 16)}... (test: ${test.fullName})`);
|
|
2265
|
-
}
|
|
2266
|
-
else {
|
|
2267
|
-
this.log(`[stepId] WARNING: No hash-based stepId from parser, using sequential: ${stepId} (test: ${test.fullName})`);
|
|
2268
|
-
}
|
|
2269
|
-
}
|
|
2270
|
-
return {
|
|
2271
|
-
id: stepId,
|
|
2272
|
-
code: stmt.code,
|
|
2273
|
-
description: stmt.intentComment || stmt.code.trim().substring(0, 100) || '',
|
|
2274
|
-
isVariableDeclaration: stmt.isVariableDeclaration, // Pass AST-based detection flag
|
|
2275
|
-
scenarioAnnotation: stmt.scenarioAnnotation
|
|
2276
|
-
};
|
|
2277
|
-
});
|
|
2278
|
-
// Get or create suite context for this test's suite
|
|
2279
|
-
const suiteContext = await getOrCreateSuiteContext(testData.suitePath, testData.suiteVariables, flattened.fileVariables);
|
|
2280
|
-
// Execute using the existing step-wise execution infrastructure
|
|
2281
|
-
// executeStepsInPersistentContext will:
|
|
2282
|
-
// 1. Use shared context (variables already initialized)
|
|
2283
|
-
// 2. Execute ALL statements sequentially (variables + actions)
|
|
2284
|
-
// 3. Variables execute silently and persist in context
|
|
2285
|
-
// 4. Non-variable statements trigger callbacks (onStepProgress)
|
|
2286
|
-
const result = await this.executeStepsInPersistentContext(steps, // Pre-parsed steps from initial parse with AST-based flags
|
|
2287
|
-
page, browser, context, {
|
|
2288
|
-
mode: request.mode || types_1.ExecutionMode.RUN_EXACTLY,
|
|
2289
|
-
jobId: jobId,
|
|
2290
|
-
tempDir: request.tempDir,
|
|
2291
|
-
originalScript: testScript, // For module resolution (imports)
|
|
2292
|
-
model: request.model,
|
|
2293
|
-
testFolderPath: request.testFolderPath,
|
|
2294
|
-
runPomDenormalized: request.runPomDenormalized
|
|
2295
|
-
}, suiteContext // Pass shared context
|
|
2296
|
-
);
|
|
2297
|
-
if (!result.success) {
|
|
2298
|
-
testStatus = 'failed';
|
|
2299
|
-
testError = result.error || 'Test execution failed';
|
|
2187
|
+
};
|
|
2188
|
+
// Initialize file-level context before file-level hooks
|
|
2189
|
+
await initializeFileLevelContext();
|
|
2190
|
+
// Execute file-level beforeAll hooks
|
|
2191
|
+
if (flattened.fileLevelHooks.beforeAll.length > 0) {
|
|
2192
|
+
this.log(`Executing ${flattened.fileLevelHooks.beforeAll.length} file-level beforeAll hook(s)...`);
|
|
2193
|
+
try {
|
|
2194
|
+
for (const hook of flattened.fileLevelHooks.beforeAll) {
|
|
2195
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook);
|
|
2196
|
+
}
|
|
2197
|
+
this.log('File-level beforeAll hooks executed successfully');
|
|
2198
|
+
}
|
|
2199
|
+
catch (error) {
|
|
2200
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2201
|
+
this.log(`File-level beforeAll hook failed: ${errorMsg}`, 'error');
|
|
2202
|
+
// Cleanup browser if we created it
|
|
2203
|
+
if (browserCreated && browser) {
|
|
2204
|
+
try {
|
|
2205
|
+
await browser.close();
|
|
2300
2206
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2207
|
+
catch (closeError) {
|
|
2208
|
+
// Ignore close errors
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
return {
|
|
2212
|
+
success: false,
|
|
2213
|
+
testResults: [],
|
|
2214
|
+
executionTime: Date.now() - startTime,
|
|
2215
|
+
error: `File-level beforeAll hook failed: ${errorMsg}`
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
// Helper function to create a unique key from suite path (avoids collisions if suite names contain '__')
|
|
2220
|
+
const getSuiteKey = (suitePath) => {
|
|
2221
|
+
// Use JSON.stringify to safely serialize the array, avoiding collisions
|
|
2222
|
+
return JSON.stringify(suitePath);
|
|
2223
|
+
};
|
|
2224
|
+
// Build a Map of suites by key for O(1) lookups (instead of O(n) .find() calls)
|
|
2225
|
+
const suiteMap = new Map();
|
|
2226
|
+
for (const suite of flattened.suites) {
|
|
2227
|
+
const suiteKey = getSuiteKey(suite.suitePath);
|
|
2228
|
+
suiteMap.set(suiteKey, suite);
|
|
2229
|
+
}
|
|
2230
|
+
// Track suite execution state (which suites have had beforeAll executed)
|
|
2231
|
+
const suiteBeforeAllExecuted = new Set();
|
|
2232
|
+
// Track test counts per suite (for determining last test in suite)
|
|
2233
|
+
const suiteTestCounts = new Map();
|
|
2234
|
+
const suiteTotalTests = new Map();
|
|
2235
|
+
// Initialize suite test counts
|
|
2236
|
+
for (const suite of flattened.suites) {
|
|
2237
|
+
const suiteKey = getSuiteKey(suite.suitePath);
|
|
2238
|
+
suiteTotalTests.set(suiteKey, suite.testIndices.length);
|
|
2239
|
+
suiteTestCounts.set(suiteKey, 0);
|
|
2240
|
+
}
|
|
2241
|
+
// Execute each test
|
|
2242
|
+
for (let testIndex = 0; testIndex < flattened.tests.length; testIndex++) {
|
|
2243
|
+
const testData = flattened.tests[testIndex];
|
|
2244
|
+
const test = testData.test;
|
|
2245
|
+
const testStartTime = Date.now();
|
|
2246
|
+
// Use pre-assigned jobId when caller provides one (e.g. batch pre-create) and we have exactly one test
|
|
2247
|
+
const jobId = (request.jobId && flattened.tests.length === 1)
|
|
2248
|
+
? request.jobId
|
|
2249
|
+
: crypto.randomUUID();
|
|
2250
|
+
let testStatus = 'passed';
|
|
2251
|
+
let testError;
|
|
2252
|
+
try {
|
|
2253
|
+
// Before test execution: Execute suite-level beforeAll hooks (if first test in suite)
|
|
2254
|
+
if (testData.suitePath.length > 0) {
|
|
2255
|
+
// Execute beforeAll for each suite in the path (in order: parent → child)
|
|
2256
|
+
for (let i = 0; i < testData.suitePath.length; i++) {
|
|
2257
|
+
const suitePath = testData.suitePath.slice(0, i + 1);
|
|
2258
|
+
const suiteKey = getSuiteKey(suitePath);
|
|
2259
|
+
// Look up suite in Map (O(1) instead of O(n) .find())
|
|
2260
|
+
const suite = suiteMap.get(suiteKey);
|
|
2261
|
+
if (suite && suite.beforeAll.length > 0 && !suiteBeforeAllExecuted.has(suiteKey)) {
|
|
2262
|
+
// Use human-readable suite name for logging
|
|
2263
|
+
const suiteDisplayName = suitePath.join('__');
|
|
2264
|
+
this.log(`Executing suite-level beforeAll hooks for suite: ${suiteDisplayName}`);
|
|
2265
|
+
try {
|
|
2266
|
+
// Get or create suite context (includes file variables + suite variables)
|
|
2267
|
+
const suiteContext = await getOrCreateSuiteContext(suitePath, suite.suiteVariables, flattened.fileVariables);
|
|
2268
|
+
for (const hook of suite.beforeAll) {
|
|
2269
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook);
|
|
2321
2270
|
}
|
|
2271
|
+
suiteBeforeAllExecuted.add(suiteKey);
|
|
2272
|
+
this.log(`Suite-level beforeAll hooks executed for suite: ${suiteDisplayName}`);
|
|
2273
|
+
}
|
|
2274
|
+
catch (error) {
|
|
2275
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2276
|
+
this.log(`Suite-level beforeAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'error');
|
|
2277
|
+
testStatus = 'failed';
|
|
2278
|
+
testError = `Suite-level beforeAll hook failed: ${errorMsg}`;
|
|
2322
2279
|
}
|
|
2323
2280
|
}
|
|
2324
2281
|
}
|
|
2325
|
-
// Update step counter after test execution (whether success or failure)
|
|
2326
|
-
// Steps were executed up to the failure point
|
|
2327
|
-
testStepCounter += testStepsExecuted;
|
|
2328
2282
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2283
|
+
// Call onStartTest callback (use TestPathway)
|
|
2284
|
+
if (this.progressReporter?.onStartTest) {
|
|
2285
|
+
try {
|
|
2286
|
+
await this.progressReporter.onStartTest({ suitePath: test.suitePath || [], testName: test.name }, jobId, page, browser, context);
|
|
2287
|
+
}
|
|
2288
|
+
catch (callbackError) {
|
|
2289
|
+
this.log(`onStartTest callback failed: ${callbackError instanceof Error ? callbackError.message : String(callbackError)}`, 'warn');
|
|
2290
|
+
// Don't fail the test if callback has issues - runner-core is a library
|
|
2291
|
+
}
|
|
2334
2292
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2293
|
+
// Track step counter for sequential numeric step IDs across hooks and test
|
|
2294
|
+
let testStepCounter = 0;
|
|
2295
|
+
// Execute file-level beforeEach hooks
|
|
2296
|
+
if (flattened.fileLevelHooks.beforeEach.length > 0 && testStatus === 'passed') {
|
|
2297
|
+
this.log(`Executing ${flattened.fileLevelHooks.beforeEach.length} file-level beforeEach hook(s) for test: ${test.fullName}`);
|
|
2298
|
+
try {
|
|
2299
|
+
for (const hook of flattened.fileLevelHooks.beforeEach) {
|
|
2300
|
+
const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook, jobId, testStepCounter);
|
|
2301
|
+
testStepCounter += stepsExecuted;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
catch (error) {
|
|
2305
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2306
|
+
this.log(`File-level beforeEach hook failed for test ${test.fullName}: ${errorMsg}`, 'error');
|
|
2307
|
+
testStatus = 'failed';
|
|
2308
|
+
testError = `File-level beforeEach hook failed: ${errorMsg}`;
|
|
2345
2309
|
}
|
|
2346
2310
|
}
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
this.log(`
|
|
2350
|
-
|
|
2311
|
+
// Execute suite-level beforeEach hooks (from all parent suites, in order: parent → child)
|
|
2312
|
+
if (testData.suiteBeforeEachHooks.length > 0 && testStatus === 'passed') {
|
|
2313
|
+
this.log(`Executing ${testData.suiteBeforeEachHooks.length} suite-level beforeEach hook(s) for test: ${test.fullName}`);
|
|
2314
|
+
try {
|
|
2315
|
+
// Get or create suite context for this test's suite
|
|
2316
|
+
const suiteContext = await getOrCreateSuiteContext(testData.suitePath, testData.suiteVariables, flattened.fileVariables);
|
|
2317
|
+
for (const hook of testData.suiteBeforeEachHooks) {
|
|
2318
|
+
const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook, jobId, testStepCounter);
|
|
2319
|
+
testStepCounter += stepsExecuted;
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
catch (error) {
|
|
2323
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2324
|
+
this.log(`Suite-level beforeEach hook failed for test ${test.fullName}: ${errorMsg}`, 'error');
|
|
2325
|
+
testStatus = 'failed';
|
|
2326
|
+
testError = `Suite-level beforeEach hook failed: ${errorMsg}`;
|
|
2327
|
+
}
|
|
2351
2328
|
}
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2329
|
+
// Run the test if hooks didn't fail
|
|
2330
|
+
if (testStatus === 'passed') {
|
|
2331
|
+
// Count non-variable statements for step counter tracking (needed in catch block)
|
|
2332
|
+
const testStepsExecuted = test.statements.filter(stmt => !stmt.isVariableDeclaration).length;
|
|
2333
|
+
try {
|
|
2334
|
+
// Construct a script with imports + this test (needed for module resolution)
|
|
2335
|
+
// Variables are already in shared context, so we don't need to include them in script
|
|
2336
|
+
const testScript = test_file_parser_1.TestFileParser.constructTestScriptWithImports(originalScript, test.name, // Use test name only, not fullName (suite prefix not needed in generated script)
|
|
2337
|
+
test.code);
|
|
2338
|
+
// Use pre-parsed statements from initial file parse
|
|
2339
|
+
// These were extracted directly from the original AST, ensuring accuracy
|
|
2340
|
+
this.log(`Using ${test.statements.length} pre-parsed statements for test: ${test.fullName}`);
|
|
2341
|
+
// Count non-variable statements for step ID assignment
|
|
2342
|
+
let stepIndex = 0; // Track step index (only for non-variable statements)
|
|
2343
|
+
const steps = test.statements.map((stmt) => {
|
|
2344
|
+
// CRITICAL: Always use hash-based stepId from parser if available
|
|
2345
|
+
// Only assign step ID to non-variable statements
|
|
2346
|
+
const stepId = stmt.isVariableDeclaration
|
|
2347
|
+
? undefined
|
|
2348
|
+
: (stmt.stepId || String(testStepCounter + stepIndex++));
|
|
2349
|
+
// Log stepId assignment for debugging
|
|
2350
|
+
if (!stmt.isVariableDeclaration) {
|
|
2351
|
+
if (stmt.stepId) {
|
|
2352
|
+
this.log(`[stepId] Using hash-based stepId from parser: ${stmt.stepId.substring(0, 16)}... (test: ${test.fullName})`);
|
|
2353
|
+
}
|
|
2354
|
+
else {
|
|
2355
|
+
this.log(`[stepId] WARNING: No hash-based stepId from parser, using sequential: ${stepId} (test: ${test.fullName})`);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
return {
|
|
2359
|
+
id: stepId,
|
|
2360
|
+
code: stmt.code,
|
|
2361
|
+
description: stmt.intentComment || stmt.code.trim().substring(0, 100) || '',
|
|
2362
|
+
isVariableDeclaration: stmt.isVariableDeclaration, // Pass AST-based detection flag
|
|
2363
|
+
scenarioAnnotation: stmt.scenarioAnnotation
|
|
2364
|
+
};
|
|
2365
|
+
});
|
|
2366
|
+
// Get or create suite context for this test's suite
|
|
2367
|
+
const suiteContext = await getOrCreateSuiteContext(testData.suitePath, testData.suiteVariables, flattened.fileVariables);
|
|
2368
|
+
// Execute using the existing step-wise execution infrastructure
|
|
2369
|
+
// executeStepsInPersistentContext will:
|
|
2370
|
+
// 1. Use shared context (variables already initialized)
|
|
2371
|
+
// 2. Execute ALL statements sequentially (variables + actions)
|
|
2372
|
+
// 3. Variables execute silently and persist in context
|
|
2373
|
+
// 4. Non-variable statements trigger callbacks (onStepProgress)
|
|
2374
|
+
const result = await this.executeStepsInPersistentContext(steps, // Pre-parsed steps from initial parse with AST-based flags
|
|
2375
|
+
page, browser, context, {
|
|
2376
|
+
mode: request.mode || types_1.ExecutionMode.RUN_EXACTLY,
|
|
2377
|
+
jobId: jobId,
|
|
2378
|
+
tempDir: request.tempDir,
|
|
2379
|
+
originalScript: testScript, // For module resolution (imports)
|
|
2380
|
+
model: request.model,
|
|
2381
|
+
testFolderPath: request.testFolderPath,
|
|
2382
|
+
runPomDenormalized: request.runPomDenormalized
|
|
2383
|
+
}, suiteContext // Pass shared context
|
|
2384
|
+
);
|
|
2385
|
+
if (!result.success) {
|
|
2386
|
+
testStatus = 'failed';
|
|
2387
|
+
testError = result.error || 'Test execution failed';
|
|
2388
|
+
}
|
|
2389
|
+
// Track repair if we're in repair mode and steps were updated
|
|
2390
|
+
// Note: updatedSteps are always returned, but in repair mode they may contain repaired code
|
|
2391
|
+
if (request.mode === types_1.ExecutionMode.RUN_WITH_AI_REPAIR && result.updatedSteps && result.updatedSteps.length > 0) {
|
|
2392
|
+
// Generate updated script from steps (will include repairs if any occurred)
|
|
2393
|
+
const updatedScript = this.generateUpdatedScript(result.updatedSteps, undefined, testScript);
|
|
2394
|
+
if (updatedScript) {
|
|
2395
|
+
// Extract the test body from the generated script
|
|
2396
|
+
const repairedTestBody = test_file_parser_1.TestFileParser.extractTestBodyFromScript(updatedScript);
|
|
2397
|
+
if (repairedTestBody) {
|
|
2398
|
+
// Compare with original test body to confirm it's actually different (repairs occurred)
|
|
2399
|
+
const originalTestBody = test.code.trim();
|
|
2400
|
+
const repairedTestBodyTrimmed = repairedTestBody.trim();
|
|
2401
|
+
if (repairedTestBodyTrimmed !== originalTestBody) {
|
|
2402
|
+
// Create TestPathway key (JSON stringified for Map lookup)
|
|
2403
|
+
const testPathwayKey = JSON.stringify({
|
|
2404
|
+
suitePath: test.suitePath || [],
|
|
2405
|
+
testName: test.name.trim()
|
|
2406
|
+
});
|
|
2407
|
+
testRepairs.set(testPathwayKey, repairedTestBody);
|
|
2408
|
+
this.log(`Tracked repair for test: ${test.fullName} (pathway: ${testPathwayKey})`);
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
// Update step counter after test execution (whether success or failure)
|
|
2414
|
+
// Steps were executed up to the failure point
|
|
2415
|
+
testStepCounter += testStepsExecuted;
|
|
2416
|
+
}
|
|
2417
|
+
catch (error) {
|
|
2418
|
+
testStatus = 'failed';
|
|
2419
|
+
testError = error instanceof Error ? error.message : String(error);
|
|
2420
|
+
// Still update step counter even if test failed (steps were executed up to failure point)
|
|
2421
|
+
testStepCounter += testStepsExecuted;
|
|
2360
2422
|
}
|
|
2361
2423
|
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
this.log(`
|
|
2365
|
-
|
|
2424
|
+
// Execute suite-level afterEach hooks (from all parent suites, in reverse order: child → parent)
|
|
2425
|
+
if (testData.suiteAfterEachHooks.length > 0) {
|
|
2426
|
+
this.log(`Executing ${testData.suiteAfterEachHooks.length} suite-level afterEach hook(s) for test: ${test.fullName}`);
|
|
2427
|
+
try {
|
|
2428
|
+
// Get or create suite context for this test's suite
|
|
2429
|
+
const suiteContext = await getOrCreateSuiteContext(testData.suitePath, testData.suiteVariables, flattened.fileVariables);
|
|
2430
|
+
for (const hook of testData.suiteAfterEachHooks) {
|
|
2431
|
+
const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook, jobId, testStepCounter);
|
|
2432
|
+
testStepCounter += stepsExecuted;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
catch (error) {
|
|
2436
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2437
|
+
this.log(`Suite-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
|
|
2438
|
+
// Log but continue - don't fail the test because of afterEach failure
|
|
2439
|
+
}
|
|
2366
2440
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2441
|
+
// Execute file-level afterEach hooks (even if test failed)
|
|
2442
|
+
if (flattened.fileLevelHooks.afterEach.length > 0) {
|
|
2443
|
+
this.log(`Executing ${flattened.fileLevelHooks.afterEach.length} file-level afterEach hook(s) for test: ${test.fullName}`);
|
|
2444
|
+
try {
|
|
2445
|
+
for (const hook of flattened.fileLevelHooks.afterEach) {
|
|
2446
|
+
const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook, jobId, testStepCounter);
|
|
2447
|
+
testStepCounter += stepsExecuted;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
catch (error) {
|
|
2451
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2452
|
+
this.log(`File-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
|
|
2453
|
+
// Log but continue - don't fail the test because of afterEach failure
|
|
2454
|
+
}
|
|
2376
2455
|
}
|
|
2377
|
-
//
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
if (suite) {
|
|
2384
|
-
const totalTests = suiteTotalTests.get(suiteKey) || 0;
|
|
2456
|
+
// After test execution: Increment test counts and execute suite-level afterAll hooks (if last test in suite)
|
|
2457
|
+
if (testData.suitePath.length > 0) {
|
|
2458
|
+
// Increment test count for each suite in the path
|
|
2459
|
+
for (let i = 0; i < testData.suitePath.length; i++) {
|
|
2460
|
+
const suitePath = testData.suitePath.slice(0, i + 1);
|
|
2461
|
+
const suiteKey = getSuiteKey(suitePath);
|
|
2385
2462
|
const currentCount = suiteTestCounts.get(suiteKey) || 0;
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2463
|
+
suiteTestCounts.set(suiteKey, currentCount + 1);
|
|
2464
|
+
}
|
|
2465
|
+
// Execute afterAll for each suite in the path (in reverse order: child → parent)
|
|
2466
|
+
for (let i = testData.suitePath.length - 1; i >= 0; i--) {
|
|
2467
|
+
const suitePath = testData.suitePath.slice(0, i + 1);
|
|
2468
|
+
const suiteKey = getSuiteKey(suitePath);
|
|
2469
|
+
// Look up suite in Map (O(1) instead of O(n) .find())
|
|
2470
|
+
const suite = suiteMap.get(suiteKey);
|
|
2471
|
+
if (suite) {
|
|
2472
|
+
const totalTests = suiteTotalTests.get(suiteKey) || 0;
|
|
2473
|
+
const currentCount = suiteTestCounts.get(suiteKey) || 0;
|
|
2474
|
+
// If this is the last test in the suite, execute afterAll (if any) and dispose context
|
|
2475
|
+
if (currentCount >= totalTests) {
|
|
2476
|
+
// Use human-readable suite name for logging
|
|
2477
|
+
const suiteDisplayName = suitePath.join('__');
|
|
2478
|
+
// Execute afterAll hooks if they exist
|
|
2479
|
+
if (suite.afterAll.length > 0) {
|
|
2480
|
+
this.log(`Executing suite-level afterAll hooks for suite: ${suiteDisplayName}`);
|
|
2481
|
+
try {
|
|
2482
|
+
// Get or create suite context (should already exist, but safe to call)
|
|
2483
|
+
const suiteContext = await getOrCreateSuiteContext(suitePath, suite.suiteVariables, flattened.fileVariables);
|
|
2484
|
+
for (const hook of suite.afterAll) {
|
|
2485
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook);
|
|
2486
|
+
}
|
|
2487
|
+
this.log(`Suite-level afterAll hooks executed for suite: ${suiteDisplayName}`);
|
|
2488
|
+
}
|
|
2489
|
+
catch (error) {
|
|
2490
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2491
|
+
this.log(`Suite-level afterAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'warn');
|
|
2492
|
+
// Log but continue - don't fail the test because of afterAll failure
|
|
2398
2493
|
}
|
|
2399
|
-
this.log(`Suite-level afterAll hooks executed for suite: ${suiteDisplayName}`);
|
|
2400
|
-
}
|
|
2401
|
-
catch (error) {
|
|
2402
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2403
|
-
this.log(`Suite-level afterAll hook failed for suite ${suiteDisplayName}: ${errorMsg}`, 'warn');
|
|
2404
|
-
// Log but continue - don't fail the test because of afterAll failure
|
|
2405
2494
|
}
|
|
2495
|
+
// Dispose suite context after afterAll hooks complete (or if no afterAll hooks)
|
|
2496
|
+
// This ensures contexts are always disposed, even for suites without afterAll hooks
|
|
2497
|
+
disposeSuiteContext(suitePath);
|
|
2406
2498
|
}
|
|
2407
|
-
// Dispose suite context after afterAll hooks complete (or if no afterAll hooks)
|
|
2408
|
-
// This ensures contexts are always disposed, even for suites without afterAll hooks
|
|
2409
|
-
disposeSuiteContext(suitePath);
|
|
2410
2499
|
}
|
|
2411
2500
|
}
|
|
2412
2501
|
}
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2502
|
+
// Call onEndTest callback (use TestPathway)
|
|
2503
|
+
if (this.progressReporter?.onEndTest) {
|
|
2504
|
+
try {
|
|
2505
|
+
await this.progressReporter.onEndTest({ suitePath: test.suitePath || [], testName: test.name }, jobId, testStatus, testError, page, browser, context);
|
|
2506
|
+
}
|
|
2507
|
+
catch (callbackError) {
|
|
2508
|
+
this.log(`onEndTest callback failed: ${callbackError instanceof Error ? callbackError.message : String(callbackError)}`, 'warn');
|
|
2509
|
+
// Don't fail the test if callback has issues - runner-core is a library
|
|
2510
|
+
}
|
|
2418
2511
|
}
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2512
|
+
}
|
|
2513
|
+
catch (error) {
|
|
2514
|
+
// Unexpected error during test execution
|
|
2515
|
+
testStatus = 'failed';
|
|
2516
|
+
testError = error instanceof Error ? error.message : String(error);
|
|
2517
|
+
this.log(`Unexpected error during test ${test.name}: ${testError}`, 'error');
|
|
2518
|
+
// Still call onEndTest if possible
|
|
2519
|
+
if (this.progressReporter?.onEndTest) {
|
|
2520
|
+
try {
|
|
2521
|
+
await this.progressReporter.onEndTest({ suitePath: test.suitePath || [], testName: test.name }, jobId, testStatus, testError, page, browser, context);
|
|
2522
|
+
}
|
|
2523
|
+
catch (callbackError) {
|
|
2524
|
+
this.log(`onEndTest callback failed: ${callbackError}`, 'warn');
|
|
2525
|
+
}
|
|
2422
2526
|
}
|
|
2423
2527
|
}
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2528
|
+
// Record test result (use TestPathway)
|
|
2529
|
+
testResults.push({
|
|
2530
|
+
testPathway: { suitePath: test.suitePath || [], testName: test.name },
|
|
2531
|
+
jobId: jobId,
|
|
2532
|
+
status: testStatus,
|
|
2533
|
+
error: testError,
|
|
2534
|
+
executionTime: Date.now() - testStartTime
|
|
2535
|
+
});
|
|
2536
|
+
this.log(`Test ${test.fullName} completed with status: ${testStatus}`);
|
|
2537
|
+
// Call afterEndTest callback if provided (called after each test completes)
|
|
2538
|
+
// This allows each test in a file to be treated as a separate journey
|
|
2539
|
+
if (request.afterEndTest) {
|
|
2432
2540
|
try {
|
|
2433
|
-
|
|
2541
|
+
const callbackStatus = testStatus === 'passed' ? 'passed' : 'failed';
|
|
2542
|
+
await request.afterEndTest(callbackStatus, testError, page, undefined, undefined);
|
|
2543
|
+
this.log(`afterEndTest callback completed for test ${test.fullName}`);
|
|
2434
2544
|
}
|
|
2435
2545
|
catch (callbackError) {
|
|
2436
|
-
this.log(`
|
|
2546
|
+
this.log(`afterEndTest callback failed for test ${test.fullName}: ${callbackError}`, 'warn');
|
|
2547
|
+
// Don't fail the test execution if callback has issues
|
|
2437
2548
|
}
|
|
2438
2549
|
}
|
|
2439
2550
|
}
|
|
2440
|
-
//
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
jobId: jobId,
|
|
2444
|
-
status: testStatus,
|
|
2445
|
-
error: testError,
|
|
2446
|
-
executionTime: Date.now() - testStartTime
|
|
2447
|
-
});
|
|
2448
|
-
this.log(`Test ${test.fullName} completed with status: ${testStatus}`);
|
|
2449
|
-
// Call afterEndTest callback if provided (called after each test completes)
|
|
2450
|
-
// This allows each test in a file to be treated as a separate journey
|
|
2451
|
-
if (request.afterEndTest) {
|
|
2551
|
+
// Execute file-level afterAll hooks
|
|
2552
|
+
if (flattened.fileLevelHooks.afterAll.length > 0) {
|
|
2553
|
+
this.log(`Executing ${flattened.fileLevelHooks.afterAll.length} file-level afterAll hook(s)...`);
|
|
2452
2554
|
try {
|
|
2453
|
-
const
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
catch (callbackError) {
|
|
2458
|
-
this.log(`afterEndTest callback failed for test ${test.fullName}: ${callbackError}`, 'warn');
|
|
2459
|
-
// Don't fail the test execution if callback has issues
|
|
2555
|
+
for (const hook of flattened.fileLevelHooks.afterAll) {
|
|
2556
|
+
await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook);
|
|
2557
|
+
}
|
|
2558
|
+
this.log('File-level afterAll hooks executed successfully');
|
|
2460
2559
|
}
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
this.log(`Executing ${flattened.fileLevelHooks.afterAll.length} file-level afterAll hook(s)...`);
|
|
2466
|
-
try {
|
|
2467
|
-
for (const hook of flattened.fileLevelHooks.afterAll) {
|
|
2468
|
-
await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook);
|
|
2560
|
+
catch (error) {
|
|
2561
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2562
|
+
this.log(`File-level afterAll hook failed: ${errorMsg}`, 'warn');
|
|
2563
|
+
// Log but don't fail overall execution
|
|
2469
2564
|
}
|
|
2470
|
-
this.log('File-level afterAll hooks executed successfully');
|
|
2471
|
-
}
|
|
2472
|
-
catch (error) {
|
|
2473
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2474
|
-
this.log(`File-level afterAll hook failed: ${errorMsg}`, 'warn');
|
|
2475
|
-
// Log but don't fail overall execution
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
// Dispose file-level context after file-level afterAll hooks
|
|
2479
|
-
if (fileLevelContext !== null && fileLevelContext !== undefined) {
|
|
2480
|
-
fileLevelContext.dispose();
|
|
2481
|
-
fileLevelContext = null;
|
|
2482
|
-
}
|
|
2483
|
-
// Cleanup browser if we created it
|
|
2484
|
-
if (browserCreated && browser) {
|
|
2485
|
-
try {
|
|
2486
|
-
await browser.close();
|
|
2487
|
-
this.log('Browser closed');
|
|
2488
2565
|
}
|
|
2489
|
-
|
|
2490
|
-
|
|
2566
|
+
// Dispose file-level context after file-level afterAll hooks
|
|
2567
|
+
if (fileLevelContext !== null && fileLevelContext !== undefined) {
|
|
2568
|
+
fileLevelContext.dispose();
|
|
2569
|
+
fileLevelContext = null;
|
|
2491
2570
|
}
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
this.log('Test file reconstruction completed successfully');
|
|
2571
|
+
// Cleanup browser if we created it
|
|
2572
|
+
if (browserCreated && browser) {
|
|
2573
|
+
try {
|
|
2574
|
+
await browser.close();
|
|
2575
|
+
this.log('Browser closed');
|
|
2576
|
+
}
|
|
2577
|
+
catch (closeError) {
|
|
2578
|
+
this.log(`Error closing browser: ${closeError}`, 'warn');
|
|
2579
|
+
}
|
|
2502
2580
|
}
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2581
|
+
// Determine overall success (all tests passed)
|
|
2582
|
+
const allPassed = testResults.every(result => result.status === 'passed');
|
|
2583
|
+
// Reconstruct the full test file with repairs if any tests were repaired
|
|
2584
|
+
let updatedScript;
|
|
2585
|
+
if (testRepairs.size > 0) {
|
|
2586
|
+
this.log(`Reconstructing test file with ${testRepairs.size} repaired test(s)...`);
|
|
2587
|
+
try {
|
|
2588
|
+
updatedScript = test_file_parser_1.TestFileParser.reconstructTestFileWithRepairs(originalScript, parsed, testRepairs);
|
|
2589
|
+
this.log('Test file reconstruction completed successfully');
|
|
2590
|
+
}
|
|
2591
|
+
catch (error) {
|
|
2592
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2593
|
+
this.log(`Failed to reconstruct test file: ${errorMsg}`, 'warn');
|
|
2594
|
+
// Continue without updatedScript - original script will be used
|
|
2595
|
+
}
|
|
2507
2596
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
};
|
|
2597
|
+
return {
|
|
2598
|
+
success: allPassed,
|
|
2599
|
+
testResults: testResults,
|
|
2600
|
+
executionTime: Date.now() - startTime,
|
|
2601
|
+
updatedScript: updatedScript
|
|
2602
|
+
};
|
|
2603
|
+
});
|
|
2515
2604
|
}
|
|
2516
2605
|
catch (error) {
|
|
2517
2606
|
const errorMsg = error instanceof Error ? error.message : String(error);
|