diff --git a/e2e/browser-mode/fixtures/github-actions/rstest.config.ts b/e2e/browser-mode/fixtures/github-actions/rstest.config.ts new file mode 100644 index 000000000..1bca4b586 --- /dev/null +++ b/e2e/browser-mode/fixtures/github-actions/rstest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from '@rstest/core'; +import { BROWSER_PORTS } from '../ports'; + +export default defineConfig({ + reporters: ['github-actions'], + projects: [ + { + name: 'browser', + browser: { + enabled: true, + provider: 'playwright', + headless: true, + port: BROWSER_PORTS['github-actions'], + }, + include: ['tests/browser/**/*.test.ts'], + }, + { + name: 'node', + include: ['tests/node/**/*.test.ts'], + }, + ], +}); diff --git a/e2e/browser-mode/fixtures/github-actions/tests/browser/failing.test.ts b/e2e/browser-mode/fixtures/github-actions/tests/browser/failing.test.ts new file mode 100644 index 000000000..56b5a148c --- /dev/null +++ b/e2e/browser-mode/fixtures/github-actions/tests/browser/failing.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from '@rstest/core'; + +describe('browser failing test', () => { + it('should fail in browser', () => { + expect('4').toBe('41'); + }); +}); diff --git a/e2e/browser-mode/fixtures/github-actions/tests/node/passing.test.ts b/e2e/browser-mode/fixtures/github-actions/tests/node/passing.test.ts new file mode 100644 index 000000000..3417ae39b --- /dev/null +++ b/e2e/browser-mode/fixtures/github-actions/tests/node/passing.test.ts @@ -0,0 +1,7 @@ +import { describe, expect, it } from '@rstest/core'; + +describe('node passing test', () => { + it('should pass in node', () => { + expect(1).toBe(1); + }); +}); diff --git a/e2e/browser-mode/fixtures/ports.ts b/e2e/browser-mode/fixtures/ports.ts index a08bfb217..a60767b62 100644 --- a/e2e/browser-mode/fixtures/ports.ts +++ b/e2e/browser-mode/fixtures/ports.ts @@ -20,4 +20,5 @@ export const BROWSER_PORTS = { 'viewport-preset': 5216, reporter: 5220, 'reporter-watch': 5222, + 'github-actions': 5224, } as const; diff --git a/e2e/browser-mode/githubActions.test.ts b/e2e/browser-mode/githubActions.test.ts new file mode 100644 index 000000000..70dc3f9b9 --- /dev/null +++ b/e2e/browser-mode/githubActions.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from '@rstest/core'; +import { runBrowserCli } from './utils'; + +describe('browser mode - github-actions reporter', () => { + it('should annotate browser failures with test source path', async () => { + const { expectExecFailed, cli } = await runBrowserCli('github-actions'); + + await expectExecFailed(); + + const logs = cli.stdout + .split('\n') + .filter(Boolean) + .filter((log) => log.startsWith('::error')); + + expect(logs.length).toBeGreaterThan(0); + + const browserFailure = + logs.find((log) => log.includes('browser failing test')) || logs[0]!; + + expect(browserFailure).toContain('tests/browser/failing.test.ts'); + expect(browserFailure).not.toContain('http://localhost'); + }); +}); diff --git a/packages/browser/src/client/sourceMapSupport.ts b/packages/browser/src/client/sourceMapSupport.ts index 04d6474fa..dd752828f 100644 --- a/packages/browser/src/client/sourceMapSupport.ts +++ b/packages/browser/src/client/sourceMapSupport.ts @@ -1,8 +1,13 @@ import { originalPositionFor, TraceMap } from '@jridgewell/trace-mapping'; -import convert from 'convert-source-map'; +import { + loadSourceMapWithCache, + normalizeJavaScriptUrl, + type SourceMapPayload, +} from '../sourceMap/sourceMapLoader'; // Source map cache: JS URL → TraceMap const sourceMapCache = new Map(); +const sourceMapPayloadCache = new Map(); /** * Get TraceMap for specified URL (sync cache lookup) @@ -23,40 +28,23 @@ const preloadSourceMap = async ( jsUrl: string, force = false, ): Promise => { - if (!force && sourceMapCache.has(jsUrl)) return; - - try { - // First, fetch JS file and try to extract inline source map - const jsResponse = await fetch(jsUrl); - if (!jsResponse.ok) { - sourceMapCache.set(jsUrl, null); - return; - } - - const code = await jsResponse.text(); + const normalizedUrl = normalizeJavaScriptUrl(jsUrl, { + origin: window.location.origin, + }); + if (!normalizedUrl) { + return; + } - // Try to extract inline source map using convert-source-map - const inlineConverter = convert.fromSource(code); - if (inlineConverter) { - const mapObject = inlineConverter.toObject(); - sourceMapCache.set(jsUrl, new TraceMap(mapObject)); - return; - } + if (!force && sourceMapCache.has(normalizedUrl)) return; - // Fallback: try to fetch external .map file - const mapUrl = `${jsUrl}.map`; - const mapResponse = await fetch(mapUrl); - if (mapResponse.ok) { - const mapJson = await mapResponse.json(); - sourceMapCache.set(jsUrl, new TraceMap(mapJson)); - return; - } + const sourceMap = await loadSourceMapWithCache({ + jsUrl: normalizedUrl, + cache: sourceMapPayloadCache, + force, + origin: window.location.origin, + }); - // No source map found - sourceMapCache.set(jsUrl, null); - } catch { - sourceMapCache.set(jsUrl, null); - } + sourceMapCache.set(normalizedUrl, sourceMap ? new TraceMap(sourceMap) : null); }; /** @@ -128,6 +116,7 @@ export const preloadRunnerSourceMap = async (): Promise => { */ export const clearCache = (): void => { sourceMapCache.clear(); + sourceMapPayloadCache.clear(); }; /** @@ -147,11 +136,11 @@ export interface StackFrame { export const mapStackFrame = (frame: StackFrame): StackFrame => { const { file, line, column } = frame; - // Normalize file path to full URL for cache lookup - let fullUrl = file; - if (!file.startsWith('http://') && !file.startsWith('https://')) { - // Convert relative path to full URL - fullUrl = `${window.location.origin}${file.startsWith('/') ? '' : '/'}${file}`; + const fullUrl = normalizeJavaScriptUrl(file, { + origin: window.location.origin, + }); + if (!fullUrl) { + return frame; } const traceMap = getSourceMap(fullUrl); diff --git a/packages/browser/src/hostController.ts b/packages/browser/src/hostController.ts index ea28463ad..7b7a50571 100644 --- a/packages/browser/src/hostController.ts +++ b/packages/browser/src/hostController.ts @@ -57,6 +57,11 @@ import { RunSessionLifecycle, } from './runSession'; import { RunnerSessionRegistry } from './sessionRegistry'; +import { + loadSourceMapWithCache, + normalizeJavaScriptUrl, + type SourceMapPayload, +} from './sourceMap/sourceMapLoader'; import { resolveBrowserViewportPreset } from './viewportPresets'; import { collectWatchTestFiles, planWatchRerun } from './watchRerunPlanner'; @@ -1346,20 +1351,67 @@ export const runBrowserController = async ( (project) => project.normalizedConfig.browser.headless, ); + const browserSourceMapCache = new Map(); + + const isHttpLikeFile = (file: string): boolean => /^https?:\/\//.test(file); + + const resolveBrowserSourcemap = async (sourcePath: string) => { + if (!isHttpLikeFile(sourcePath)) { + return { + handled: false, + sourcemap: null, + }; + } + + const normalizedUrl = normalizeJavaScriptUrl(sourcePath); + if (!normalizedUrl) { + return { + handled: true, + sourcemap: null, + }; + } + + if (browserSourceMapCache.has(normalizedUrl)) { + return { + handled: true, + sourcemap: browserSourceMapCache.get(normalizedUrl) ?? null, + }; + } + + return { + handled: true, + sourcemap: await loadSourceMapWithCache({ + jsUrl: normalizedUrl, + cache: browserSourceMapCache, + }), + }; + }; + + const getBrowserSourcemap = async ( + sourcePath: string, + ): Promise => { + const result = await resolveBrowserSourcemap(sourcePath); + return result.handled ? result.sourcemap : null; + }; + /** * Build an error BrowserTestRunResult and call onTestRunEnd if needed. * Used for early-exit error paths to ensure errors reach the summary report. */ const buildErrorResult = async ( error: Error, + close?: () => Promise, ): Promise => { const elapsed = Math.max(0, Date.now() - buildStart); - const errorResult: BrowserTestRunResult = { + const errorResult = { results: [], testResults: [], duration: { totalTime: elapsed, buildTime: elapsed, testTime: 0 }, hasFailure: true, unhandledErrors: [error], + getSourcemap: getBrowserSourcemap, + resolveSourcemap: resolveBrowserSourcemap, + close, }; if (!skipOnTestRunEnd) { @@ -1369,7 +1421,7 @@ export const runBrowserController = async ( testResults: [], duration: errorResult.duration, snapshotSummary: context.snapshotManager.summary, - getSourcemap: async () => null, + getSourcemap: getBrowserSourcemap, unhandledErrors: errorResult.unhandledErrors, }); } @@ -1387,8 +1439,18 @@ export const runBrowserController = async ( cleanup?: () => Promise, ): Promise => { ensureProcessExitCode(1); - await cleanup?.(); - return buildErrorResult(toError(error)); + + const normalizedError = toError(error); + + if (cleanup && skipOnTestRunEnd) { + return buildErrorResult(normalizedError, cleanup); + } + + try { + return await buildErrorResult(normalizedError); + } finally { + await cleanup?.(); + } }; const collectDeletedTestPaths = ( @@ -1434,7 +1496,7 @@ export const runBrowserController = async ( testResults: context.reporterResults.testResults, duration, snapshotSummary: context.snapshotManager.summary, - getSourcemap: async () => null, + getSourcemap: getBrowserSourcemap, unhandledErrors, filterRerunTestPaths, }); @@ -2179,13 +2241,15 @@ export const runBrowserController = async ( }; } - if (!isWatchMode) { - sessionRegistry.clear(); - await destroyBrowserRuntime(runtime); - } + const closeHeadlessRuntime = !isWatchMode + ? async () => { + sessionRegistry.clear(); + await destroyBrowserRuntime(runtime); + } + : undefined; if (fatalError) { - return failWithError(fatalError); + return failWithError(fatalError, closeHeadlessRuntime); } const duration = { @@ -2203,14 +2267,23 @@ export const runBrowserController = async ( ensureProcessExitCode(1); } - const result: BrowserTestRunResult = { + const result = { results: reporterResults, testResults: caseResults, duration, hasFailure: isFailure, + getSourcemap: getBrowserSourcemap, + resolveSourcemap: resolveBrowserSourcemap, + close: skipOnTestRunEnd ? closeHeadlessRuntime : undefined, }; - await notifyTestRunEnd({ duration }); + if (!skipOnTestRunEnd) { + try { + await notifyTestRunEnd({ duration }); + } finally { + await closeHeadlessRuntime?.(); + } + } if (isWatchMode && triggerRerun) { watchContext.hooksEnabled = true; @@ -2451,22 +2524,24 @@ export const runBrowserController = async ( }; } - if (!isWatchMode) { - try { - await containerPage.close(); - } catch { - // ignore - } - try { - await containerContext.close(); - } catch { - // ignore - } - await destroyBrowserRuntime(runtime); - } + const closeContainerRuntime = !isWatchMode + ? async () => { + try { + await containerPage.close(); + } catch { + // ignore + } + try { + await containerContext.close(); + } catch { + // ignore + } + await destroyBrowserRuntime(runtime); + } + : undefined; if (fatalError) { - return failWithError(fatalError); + return failWithError(fatalError, closeContainerRuntime); } const duration = { @@ -2484,14 +2559,23 @@ export const runBrowserController = async ( ensureProcessExitCode(1); } - const result: BrowserTestRunResult = { + const result = { results: reporterResults, testResults: caseResults, duration, hasFailure: isFailure, + getSourcemap: getBrowserSourcemap, + resolveSourcemap: resolveBrowserSourcemap, + close: skipOnTestRunEnd ? closeContainerRuntime : undefined, }; - await notifyTestRunEnd({ duration }); + if (!skipOnTestRunEnd) { + try { + await notifyTestRunEnd({ duration }); + } finally { + await closeContainerRuntime?.(); + } + } // Enable watch hooks AFTER initial test run to avoid duplicate runs if (isWatchMode && triggerRerun) { diff --git a/packages/browser/src/sourceMap/sourceMapLoader.ts b/packages/browser/src/sourceMap/sourceMapLoader.ts new file mode 100644 index 000000000..5e115071e --- /dev/null +++ b/packages/browser/src/sourceMap/sourceMapLoader.ts @@ -0,0 +1,96 @@ +import type { + DecodedSourceMapXInput, + EncodedSourceMapXInput, +} from '@jridgewell/trace-mapping'; +import convert from 'convert-source-map'; + +export type SourceMapPayload = EncodedSourceMapXInput | DecodedSourceMapXInput; + +type Fetcher = typeof fetch; + +export const normalizeJavaScriptUrl = ( + value: string, + options?: { + origin?: string; + }, +): string | null => { + try { + const url = options?.origin + ? new URL(value, options.origin) + : new URL(value); + + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + return null; + } + + url.search = ''; + url.hash = ''; + return url.toString(); + } catch { + return null; + } +}; + +const resolveInlineSourceMap = (code: string): SourceMapPayload | null => { + const converter = convert.fromSource(code); + if (!converter) { + return null; + } + + return converter.toObject() as SourceMapPayload; +}; + +const fetchSourceMap = async ( + jsUrl: string, + fetcher: Fetcher, +): Promise => { + const jsResponse = await fetcher(jsUrl); + if (!jsResponse.ok) { + return null; + } + + const code = await jsResponse.text(); + const inlineMap = resolveInlineSourceMap(code); + if (inlineMap) { + return inlineMap; + } + + const mapResponse = await fetcher(`${jsUrl}.map`); + if (!mapResponse.ok) { + return null; + } + + return (await mapResponse.json()) as SourceMapPayload; +}; + +export const loadSourceMapWithCache = async ({ + jsUrl, + cache, + force = false, + origin, + fetcher = fetch, +}: { + jsUrl: string; + cache: Map; + force?: boolean; + origin?: string; + fetcher?: Fetcher; +}): Promise => { + const normalizedUrl = normalizeJavaScriptUrl(jsUrl, { origin }); + if (!normalizedUrl) { + return null; + } + + if (!force && cache.has(normalizedUrl)) { + return cache.get(normalizedUrl) ?? null; + } + + try { + const sourceMap = await fetchSourceMap(normalizedUrl, fetcher); + cache.set(normalizedUrl, sourceMap); + return sourceMap; + } catch { + cache.set(normalizedUrl, null); + return null; + } +}; diff --git a/packages/core/src/core/runTests.ts b/packages/core/src/core/runTests.ts index 4fb983243..d101af6a1 100644 --- a/packages/core/src/core/runTests.ts +++ b/packages/core/src/core/runTests.ts @@ -1,6 +1,6 @@ import { createCoverageProvider } from '../coverage'; import { createPool } from '../pool'; -import type { EntryInfo, ProjectEntries } from '../types'; +import type { EntryInfo, ProjectEntries, SourceMapInput } from '../types'; import { clearScreen, color, @@ -405,137 +405,166 @@ export async function runTests(context: Rstest): Promise { const browserResult = browserResultPromise ? await browserResultPromise : undefined; + const browserResolveSourcemap = browserResult?.resolveSourcemap; + const browserClose = browserResult?.close; - // When unifying reporter output, combine browser and node durations - const duration = - shouldUnifyReporter && browserResult - ? { - totalTime: testTime + buildTime + browserResult.duration.totalTime, - buildTime: buildTime + browserResult.duration.buildTime, - testTime: testTime + browserResult.duration.testTime, + try { + const nodeResourceByAssetName = new Map< + string, + (typeof returns)[number]['getSourceMaps'] + >(); + + for (const item of returns) { + for (const assetName of item.assetNames) { + nodeResourceByAssetName.set(assetName, item.getSourceMaps); + } + } + + const getSourcemap = async ( + sourcePath: string, + ): Promise => { + if (browserResolveSourcemap) { + const resolved = await browserResolveSourcemap(sourcePath); + if (resolved.handled) { + return resolved.sourcemap; } - : { - totalTime: testTime + buildTime, - buildTime, - testTime, - }; - - const results = returns.flatMap((r) => r.results); - const testResults = returns.flatMap((r) => r.testResults); - const errors = returns.flatMap((r) => r.errors || []); - - // Merge browser test results for coverage collection (only when unifying reporter output) - // In watch mode, browser and node tests run independently with their own reporters, - // so we should not merge stale browser results into node results - if (shouldUnifyReporter && browserResult?.results) { - results.push(...browserResult.results); - } - if (shouldUnifyReporter && browserResult?.testResults) { - testResults.push(...browserResult.testResults); - } - if (shouldUnifyReporter && browserResult?.unhandledErrors) { - errors.push(...browserResult.unhandledErrors); - } + } - context.updateReporterResultState( - results, - testResults, - currentDeletedEntries, - ); + const getSourceMaps = nodeResourceByAssetName.get(sourcePath); + const sourceMap = (await getSourceMaps?.([sourcePath]))?.[sourcePath]; + return sourceMap ? JSON.parse(sourceMap) : null; + }; + + // When unifying reporter output, combine browser and node durations + const duration = + shouldUnifyReporter && browserResult + ? { + totalTime: + testTime + buildTime + browserResult.duration.totalTime, + buildTime: buildTime + browserResult.duration.buildTime, + testTime: testTime + browserResult.duration.testTime, + } + : { + totalTime: testTime + buildTime, + buildTime, + testTime, + }; - // Check for failures including browser results when unified - const nodeHasFailure = - results.some((r) => r.status === 'fail') || errors.length; - const browserHasFailure = shouldUnifyReporter && browserResult?.hasFailure; + const results = returns.flatMap((r) => r.results); + const testResults = returns.flatMap((r) => r.testResults); + const errors = returns.flatMap((r) => r.errors || []); - if (results.length === 0 && !errors.length) { - if (command === 'watch') { - if (mode === 'on-demand') { - logger.log(color.yellow('No test files need re-run.')); - } else { - logger.log(color.yellow('No test files found.')); - } - } else { - const code = context.normalizedConfig.passWithNoTests ? 0 : 1; + // Merge browser test results for coverage collection (only when unifying reporter output) + // In watch mode, browser and node tests run independently with their own reporters, + // so we should not merge stale browser results into node results + if (shouldUnifyReporter && browserResult?.results) { + results.push(...browserResult.results); + } + if (shouldUnifyReporter && browserResult?.testResults) { + testResults.push(...browserResult.testResults); + } + if (shouldUnifyReporter && browserResult?.unhandledErrors) { + errors.push(...browserResult.unhandledErrors); + } + + context.updateReporterResultState( + results, + testResults, + currentDeletedEntries, + ); - const message = `No test files found, exiting with code ${code}.`; - if (code === 0) { - logger.log(color.yellow(message)); + // Check for failures including browser results when unified + const nodeHasFailure = + results.some((r) => r.status === 'fail') || errors.length; + const browserHasFailure = + shouldUnifyReporter && browserResult?.hasFailure; + + if (results.length === 0 && !errors.length) { + if (command === 'watch') { + if (mode === 'on-demand') { + logger.log(color.yellow('No test files need re-run.')); + } else { + logger.log(color.yellow('No test files found.')); + } } else { - logger.error(color.red(message)); - } + const code = context.normalizedConfig.passWithNoTests ? 0 : 1; - process.exitCode = code; - } - if (mode === 'all') { - if (context.fileFilters?.length) { - logger.log( - color.gray('filter: '), - context.fileFilters.join(color.gray(', ')), - ); - } + const message = `No test files found, exiting with code ${code}.`; + if (code === 0) { + logger.log(color.yellow(message)); + } else { + logger.error(color.red(message)); + } - allProjects.forEach((p) => { - if (allProjects.length > 1) { - logger.log(''); - logger.log(color.gray('project:'), p.name); + process.exitCode = code; + } + if (mode === 'all') { + if (context.fileFilters?.length) { + logger.log( + color.gray('filter: '), + context.fileFilters.join(color.gray(', ')), + ); } - logger.log(color.gray('root:'), p.rootPath); - logger.log( - color.gray('include:'), - p.normalizedConfig.include.join(color.gray(', ')), - ); - logger.log( - color.gray('exclude:'), - p.normalizedConfig.exclude.patterns.join(color.gray(', ')), - ); - }); + allProjects.forEach((p) => { + if (allProjects.length > 1) { + logger.log(''); + logger.log(color.gray('project:'), p.name); + } + logger.log(color.gray('root:'), p.rootPath); + + logger.log( + color.gray('include:'), + p.normalizedConfig.include.join(color.gray(', ')), + ); + logger.log( + color.gray('exclude:'), + p.normalizedConfig.exclude.patterns.join(color.gray(', ')), + ); + }); + } } - } - const isFailure = nodeHasFailure || browserHasFailure; + const isFailure = nodeHasFailure || browserHasFailure; - if (isFailure) { - process.exitCode = 1; - } + if (isFailure) { + process.exitCode = 1; + } - for (const reporter of reporters) { - await reporter.onTestRunEnd?.({ - results: context.reporterResults.results, - testResults: context.reporterResults.testResults, - unhandledErrors: errors, - snapshotSummary: snapshotManager.summary, - duration, - getSourcemap: async (name: string) => { - const resource = returns.find((r) => r.assetNames.includes(name)); - - const sourceMap = (await resource?.getSourceMaps([name]))?.[name]; - return sourceMap ? JSON.parse(sourceMap) : null; - }, - filterRerunTestPaths: currentEntries.length - ? currentEntries.map((e) => e.testPath) - : undefined, - }); - } + for (const reporter of reporters) { + await reporter.onTestRunEnd?.({ + results: context.reporterResults.results, + testResults: context.reporterResults.testResults, + unhandledErrors: errors, + snapshotSummary: snapshotManager.summary, + duration, + getSourcemap, + filterRerunTestPaths: currentEntries.length + ? currentEntries.map((e) => e.testPath) + : undefined, + }); + } - // Generate coverage reports after all tests complete - if (coverageProvider && (!isFailure || coverage.reportOnFailure)) { - const { generateCoverage } = await import('../coverage/generate'); + // Generate coverage reports after all tests complete + if (coverageProvider && (!isFailure || coverage.reportOnFailure)) { + const { generateCoverage } = await import('../coverage/generate'); - await generateCoverage(context, results, coverageProvider); - } + await generateCoverage(context, results, coverageProvider); + } - if (isFailure) { - const bail = context.normalizedConfig.bail; + if (isFailure) { + const bail = context.normalizedConfig.bail; - if (bail && context.stateManager.getCountOfFailedTests() >= bail) { - logger.log( - color.yellow( - `Test run aborted due to reaching the bail limit of ${bail} failed test(s).`, - ), - ); + if (bail && context.stateManager.getCountOfFailedTests() >= bail) { + logger.log( + color.yellow( + `Test run aborted due to reaching the bail limit of ${bail} failed test(s).`, + ), + ); + } } + } finally { + await browserClose?.(); } }; diff --git a/packages/core/src/types/browser.ts b/packages/core/src/types/browser.ts index 83e9d4ba1..8bd82cae8 100644 --- a/packages/core/src/types/browser.ts +++ b/packages/core/src/types/browser.ts @@ -1,5 +1,16 @@ +import type { SourceMapInput } from '@jridgewell/trace-mapping'; +import type { GetSourcemap } from './reporter'; import type { TestFileResult, TestResult } from './testSuite'; +export interface BrowserSourcemapResolutionResult { + handled: boolean; + sourcemap: SourceMapInput | null; +} + +export type ResolveBrowserSourcemap = ( + sourcePath: string, +) => Promise; + /** * Options for running browser tests. */ @@ -35,4 +46,10 @@ export interface BrowserTestRunResult { hasFailure: boolean; /** Errors that occurred before/outside test execution (e.g., browser launch failure) */ unhandledErrors?: Error[]; + /** Source map resolver used when reporter output is unified in core */ + getSourcemap?: GetSourcemap; + /** Route-aware source map resolver used by core unified reporter flow */ + resolveSourcemap?: ResolveBrowserSourcemap; + /** Deferred cleanup hook for unified reporter mode */ + close?: () => Promise; } diff --git a/packages/core/src/utils/error.ts b/packages/core/src/utils/error.ts index b80275487..ad6a71371 100644 --- a/packages/core/src/utils/error.ts +++ b/packages/core/src/utils/error.ts @@ -9,6 +9,8 @@ import { formatTestPath } from './testFiles'; export const isRelativePath = (p: string): boolean => /^\.\.?\//.test(p); +const isHttpLikeFile = (file: string): boolean => /^https?:\/\//.test(file); + const hintNotDefinedError = (message: string): string => { const [, varName] = message.match(/(\w+) is not defined/) || []; if (varName) { @@ -210,5 +212,22 @@ export async function parseErrorStacktrace({ frames.filter((frame): frame is StackFrame => frame !== null), ); - return stackFrames; + if (fullStack) { + return stackFrames; + } + + const filteredFrames = stackFrames.filter((frame) => { + if (!frame.file) { + return false; + } + + if (isHttpLikeFile(frame.file)) { + return false; + } + + const normalizedFile = frame.file.replace(/\\/g, '/'); + return !stackIgnores.some((entry) => normalizedFile.match(entry)); + }); + + return filteredFrames; }