From 60adf092594a336c9b8b9e35054041fd2e5ee5b4 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 2 Jun 2026 14:33:23 +0200 Subject: [PATCH] fix: report active test on device timeout --- .../jest/src/__tests__/execute-run.test.ts | 40 ++++++++++++++++++ packages/jest/src/execute-run.ts | 41 +++++++++++++++++-- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/packages/jest/src/__tests__/execute-run.test.ts b/packages/jest/src/__tests__/execute-run.test.ts index 632ce14..37e4bb7 100644 --- a/packages/jest/src/__tests__/execute-run.test.ts +++ b/packages/jest/src/__tests__/execute-run.test.ts @@ -365,6 +365,46 @@ describe('executeRun', () => { ]); }); + it('includes the last active test when the device stops responding during a test', async () => { + let testRunnerListener: + | ((event: TestRunnerTestStartedEvent | TestRunnerTestFinishedEvent) => void) + | undefined; + const { emitEvent, calls } = makeEmitEvent(); + const session = makeSession({ + onTestRunnerEvent: vi.fn((listener) => { + testRunnerListener = listener as typeof testRunnerListener; + return () => undefined; + }), + }); + + mockRunHarnessTestFile.mockImplementation(async () => { + testRunnerListener?.({ + type: 'test-started', + file: 'example.ts', + suite: 'VisionCamera - Controller', + name: 'runs a zoom animation', + ancestorTitles: ['VisionCamera - Controller'], + fullName: 'VisionCamera - Controller runs a zoom animation', + startedAt: 10, + }); + + throw new DeviceNotRespondingError('runTests', []); + }); + + await executeRun(session, [makeTest()], makeWatcher(), emitEvent, makeGlobalConfig()); + + expect(calls).toContainEqual([ + 'test-file-failure', + expect.anything(), + expect.objectContaining({ + message: expect.stringContaining( + 'Last active test: VisionCamera - Controller runs a zoom animation', + ), + stack: '', + }), + ]); + }); + it('passes AppBridgeDisconnectedError to onFailure with an empty stack', async () => { const { emitEvent, calls } = makeEmitEvent(); const session = makeSession({ diff --git a/packages/jest/src/execute-run.ts b/packages/jest/src/execute-run.ts index 6d326bb..8eb4a10 100644 --- a/packages/jest/src/execute-run.ts +++ b/packages/jest/src/execute-run.ts @@ -52,7 +52,18 @@ class CancelRun extends Error { } } -const buildTestFailure = (err: unknown): { message: string; stack: string } => { +const isSameTestRun = ( + started: TestRunnerTestStartedEvent, + finished: TestRunnerTestFinishedEvent, +): boolean => + started.file === finished.file && + started.fullName === finished.fullName && + started.startedAt === finished.startedAt; + +const buildTestFailure = ( + err: unknown, + activeTest?: TestRunnerTestStartedEvent | null, +): { message: string; stack: string } => { if ( err instanceof NativeCrashError || err instanceof RuntimeDisconnectError || @@ -60,7 +71,13 @@ const buildTestFailure = (err: unknown): { message: string; stack: string } => { err instanceof AppBridgeDisconnectedError || err instanceof DeviceNotRespondingError ) { - return { message: (err as Error).message, stack: '' }; + let message = (err as Error).message; + + if (err instanceof DeviceNotRespondingError && activeTest) { + message += `\nLast active test: ${activeTest.fullName}`; + } + + return { message, stack: '' }; } return err as { message: string; stack: string }; }; @@ -128,8 +145,15 @@ export const executeRun = async ( const testFiles = tests.map((t) => path.relative(rootDir, t.path)); const summary = createRunSummary(); let caseEventChain = Promise.resolve(); + let activeTest: TestRunnerTestStartedEvent | null = null; const unsubscribe = session.onTestRunnerEvent((event) => { if (isHarnessCaseEvent(event)) { + if (event.type === 'test-started') { + activeTest = event; + } else if (activeTest && isSameTestRun(activeTest, event)) { + activeTest = null; + } + caseEventChain = caseEventChain.then(() => event.type === 'test-started' ? emitHarnessTestStarted(emitEvent, event) @@ -183,6 +207,7 @@ export const executeRun = async ( const relativeTestPath = path.relative(rootDir, test.path); const fileStartedAt = Date.now(); let emittedTestFileFinished = false; + activeTest = null; const emitTestFileFinished = async (options: { status: 'passed' | 'failed' | 'skipped' | 'todo'; @@ -224,7 +249,11 @@ export const executeRun = async ( }); } updateRunState({ error: err }); - await emitEvent('test-file-failure', test, buildTestFailure(err)); + await emitEvent( + 'test-file-failure', + test, + buildTestFailure(err, activeTest), + ); } continue; } @@ -288,7 +317,11 @@ export const executeRun = async ( updateRunState({ error: isRuntimeFailure ? undefined : err }); await caseEventChain; - await emitEvent('test-file-failure', test, buildTestFailure(err)); + await emitEvent( + 'test-file-failure', + test, + buildTestFailure(err, activeTest), + ); } } } catch (err) {