diff --git a/packages/playwright/src/errorContext.ts b/packages/playwright/src/errorContext.ts index 651258c77ba39..cc85153d7356d 100644 --- a/packages/playwright/src/errorContext.ts +++ b/packages/playwright/src/errorContext.ts @@ -24,6 +24,7 @@ import { codeFrameColumns } from './transform/babelBundle'; import type { MetadataWithCommitInfo } from './isomorphic/types'; import type { TestInfoImpl } from './worker/testInfo'; +import type { Location } from '../types/test'; export async function attachErrorContext(testInfo: TestInfoImpl, format: 'markdown' | 'json', sourceCache: Map, ariaSnapshot: string | undefined) { if (format === 'json') { @@ -91,31 +92,32 @@ export async function attachErrorContext(testInfo: TestInfoImpl, format: 'markdo const parsedError = error.stack ? parseErrorStack(error.stack, path.sep) : undefined; const inlineMessage = stripAnsiEscapes(parsedError?.message || error.message || '').split('\n')[0]; - const location = parsedError?.location || { file: testInfo.file, line: testInfo.line, column: testInfo.column }; - const source = await loadSource(location.file, sourceCache); - const codeFrame = codeFrameColumns( - source, - { - start: { - line: location.line, - column: location.column + const loadedSource = await loadSource(parsedError?.location, testInfo, sourceCache); + if (loadedSource) { + const codeFrame = codeFrameColumns( + loadedSource.source, + { + start: { + line: loadedSource.location.line, + column: loadedSource.location.column + }, }, - }, - { - highlightCode: false, - linesAbove: 100, - linesBelow: 100, - message: inlineMessage || undefined, - } - ); - lines.push( - '', - '# Test source', - '', - '```ts', - codeFrame, - '```', - ); + { + highlightCode: false, + linesAbove: 100, + linesBelow: 100, + message: inlineMessage || undefined, + } + ); + lines.push( + '', + '# Test source', + '', + '```ts', + codeFrame, + '```', + ); + } if (metadata.gitDiff) { lines.push( @@ -139,12 +141,33 @@ export async function attachErrorContext(testInfo: TestInfoImpl, format: 'markdo } } -async function loadSource(file: string, sourceCache: Map) { +async function loadSource( + errorLocation: Location | undefined, + testLocation: Location, + sourceCache: Map +): Promise<{ location: Location, source: string } | undefined> { + if (errorLocation) { + const source = await loadSourceCached(errorLocation.file, sourceCache); + if (source) + return { location: errorLocation, source }; + } + // If the error location is not available on the disk (e.g. fake page.evaluate in-browser error), then fallback to the test file. + const source = await loadSourceCached(testLocation.file, sourceCache); + if (source) + return { location: testLocation, source }; + return undefined; +} + +async function loadSourceCached(file: string, sourceCache: Map): Promise { let source = sourceCache.get(file); if (!source) { - // A mild race is Ok here. - source = await fs.readFile(file, 'utf8'); - sourceCache.set(file, source); + try { + // A mild race is Ok here. + source = await fs.readFile(file, 'utf8'); + sourceCache.set(file, source); + } catch (e) { + // Ignore errors. + } } return source; } diff --git a/tests/playwright-test/reporter-line.spec.ts b/tests/playwright-test/reporter-line.spec.ts index b3a84ac1dc79c..1d7ff8b07878b 100644 --- a/tests/playwright-test/reporter-line.spec.ts +++ b/tests/playwright-test/reporter-line.spec.ts @@ -15,6 +15,7 @@ */ import path from 'path'; +import fs from 'fs'; import { test, expect } from './playwright-test-fixtures'; for (const useIntermediateMergeReport of [false, true] as const) { @@ -206,5 +207,26 @@ for (const useIntermediateMergeReport of [false, true] as const) { expect(text).toContain(`Error Context: ${path.join('test-results', 'a-one', 'error-context.md')}`); expect(result.exitCode).toBe(1); }); + + test('should show error context if exception contains non-existent file', async ({ runInlineTest, useIntermediateMergeReport }) => { + const result = await runInlineTest({ + 'a.test.js': ` + const { test, expect } = require('@playwright/test'); + test('one', async ({ page }) => { + await page.evaluate(() => { + throw new Error('error'); + }); + }); + `, + }, { reporter: 'line' }); + if (useIntermediateMergeReport) + expect(result.output).toContain(`Error Context: ${path.join('blob-report', 'resources')}`); + else + expect(result.output).toContain(`Error Context: ${path.join('test-results', 'a-one', 'error-context.md')}`); + const file = /Error Context: (.*)/.exec(result.output)?.[1]; + const content = await fs.promises.readFile(path.join(result.report.config.rootDir, file), 'utf8'); + expect(content).toContain('^ Error: page.evaluate: Error: error'); + expect(result.exitCode).toBe(1); + }); }); }