Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 54 additions & 19 deletions packages/browser/src/hostController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,52 @@ export const runBrowserController = async (
): Promise<BrowserTestRunResult | void> => {
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<BrowserTestRunResult> => {
const elapsed = Math.max(0, Date.now() - buildStart);
const errorResult: BrowserTestRunResult = {
results: [],
testResults: [],
duration: { totalTime: elapsed, buildTime: elapsed, testTime: 0 },
hasFailure: true,
Comment thread
fi3ework marked this conversation as resolved.
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<void>,
): Promise<BrowserTestRunResult> => {
ensureProcessExitCode(1);
await cleanup?.();
return buildErrorResult(toError(error));
};
Comment thread
fi3ework marked this conversation as resolved.

const containerDevServerEnv = process.env.RSTEST_CONTAINER_DEV_SERVER;
let containerDevServer: string | undefined;
let containerDistPath: string | undefined;
Expand All @@ -1243,23 +1289,17 @@ 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);
}
}

if (!containerDevServer) {
try {
containerDistPath = resolveContainerDist();
} catch (error) {
logger.error(color.red(String(error)));
ensureProcessExitCode(1);
return;
return failWithError(error);
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 = {
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/core/runTests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,13 @@ export async function runTests(context: Rstest): Promise<void> {
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,
Expand Down Expand Up @@ -421,6 +426,9 @@ export async function runTests(context: Rstest): Promise<void> {
if (shouldUnifyReporter && browserResult?.testResults) {
testResults.push(...browserResult.testResults);
}
if (shouldUnifyReporter && browserResult?.unhandledErrors) {
errors.push(...browserResult.unhandledErrors);
}

context.updateReporterResultState(
results,
Expand Down
22 changes: 4 additions & 18 deletions packages/core/src/reporter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Comment thread
fi3ework marked this conversation as resolved.
);
}
Comment on lines +47 to 53

Copilot AI Feb 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DefaultReporter now instantiates StatusRenderer in the constructor, which immediately starts WindowRenderer and intercepts process.stdout/stderr. Because WindowRenderer periodically clears/re-renders the terminal window, this can again buffer or wipe startup-time error output that occurs before the first test file starts (the exact regression the previous lazy-init avoided). Consider restoring lazy initialization (create StatusRenderer on first onTestFileStart) or delaying WindowRenderer.start()/stream interception until tests actually begin.

Copilot uses AI. Check for mistakes.
}

onTestFileStart(): void {
this.ensureStatusRenderer();
this.statusRenderer?.onTestFileStart();
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/types/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Loading