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
- // Create suite context manager - one context per suite, shared across all hooks and tests
1997
- const suiteContexts = new Map();
1998
- // Helper to get or create suite context
1999
- const getOrCreateSuiteContext = async (suitePath, suiteVariables, fileVariables) => {
2000
- const suiteKey = JSON.stringify(suitePath);
2001
- if (!suiteContexts.has(suiteKey)) {
2002
- const { expect, test } = require('@playwright/test');
2003
- const { ai } = require('ai-wright');
2004
- const suiteContext = new PersistentExecutionContext(page, expect, test, ai, browser, context, request.tempDir);
2005
- suiteContexts.set(suiteKey, { context: suiteContext, variablesInitialized: false });
2006
- }
2007
- const suiteContextData = suiteContexts.get(suiteKey);
2008
- if (!suiteContextData.variablesInitialized) {
2009
- try {
2010
- // Set testFileDir on context for module resolution (needed before executing requires)
2011
- // Use testFolderPath from request if available, otherwise default to tempDir/tests
2012
- const testFileDir = request.testFolderPath && request.testFolderPath.length > 0
2013
- ? path.join(request.tempDir, ...request.testFolderPath)
2014
- : path.join(request.tempDir, 'tests');
2015
- suiteContextData.context.setTestFileDir(testFileDir);
2016
- // Load .env inside VM before any user code so process.env is available in test and POM code
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);
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
- // 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);
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
- 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
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
- 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);
2057
- }
2058
- };
2059
- // Create file-level shared context for file-level hooks
2060
- let fileLevelContext = null;
2061
- const initializeFileLevelContext = async () => {
2062
- if (!fileLevelContext && (flattened.fileVariables.length > 0 || flattened.fileLevelHooks.beforeAll.length > 0)) {
2063
- const { expect, test } = require('@playwright/test');
2064
- const { ai } = require('ai-wright');
2065
- fileLevelContext = new PersistentExecutionContext(page, expect, test, ai, browser, context, request.tempDir);
2066
- try {
2067
- // Set testFileDir on context for module resolution (needed before executing requires)
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);
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
- // Initialize file-level variables
2088
- if (flattened.fileVariables.length > 0) {
2089
- const fileVarsCode = flattened.fileVariables.join('\n');
2090
- await fileLevelContext.executeCode(fileVarsCode);
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 variable initialization failed: ${errorMsg}`, 'warn');
2096
- // Continue - variables may be optional
2097
- }
2098
- }
2099
- };
2100
- // Initialize file-level context before file-level hooks
2101
- await initializeFileLevelContext();
2102
- // Execute file-level beforeAll hooks
2103
- if (flattened.fileLevelHooks.beforeAll.length > 0) {
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
- // Helper function to create a unique key from suite path (avoids collisions if suite names contain '__')
2132
- const getSuiteKey = (suitePath) => {
2133
- // Use JSON.stringify to safely serialize the array, avoiding collisions
2134
- return JSON.stringify(suitePath);
2135
- };
2136
- // Build a Map of suites by key for O(1) lookups (instead of O(n) .find() calls)
2137
- const suiteMap = new Map();
2138
- for (const suite of flattened.suites) {
2139
- const suiteKey = getSuiteKey(suite.suitePath);
2140
- suiteMap.set(suiteKey, suite);
2141
- }
2142
- // Track suite execution state (which suites have had beforeAll executed)
2143
- const suiteBeforeAllExecuted = new Set();
2144
- // Track test counts per suite (for determining last test in suite)
2145
- const suiteTestCounts = new Map();
2146
- const suiteTotalTests = new Map();
2147
- // Initialize suite test counts
2148
- for (const suite of flattened.suites) {
2149
- const suiteKey = getSuiteKey(suite.suitePath);
2150
- suiteTotalTests.set(suiteKey, suite.testIndices.length);
2151
- suiteTestCounts.set(suiteKey, 0);
2152
- }
2153
- // Execute each test
2154
- for (let testIndex = 0; testIndex < flattened.tests.length; testIndex++) {
2155
- const testData = flattened.tests[testIndex];
2156
- const test = testData.test;
2157
- const testStartTime = Date.now();
2158
- // Use pre-assigned jobId when caller provides one (e.g. batch pre-create) and we have exactly one test
2159
- const jobId = (request.jobId && flattened.tests.length === 1)
2160
- ? request.jobId
2161
- : crypto.randomUUID();
2162
- let testStatus = 'passed';
2163
- let testError;
2164
- try {
2165
- // Before test execution: Execute suite-level beforeAll hooks (if first test in suite)
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);
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
- // Call onStartTest callback (use TestPathway)
2196
- if (this.progressReporter?.onStartTest) {
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;
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
- catch (error) {
2217
- const errorMsg = error instanceof Error ? error.message : String(error);
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
- catch (error) {
2235
- const errorMsg = error instanceof Error ? error.message : String(error);
2236
- this.log(`Suite-level beforeEach hook failed for test ${test.fullName}: ${errorMsg}`, 'error');
2237
- testStatus = 'failed';
2238
- testError = `Suite-level beforeEach hook failed: ${errorMsg}`;
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
- // Run the test if hooks didn't fail
2242
- if (testStatus === 'passed') {
2243
- // Count non-variable statements for step counter tracking (needed in catch block)
2244
- const testStepsExecuted = test.statements.filter(stmt => !stmt.isVariableDeclaration).length;
2245
- try {
2246
- // Construct a script with imports + this test (needed for module resolution)
2247
- // Variables are already in shared context, so we don't need to include them in script
2248
- const testScript = test_file_parser_1.TestFileParser.constructTestScriptWithImports(originalScript, test.name, // Use test name only, not fullName (suite prefix not needed in generated script)
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
- 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) {
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 = result.error || 'Test execution failed';
2256
+ testError = `Suite-level beforeEach hook failed: ${errorMsg}`;
2300
2257
  }
2301
- // Track repair if we're in repair mode and steps were updated
2302
- // Note: updatedSteps are always returned, but in repair mode they may contain repaired code
2303
- if (request.mode === types_1.ExecutionMode.RUN_WITH_AI_REPAIR && result.updatedSteps && result.updatedSteps.length > 0) {
2304
- // Generate updated script from steps (will include repairs if any occurred)
2305
- const updatedScript = this.generateUpdatedScript(result.updatedSteps, undefined, testScript);
2306
- if (updatedScript) {
2307
- // Extract the test body from the generated script
2308
- const repairedTestBody = test_file_parser_1.TestFileParser.extractTestBodyFromScript(updatedScript);
2309
- if (repairedTestBody) {
2310
- // Compare with original test body to confirm it's actually different (repairs occurred)
2311
- const originalTestBody = test.code.trim();
2312
- const repairedTestBodyTrimmed = repairedTestBody.trim();
2313
- if (repairedTestBodyTrimmed !== originalTestBody) {
2314
- // Create TestPathway key (JSON stringified for Map lookup)
2315
- const testPathwayKey = JSON.stringify({
2316
- suitePath: test.suitePath || [],
2317
- testName: test.name.trim()
2318
- });
2319
- testRepairs.set(testPathwayKey, repairedTestBody);
2320
- this.log(`Tracked repair for test: ${test.fullName} (pathway: ${testPathwayKey})`);
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
- // Update step counter after test execution (whether success or failure)
2326
- // Steps were executed up to the failure point
2327
- testStepCounter += testStepsExecuted;
2328
- }
2329
- catch (error) {
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
- catch (error) {
2348
- const errorMsg = error instanceof Error ? error.message : String(error);
2349
- this.log(`Suite-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
2350
- // Log but continue - don't fail the test because of afterEach failure
2351
- }
2352
- }
2353
- // Execute file-level afterEach hooks (even if test failed)
2354
- if (flattened.fileLevelHooks.afterEach.length > 0) {
2355
- this.log(`Executing ${flattened.fileLevelHooks.afterEach.length} file-level afterEach hook(s) for test: ${test.fullName}`);
2356
- try {
2357
- for (const hook of flattened.fileLevelHooks.afterEach) {
2358
- const stepsExecuted = await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, fileLevelContext || undefined, hook, jobId, testStepCounter);
2359
- testStepCounter += stepsExecuted;
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
- catch (error) {
2363
- const errorMsg = error instanceof Error ? error.message : String(error);
2364
- this.log(`File-level afterEach hook failed for test ${test.fullName}: ${errorMsg}`, 'warn');
2365
- // Log but continue - don't fail the test because of afterEach failure
2366
- }
2367
- }
2368
- // After test execution: Increment test counts and execute suite-level afterAll hooks (if last test in suite)
2369
- if (testData.suitePath.length > 0) {
2370
- // Increment test count for each suite in the path
2371
- for (let i = 0; i < testData.suitePath.length; i++) {
2372
- const suitePath = testData.suitePath.slice(0, i + 1);
2373
- const suiteKey = getSuiteKey(suitePath);
2374
- const currentCount = suiteTestCounts.get(suiteKey) || 0;
2375
- suiteTestCounts.set(suiteKey, currentCount + 1);
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
- // Execute afterAll for each suite in the path (in reverse order: child → parent)
2378
- for (let i = testData.suitePath.length - 1; i >= 0; i--) {
2379
- const suitePath = testData.suitePath.slice(0, i + 1);
2380
- const suiteKey = getSuiteKey(suitePath);
2381
- // Look up suite in Map (O(1) instead of O(n) .find())
2382
- const suite = suiteMap.get(suiteKey);
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
- // If this is the last test in the suite, execute afterAll (if any) and dispose context
2387
- if (currentCount >= totalTests) {
2388
- // Use human-readable suite name for logging
2389
- const suiteDisplayName = suitePath.join('__');
2390
- // Execute afterAll hooks if they exist
2391
- if (suite.afterAll.length > 0) {
2392
- this.log(`Executing suite-level afterAll hooks for suite: ${suiteDisplayName}`);
2393
- try {
2394
- // Get or create suite context (should already exist, but safe to call)
2395
- const suiteContext = await getOrCreateSuiteContext(suitePath, suite.suiteVariables, flattened.fileVariables);
2396
- for (const hook of suite.afterAll) {
2397
- await this.executeHook(hook.code, page, context, browser, request.tempDir, originalScript, request.testFolderPath, request.runPomDenormalized, suiteContext, hook);
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
- // Call onEndTest callback (use TestPathway)
2415
- if (this.progressReporter?.onEndTest) {
2416
- try {
2417
- await this.progressReporter.onEndTest({ suitePath: test.suitePath || [], testName: test.name }, jobId, testStatus, testError, page, browser, context);
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
- catch (callbackError) {
2420
- this.log(`onEndTest callback failed: ${callbackError instanceof Error ? callbackError.message : String(callbackError)}`, 'warn');
2421
- // Don't fail the test if callback has issues - runner-core is a library
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
- catch (error) {
2426
- // Unexpected error during test execution
2427
- testStatus = 'failed';
2428
- testError = error instanceof Error ? error.message : String(error);
2429
- this.log(`Unexpected error during test ${test.name}: ${testError}`, 'error');
2430
- // Still call onEndTest if possible
2431
- if (this.progressReporter?.onEndTest) {
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
- await this.progressReporter.onEndTest({ suitePath: test.suitePath || [], testName: test.name }, jobId, testStatus, testError, page, browser, context);
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(`onEndTest callback failed: ${callbackError}`, 'warn');
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
- // Record test result (use TestPathway)
2441
- testResults.push({
2442
- testPathway: { suitePath: test.suitePath || [], testName: test.name },
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 callbackStatus = testStatus === 'passed' ? 'passed' : 'failed';
2454
- await request.afterEndTest(callbackStatus, testError, page, undefined, undefined);
2455
- this.log(`afterEndTest callback completed for test ${test.fullName}`);
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
- // Execute file-level afterAll hooks
2464
- if (flattened.fileLevelHooks.afterAll.length > 0) {
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
- 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');
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
- catch (closeError) {
2490
- this.log(`Error closing browser: ${closeError}`, 'warn');
2491
- }
2492
- }
2493
- // Determine overall success (all tests passed)
2494
- const allPassed = testResults.every(result => result.status === 'passed');
2495
- // Reconstruct the full test file with repairs if any tests were repaired
2496
- let updatedScript;
2497
- if (testRepairs.size > 0) {
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
- catch (error) {
2504
- const errorMsg = error instanceof Error ? error.message : String(error);
2505
- this.log(`Failed to reconstruct test file: ${errorMsg}`, 'warn');
2506
- // Continue without updatedScript - original script will be used
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
- return {
2510
- success: allPassed,
2511
- testResults: testResults,
2512
- executionTime: Date.now() - startTime,
2513
- updatedScript: updatedScript
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);