diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index cca2eb18f2..d4631dd960 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -15,11 +15,9 @@ */ import path from 'path'; -import { StackUtils } from '../utilsBundle'; +import { parseStackTraceLine } from '../utilsBundle'; import { isUnderTest } from './'; -const stackUtils = new StackUtils(); - export function rewriteErrorMessage(e: E, newMessage: string): E { const lines: string[] = (e.stack?.split('\n') || []).filter(l => l.startsWith(' at ')); e.message = newMessage; @@ -82,17 +80,11 @@ export function captureStackTrace(rawStack?: string): ParsedStackTrace { inCore: boolean; }; let parsedFrames = stack.split('\n').map(line => { - const frame = stackUtils.parseLine(line); - if (!frame || !frame.file) + const { frame, fileName } = parseStackTraceLine(line); + if (!frame || !frame.file || !fileName) return null; if (isInternalFileName(frame.file, frame.function)) return null; - // Workaround for https://github.com/tapjs/stack-utils/issues/60 - let fileName: string; - if (frame.file.startsWith('file://')) - fileName = new URL(frame.file).pathname; - else - fileName = path.resolve(process.cwd(), frame.file); if (isTesting && fileName.includes(COVERAGE_PATH)) return null; const inCore = fileName.startsWith(CORE_LIB) || fileName.startsWith(CORE_SRC); diff --git a/packages/playwright-core/src/utilsBundle.ts b/packages/playwright-core/src/utilsBundle.ts index e47b55c3e1..9bab42fb64 100644 --- a/packages/playwright-core/src/utilsBundle.ts +++ b/packages/playwright-core/src/utilsBundle.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import url from 'url'; +import path from 'path'; + export const colors: typeof import('../bundles/utils/node_modules/colors/safe') = require('./utilsBundleImpl').colors; export const debug: typeof import('../bundles/utils/node_modules/@types/debug') = require('./utilsBundleImpl').debug; export const getProxyForUrl: typeof import('../bundles/utils/node_modules/@types/proxy-from-env').getProxyForUrl = require('./utilsBundleImpl').getProxyForUrl; @@ -28,10 +31,27 @@ export const program: typeof import('../bundles/utils/node_modules/commander').p export const progress: typeof import('../bundles/utils/node_modules/@types/progress') = require('./utilsBundleImpl').progress; export const rimraf: typeof import('../bundles/utils/node_modules/@types/rimraf') = require('./utilsBundleImpl').rimraf; export const SocksProxyAgent: typeof import('../bundles/utils/node_modules/socks-proxy-agent').SocksProxyAgent = require('./utilsBundleImpl').SocksProxyAgent; -export const StackUtils: typeof import('../bundles/utils/node_modules/@types/stack-utils') = require('./utilsBundleImpl').StackUtils; export const ws: typeof import('../bundles/utils/node_modules/@types/ws') = require('./utilsBundleImpl').ws; export const wsServer: typeof import('../bundles/utils/node_modules/@types/ws').WebSocketServer = require('./utilsBundleImpl').wsServer; export const wsReceiver = require('./utilsBundleImpl').wsReceiver; export const wsSender = require('./utilsBundleImpl').wsSender; export type { Command } from '../bundles/utils/node_modules/commander'; export type { WebSocket, WebSocketServer, RawData as WebSocketRawData, EventEmitter as WebSocketEventEmitter } from '../bundles/utils/node_modules/@types/ws'; + +const StackUtils: typeof import('../bundles/utils/node_modules/@types/stack-utils') = require('./utilsBundleImpl').StackUtils; +const stackUtils = new StackUtils(); + +export function parseStackTraceLine(line: string): { frame: import('../bundles/utils/node_modules/@types/stack-utils').StackLineData | null, fileName: string | null } { + const frame = stackUtils.parseLine(line); + if (!frame) + return { frame: null, fileName: null }; + let fileName = null; + if (frame.file) { + // ESM files return file:// URLs, see here: https://github.com/tapjs/stack-utils/issues/60 + fileName = frame.file.startsWith('file://') ? url.fileURLToPath(frame.file) : path.resolve(process.cwd(), frame.file); + } + return { + frame, + fileName, + }; +} diff --git a/packages/playwright-test/src/reporters/base.ts b/packages/playwright-test/src/reporters/base.ts index 3524c9a816..87fbab24ad 100644 --- a/packages/playwright-test/src/reporters/base.ts +++ b/packages/playwright-test/src/reporters/base.ts @@ -14,14 +14,12 @@ * limitations under the License. */ -import { colors, ms as milliseconds } from 'playwright-core/lib/utilsBundle'; +import { colors, ms as milliseconds, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; import fs from 'fs'; import path from 'path'; -import { StackUtils } from 'playwright-core/lib/utilsBundle'; import type { FullConfig, TestCase, Suite, TestResult, TestError, Reporter, FullResult, TestStep, Location } from '../../types/testReporter'; import type { FullConfigInternal } from '../types'; import { codeFrameColumns } from '../babelBundle'; -const stackUtils = new StackUtils(); export type TestResultOutput = { chunk: string | Buffer, type: 'stdout' | 'stderr' }; export const kOutputSymbol = Symbol('output'); @@ -411,10 +409,9 @@ export function prepareErrorStack(stack: string, file?: string): { const stackLines = lines.slice(firstStackLine); let location: Location | undefined; for (const line of stackLines) { - const parsed = stackUtils.parseLine(line); - if (!parsed || !parsed.file) + const { frame: parsed, fileName: resolvedFile } = parseStackTraceLine(line); + if (!parsed || !resolvedFile) continue; - const resolvedFile = path.join(process.cwd(), parsed.file); if (!file || resolvedFile === file) { location = { file: resolvedFile, column: parsed.column || 0, line: parsed.line || 0 }; break; diff --git a/tests/playwright-test/esm.spec.ts b/tests/playwright-test/esm.spec.ts index e3beb563a3..b3a7bd5ed4 100644 --- a/tests/playwright-test/esm.spec.ts +++ b/tests/playwright-test/esm.spec.ts @@ -148,7 +148,6 @@ test('should use source maps', async ({ runInlineTest, nodeVersion }) => { }); test('should show the codeframe in errors', async ({ runInlineTest, nodeVersion }) => { - test.fixme(); // We only support experimental esm mode on Node 16+ test.skip(nodeVersion.major < 16); const result = await runInlineTest({ @@ -163,17 +162,31 @@ test('should show the codeframe in errors', async ({ runInlineTest, nodeVersion expect(1).toBe(2); expect(testInfo.project.name).toBe('foo'); }); + + test('foobar', async ({}) => { + const error = new Error('my-message'); + error.name = 'FooBarError'; + throw error; + }); ` - }, { reporter: 'list' }); + }, { reporter: 'list' }, { + FORCE_COLOR: '0', + }); const output = stripAnsi(result.output); expect(result.exitCode).toBe(1); - expect(result.failed).toBe(1); + expect(result.failed).toBe(2); expect(output, 'error carrot—via source maps—is positioned appropriately').toContain( [ ` > 8 | expect(1).toBe(2);`, ` | ^` ].join('\n')); + expect(result.output).toContain('FooBarError: my-message'); + expect(result.output).not.toContain('at a.test.ts'); + expect(result.output).toContain(` 12 | test('foobar', async ({}) => {`); + expect(result.output).toContain(`> 13 | const error = new Error('my-message');`); + expect(result.output).toContain(' | ^'); + expect(result.output).toContain(' 14 | error.name = \'FooBarError\';'); }); test('should filter by line', async ({ runInlineTest, nodeVersion }) => {