From 4c14011f2a90fdc8141b486f2e98036452f2268a Mon Sep 17 00:00:00 2001 From: fi3ework Date: Sat, 7 Feb 2026 00:51:06 +0800 Subject: [PATCH] fix(browser): unify error reporting and avoid duplicate error messages --- packages/browser/src/hostController.ts | 73 +++++++++++++++++++------- packages/core/src/core/runTests.ts | 12 ++++- packages/core/src/reporter/index.ts | 22 ++------ packages/core/src/types/browser.ts | 2 + 4 files changed, 70 insertions(+), 39 deletions(-) diff --git a/packages/browser/src/hostController.ts b/packages/browser/src/hostController.ts index ce03b819c..4a1db8eca 100644 --- a/packages/browser/src/hostController.ts +++ b/packages/browser/src/hostController.ts @@ -1232,6 +1232,52 @@ export const runBrowserController = async ( ): Promise => { const { skipOnTestRunEnd = false } = options ?? {}; const buildStart = Date.now(); + + /** + * 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, + ): Promise => { + const elapsed = Math.max(0, Date.now() - buildStart); + const errorResult: BrowserTestRunResult = { + results: [], + testResults: [], + duration: { totalTime: elapsed, buildTime: elapsed, testTime: 0 }, + hasFailure: true, + unhandledErrors: [error], + }; + + if (!skipOnTestRunEnd) { + for (const reporter of context.reporters) { + await (reporter as Reporter).onTestRunEnd?.({ + results: [], + testResults: [], + duration: errorResult.duration, + snapshotSummary: context.snapshotManager.summary, + getSourcemap: async () => null, + unhandledErrors: errorResult.unhandledErrors, + }); + } + } + + return errorResult; + }; + + const toError = (error: unknown): Error => { + return error instanceof Error ? error : new Error(String(error)); + }; + + const failWithError = async ( + error: unknown, + cleanup?: () => Promise, + ): Promise => { + ensureProcessExitCode(1); + await cleanup?.(); + return buildErrorResult(toError(error)); + }; + const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER; let containerDevServer: string | undefined; let containerDistPath: string | undefined; @@ -1243,13 +1289,9 @@ export const runBrowserController = async ( `[Browser UI] Using dev server for container: ${containerDevServer}`, ); } catch (error) { - logger.error( - color.red( - `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${String(error)}`, - ), - ); - ensureProcessExitCode(1); - return; + const originalError = toError(error); + originalError.message = `Invalid RSTEST_CONTAINER_DEV_SERVER value: ${originalError.message}`; + return failWithError(originalError); } } @@ -1257,9 +1299,7 @@ export const runBrowserController = async ( try { containerDistPath = resolveContainerDist(); } catch (error) { - logger.error(color.red(String(error))); - ensureProcessExitCode(1); - return; + return failWithError(error); } } @@ -1340,10 +1380,9 @@ export const runBrowserController = async ( containerDevServer, }); } catch (error) { - logger.error(error instanceof Error ? error : new Error(String(error))); - ensureProcessExitCode(1); - await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); - return; + return failWithError(error, async () => { + await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); + }); } if (isWatchMode) { @@ -1675,11 +1714,7 @@ export const runBrowserController = async ( } if (fatalError) { - logger.error( - color.red(`Browser test run failed: ${(fatalError as Error).message}`), - ); - ensureProcessExitCode(1); - return; + return failWithError(fatalError); } const duration = { diff --git a/packages/core/src/core/runTests.ts b/packages/core/src/core/runTests.ts index 2922b695c..cb51a8c82 100644 --- a/packages/core/src/core/runTests.ts +++ b/packages/core/src/core/runTests.ts @@ -65,8 +65,13 @@ export async function runTests(context: Rstest): Promise { skipOnTestRunEnd: false, }); - // Generate coverage reports for browser-only tests - if (coverage.enabled && browserResult?.results) { + // Generate coverage reports for browser-only tests when execution produced test results. + // Skip coverage on early startup failures surfaced via unhandledErrors. + if ( + coverage.enabled && + browserResult?.results.length && + !browserResult.unhandledErrors?.length + ) { const coverageProvider = await createCoverageProvider( coverage, context.rootPath, @@ -421,6 +426,9 @@ export async function runTests(context: Rstest): Promise { if (shouldUnifyReporter && browserResult?.testResults) { testResults.push(...browserResult.testResults); } + if (shouldUnifyReporter && browserResult?.unhandledErrors) { + errors.push(...browserResult.unhandledErrors); + } context.updateReporterResultState( results, diff --git a/packages/core/src/reporter/index.ts b/packages/core/src/reporter/index.ts index e354681dc..1922d760b 100644 --- a/packages/core/src/reporter/index.ts +++ b/packages/core/src/reporter/index.ts @@ -44,30 +44,16 @@ export class DefaultReporter implements Reporter { this.projectConfigs = projectConfigs ?? new Map(); this.options = options; this.testState = testState; - // Note: StatusRenderer is created lazily in onTestFileStart() to avoid - // intercepting stdout/stderr too early. This ensures that errors occurring - // before tests start (e.g., Playwright browser not installed) are visible - // and not cleared by WindowRenderer's TTY control sequences. - } - - /** - * Lazily create StatusRenderer on first test file start. - * This avoids intercepting stdout/stderr before tests actually begin, - * ensuring early errors (like missing Playwright browsers) remain visible. - */ - private ensureStatusRenderer(): void { - if (this.statusRenderer) return; - if (isTTY() || this.options.logger) { + if (isTTY() || options.logger) { this.statusRenderer = new StatusRenderer( - this.rootPath, - this.testState, - this.options.logger, + rootPath, + testState, + options.logger, ); } } onTestFileStart(): void { - this.ensureStatusRenderer(); this.statusRenderer?.onTestFileStart(); } diff --git a/packages/core/src/types/browser.ts b/packages/core/src/types/browser.ts index 04c11e5db..83e9d4ba1 100644 --- a/packages/core/src/types/browser.ts +++ b/packages/core/src/types/browser.ts @@ -33,4 +33,6 @@ export interface BrowserTestRunResult { }; /** Whether the test run had failures */ hasFailure: boolean; + /** Errors that occurred before/outside test execution (e.g., browser launch failure) */ + unhandledErrors?: Error[]; }