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