diff --git a/packages/playwright/src/common/ipc.ts b/packages/playwright/src/common/ipc.ts index 5ce1991f65..82538bb6ed 100644 --- a/packages/playwright/src/common/ipc.ts +++ b/packages/playwright/src/common/ipc.ts @@ -15,9 +15,11 @@ */ import util from 'util'; -import { type SerializedCompilationCache, serializeCompilationCache } from '../transform/compilationCache'; +import { serializeCompilationCache } from '../transform/compilationCache'; +import type { SerializedCompilationCache } from '../transform/compilationCache'; import type { ConfigLocation, FullConfigInternal } from './config'; import type { ReporterDescription, TestInfoError, TestStatus } from '../../types/test'; +import type { MatcherResultProperty } from '../matchers/matcherHint'; export type ConfigCLIOverrides = { debug?: boolean; @@ -74,11 +76,15 @@ export type AttachmentPayload = { contentType: string; }; +export type TestInfoErrorImpl = TestInfoError & { + matcherResult?: MatcherResultProperty; +}; + export type TestEndPayload = { testId: string; duration: number; status: TestStatus; - errors: TestInfoError[]; + errors: TestInfoErrorImpl[]; hasNonRetriableError: boolean; expectedStatus: TestStatus; annotations: { type: string, description?: string }[]; @@ -99,7 +105,7 @@ export type StepEndPayload = { testId: string; stepId: string; wallTime: number; // milliseconds since unix epoch - error?: TestInfoError; + error?: TestInfoErrorImpl; }; export type TestEntry = { @@ -113,7 +119,7 @@ export type RunPayload = { }; export type DonePayload = { - fatalErrors: TestInfoError[]; + fatalErrors: TestInfoErrorImpl[]; skipTestsDueToSetupFailure: string[]; // test ids fatalUnknownTestIds?: string[]; }; @@ -124,7 +130,7 @@ export type TestOutputPayload = { }; export type TeardownErrorsPayload = { - fatalErrors: TestInfoError[]; + fatalErrors: TestInfoErrorImpl[]; }; export type EnvProducedPayload = [string, string | null][]; diff --git a/packages/playwright/src/common/process.ts b/packages/playwright/src/common/process.ts index 14ad995fed..a372139698 100644 --- a/packages/playwright/src/common/process.ts +++ b/packages/playwright/src/common/process.ts @@ -14,9 +14,8 @@ * limitations under the License. */ -import type { EnvProducedPayload, ProcessInitParams } from './ipc'; +import type { EnvProducedPayload, ProcessInitParams, TestInfoErrorImpl } from './ipc'; import { startProfiling, stopProfiling } from 'playwright-core/lib/utils'; -import type { TestInfoError } from '../../types/test'; import { serializeError } from '../util'; import { registerESMLoader } from './esmLoaderHost'; import { execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils'; @@ -29,7 +28,7 @@ export type ProtocolRequest = { export type ProtocolResponse = { id?: number; - error?: TestInfoError; + error?: TestInfoErrorImpl; method?: string; params?: any; result?: any; diff --git a/packages/playwright/src/matchers/matcherHint.ts b/packages/playwright/src/matchers/matcherHint.ts index 5ffc745263..200501c1bc 100644 --- a/packages/playwright/src/matchers/matcherHint.ts +++ b/packages/playwright/src/matchers/matcherHint.ts @@ -45,18 +45,18 @@ export type MatcherResult = { printedDiff?: string; }; -export class ExpectError extends Error { - matcherResult: { - message: string; - pass: boolean; - name?: string; - expected?: any; - actual?: any; - log?: string[]; - timeout?: number; - }; +export type MatcherResultProperty = Omit, 'message'> & { + message: string; +}; - constructor(jestError: ExpectError, customMessage: string, stackFrames: StackFrame[]) { +type JestError = Error & { + matcherResult: MatcherResultProperty; +}; + +export class ExpectError extends Error { + matcherResult: MatcherResultProperty; + + constructor(jestError: JestError, customMessage: string, stackFrames: StackFrame[]) { super(''); // Copy to erase the JestMatcherError constructor name from the console.log(error). this.name = jestError.name; diff --git a/packages/playwright/src/util.ts b/packages/playwright/src/util.ts index 460b3de07e..4046809433 100644 --- a/packages/playwright/src/util.ts +++ b/packages/playwright/src/util.ts @@ -21,10 +21,10 @@ import path from 'path'; import url from 'url'; import { debug, mime, minimatch, parseStackTraceLine } from 'playwright-core/lib/utilsBundle'; import { formatCallLog } from 'playwright-core/lib/utils'; -import type { TestInfoError } from './../types/test'; import type { Location } from './../types/testReporter'; import { calculateSha1, isRegExp, isString, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; import type { RawStack } from 'playwright-core/lib/utils'; +import type { TestInfoErrorImpl } from './common/ipc'; const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json')); @@ -62,7 +62,7 @@ export function filteredStackTrace(rawStack: RawStack): StackFrame[] { return frames; } -export function serializeError(error: Error | any): TestInfoError { +export function serializeError(error: Error | any): TestInfoErrorImpl { if (error instanceof Error) return filterStackTrace(error); return { diff --git a/packages/playwright/src/worker/DEPS.list b/packages/playwright/src/worker/DEPS.list index fb352ac389..ed3973d1fc 100644 --- a/packages/playwright/src/worker/DEPS.list +++ b/packages/playwright/src/worker/DEPS.list @@ -3,3 +3,4 @@ ../transform/ ../util.ts ../utilBundle.ts +../matchers/** diff --git a/packages/playwright/src/worker/testInfo.ts b/packages/playwright/src/worker/testInfo.ts index e41f1a9a52..ed71b1a751 100644 --- a/packages/playwright/src/worker/testInfo.ts +++ b/packages/playwright/src/worker/testInfo.ts @@ -17,8 +17,8 @@ import fs from 'fs'; import path from 'path'; import { captureRawStack, monotonicTime, zones, sanitizeForFilePath, stringifyStackFrames } from 'playwright-core/lib/utils'; -import type { TestInfoError, TestInfo, TestStatus, FullProject } from '../../types/test'; -import type { AttachmentPayload, StepBeginPayload, StepEndPayload, WorkerInitParams } from '../common/ipc'; +import type { TestInfo, TestStatus, FullProject } from '../../types/test'; +import type { AttachmentPayload, StepBeginPayload, StepEndPayload, TestInfoErrorImpl, WorkerInitParams } from '../common/ipc'; import type { TestCase } from '../common/test'; import { TimeoutManager, TimeoutManagerError, kMaxDeadline } from './timeoutManager'; import type { RunnableDescription } from './timeoutManager'; @@ -28,7 +28,7 @@ import { debugTest, filteredStackTrace, formatLocation, getContainedPath, normal import { TestTracing } from './testTracing'; import type { Attachment } from './testTracing'; import type { StackFrame } from '@protocol/channels'; -import { serializeWorkerError } from './util'; +import { testInfoError } from './util'; export interface TestStepInternal { complete(result: { error?: Error | unknown, attachments?: Attachment[] }): void; @@ -41,7 +41,7 @@ export interface TestStepInternal { endWallTime?: number; apiName?: string; params?: Record; - error?: TestInfoError; + error?: TestInfoErrorImpl; infectParentStepsWithError?: boolean; box?: boolean; isStage?: boolean; @@ -97,14 +97,14 @@ export class TestInfoImpl implements TestInfo { snapshotSuffix: string = ''; readonly outputDir: string; readonly snapshotDir: string; - errors: TestInfoError[] = []; + errors: TestInfoErrorImpl[] = []; readonly _attachmentsPush: (...items: TestInfo['attachments']) => number; - get error(): TestInfoError | undefined { + get error(): TestInfoErrorImpl | undefined { return this.errors[0]; } - set error(e: TestInfoError | undefined) { + set error(e: TestInfoErrorImpl | undefined) { if (e === undefined) throw new Error('Cannot assign testInfo.error undefined value!'); this.errors[0] = e; @@ -273,7 +273,7 @@ export class TestInfoImpl implements TestInfo { if (result.error) { if (typeof result.error === 'object' && !(result.error as any)?.[stepSymbol]) (result.error as any)[stepSymbol] = step; - const error = serializeWorkerError(result.error); + const error = testInfoError(result.error); if (data.boxedStack) error.stack = `${error.message}\n${stringifyStackFrames(data.boxedStack).join('\n')}`; step.error = error; @@ -331,7 +331,7 @@ export class TestInfoImpl implements TestInfo { _failWithError(error: Error | unknown) { if (this.status === 'passed' || this.status === 'skipped') this.status = error instanceof TimeoutManagerError ? 'timedOut' : 'failed'; - const serialized = serializeWorkerError(error); + const serialized = testInfoError(error); const step: TestStepInternal | undefined = typeof error === 'object' ? (error as any)?.[stepSymbol] : undefined; if (step && step.boxedStack) serialized.stack = `${(error as Error).name}: ${(error as Error).message}\n${stringifyStackFrames(step.boxedStack).join('\n')}`; diff --git a/packages/playwright/src/worker/testTracing.ts b/packages/playwright/src/worker/testTracing.ts index fed7fdde7e..5e7a3d80db 100644 --- a/packages/playwright/src/worker/testTracing.ts +++ b/packages/playwright/src/worker/testTracing.ts @@ -21,10 +21,10 @@ import fs from 'fs'; import path from 'path'; import { ManualPromise, calculateSha1, monotonicTime, createGuid, SerializedFS } from 'playwright-core/lib/utils'; import { yauzl, yazl } from 'playwright-core/lib/zipBundle'; -import type { TestInfo, TestInfoError } from '../../types/test'; import { filteredStackTrace } from '../util'; -import type { TraceMode, PlaywrightWorkerOptions } from '../../types/test'; +import type { TestInfo, TraceMode, PlaywrightWorkerOptions } from '../../types/test'; import type { TestInfoImpl } from './testInfo'; +import type { TestInfoErrorImpl } from '../common/ipc'; export type Attachment = TestInfo['attachments'][0]; export const testTraceEntryName = 'test.trace'; @@ -219,7 +219,7 @@ export class TestTracing { this._testInfo.attachments.push({ name: 'trace', path: tracePath, contentType: 'application/zip' }); } - appendForError(error: TestInfoError) { + appendForError(error: TestInfoErrorImpl) { const rawStack = error.stack?.split('\n') || []; const stack = rawStack ? filteredStackTrace(rawStack) : []; this._appendTraceEvent({ diff --git a/packages/playwright/src/worker/util.ts b/packages/playwright/src/worker/util.ts index d24d337191..a271f62c48 100644 --- a/packages/playwright/src/worker/util.ts +++ b/packages/playwright/src/worker/util.ts @@ -14,32 +14,13 @@ * limitations under the License. */ -import type { TestError } from '../../types/testReporter'; -import type { TestInfoError } from '../../types/test'; -import type { MatcherResult } from '../matchers/matcherHint'; +import type { TestInfoErrorImpl } from '../common/ipc'; +import { ExpectError } from '../matchers/matcherHint'; import { serializeError } from '../util'; - -type MatcherResultDetails = Pick; - -export function serializeWorkerError(error: Error | any): TestInfoError & MatcherResultDetails { - return { - ...serializeError(error), - ...serializeExpectDetails(error), - }; +export function testInfoError(error: Error | any): TestInfoErrorImpl { + const result = serializeError(error); + if (error instanceof ExpectError) + result.matcherResult = error.matcherResult; + return result; } - -function serializeExpectDetails(e: Error): MatcherResultDetails { - const matcherResult = (e as any).matcherResult as MatcherResult; - if (!matcherResult) - return {}; - return { - timeout: matcherResult.timeout, - matcherName: matcherResult.name, - locator: matcherResult.locator, - expected: matcherResult.printedExpected, - received: matcherResult.printedReceived, - log: matcherResult.log, - }; -} - diff --git a/packages/playwright/src/worker/workerMain.ts b/packages/playwright/src/worker/workerMain.ts index 5680c3ddb3..ed323a701b 100644 --- a/packages/playwright/src/worker/workerMain.ts +++ b/packages/playwright/src/worker/workerMain.ts @@ -16,7 +16,8 @@ import { colors } from 'playwright-core/lib/utilsBundle'; import { debugTest, relativeFilePath } from '../util'; -import { type TestBeginPayload, type TestEndPayload, type RunPayload, type DonePayload, type WorkerInitParams, type TeardownErrorsPayload, stdioChunkToParams } from '../common/ipc'; +import type { TestBeginPayload, TestEndPayload, RunPayload, DonePayload, WorkerInitParams, TeardownErrorsPayload, TestInfoErrorImpl } from '../common/ipc'; +import { stdioChunkToParams } from '../common/ipc'; import { setCurrentTestInfo, setIsWorkerProcess } from '../common/globals'; import { deserializeConfig } from '../common/configLoader'; import type { Suite, TestCase } from '../common/test'; @@ -28,11 +29,10 @@ import { ProcessRunner } from '../common/process'; import { loadTestFile } from '../common/testLoader'; import { applyRepeatEachIndex, bindFileSuiteToProject, filterTestsRemoveEmptySuites } from '../common/suiteUtils'; import { PoolBuilder } from '../common/poolBuilder'; -import type { TestInfoError } from '../../types/test'; import type { Location } from '../../types/testReporter'; import { inheritFixtureNames } from '../common/fixtures'; import { type TimeSlot } from './timeoutManager'; -import { serializeWorkerError } from './util'; +import { testInfoError } from './util'; export class WorkerMain extends ProcessRunner { private _params: WorkerInitParams; @@ -42,7 +42,7 @@ export class WorkerMain extends ProcessRunner { private _fixtureRunner: FixtureRunner; // Accumulated fatal errors that cannot be attributed to a test. - private _fatalErrors: TestInfoError[] = []; + private _fatalErrors: TestInfoErrorImpl[] = []; // Whether we should skip running remaining tests in this suite because // of a setup error, usually beforeAll hook. private _skipRemainingTestsInSuite: Suite | undefined; @@ -113,7 +113,7 @@ export class WorkerMain extends ProcessRunner { await fakeTestInfo._runAsStage({ title: 'worker cleanup', runnable }, () => gracefullyCloseAll()).catch(() => {}); this._fatalErrors.push(...fakeTestInfo.errors); } catch (e) { - this._fatalErrors.push(serializeWorkerError(e)); + this._fatalErrors.push(testInfoError(e)); } if (this._fatalErrors.length) { @@ -123,7 +123,7 @@ export class WorkerMain extends ProcessRunner { } } - private _appendProcessTeardownDiagnostics(error: TestInfoError) { + private _appendProcessTeardownDiagnostics(error: TestInfoErrorImpl) { if (!this._lastRunningTests.length) return; const count = this._totalRunningTests === 1 ? '1 test' : `${this._totalRunningTests} tests`; @@ -154,7 +154,7 @@ export class WorkerMain extends ProcessRunner { // No current test - fatal error. if (!this._currentTest) { if (!this._fatalErrors.length) - this._fatalErrors.push(serializeWorkerError(error)); + this._fatalErrors.push(testInfoError(error)); void this._stop(); return; } @@ -225,7 +225,7 @@ export class WorkerMain extends ProcessRunner { // In theory, we should run above code without any errors. // However, in the case we screwed up, or loadTestFile failed in the worker // but not in the runner, let's do a fatal error. - this._fatalErrors.push(serializeWorkerError(e)); + this._fatalErrors.push(testInfoError(e)); void this._stop(); } finally { const donePayload: DonePayload = {