diff --git a/packages/playwright-core/src/server/dispatchers/dispatcher.ts b/packages/playwright-core/src/server/dispatchers/dispatcher.ts index e14960b495..ce63891f4f 100644 --- a/packages/playwright-core/src/server/dispatchers/dispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/dispatcher.ts @@ -17,7 +17,7 @@ import { EventEmitter } from 'events'; import type * as channels from '@protocol/channels'; import { findValidator, ValidationError, createMetadataValidator, type ValidatorContext } from '../../protocol/validator'; -import { LongStandingScope, assert, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils'; +import { LongStandingScope, assert, compressCallLog, isUnderTest, monotonicTime, rewriteErrorMessage } from '../../utils'; import { TargetClosedError, isTargetClosedError, serializeError } from '../errors'; import type { CallMetadata } from '../instrumentation'; import { SdkObject } from '../instrumentation'; @@ -357,7 +357,7 @@ export class DispatcherConnection { } if (response.error) - response.log = callMetadata.log; + response.log = compressCallLog(callMetadata.log); this.onmessage(response); } } diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 4bb301bffd..dea4a52e78 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -299,7 +299,7 @@ export class ElementHandle extends js.JSHandle { while (progress.isRunning()) { if (retry) { - progress.log(`retrying ${actionName} action${options.trial ? ' (trial run)' : ''}, attempt #${retry}`); + progress.log(`retrying ${actionName} action${options.trial ? ' (trial run)' : ''}`); const timeout = waitTime[Math.min(retry - 1, waitTime.length - 1)]; if (timeout) { progress.log(` waiting ${timeout}ms`); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 597ff35951..129484a027 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -29,7 +29,7 @@ import * as types from './types'; import { BrowserContext } from './browserContext'; import type { Progress } from './progress'; import { ProgressController } from './progress'; -import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime, asLocator } from '../utils'; +import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime, asLocator, compressCallLog } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; import { debugLogger } from '../utils/debugLogger'; import type { CallMetadata } from './instrumentation'; @@ -1452,7 +1452,7 @@ export class Frame extends SdkObject { timeout -= elapsed; } if (timeout < 0) - return { matches: options.isNot, log: metadata.log, timedOut: true, received: lastIntermediateResult.received }; + return { matches: options.isNot, log: compressCallLog(metadata.log), timedOut: true, received: lastIntermediateResult.received }; // Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time. return await (new ProgressController(metadata, this)).run(async progress => { @@ -1473,7 +1473,7 @@ export class Frame extends SdkObject { // A: We want user to receive a friendly message containing the last intermediate result. if (js.isJavaScriptErrorInEvaluate(e) || isInvalidSelectorError(e)) throw e; - const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: metadata.log }; + const result: { matches: boolean, received?: any, log?: string[], timedOut?: boolean } = { matches: options.isNot, log: compressCallLog(metadata.log) }; if (lastIntermediateResult.isSet) result.received = lastIntermediateResult.received; if (e instanceof TimeoutError) diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 55f99250c8..d626b1ed3c 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -31,7 +31,7 @@ import * as accessibility from './accessibility'; import { FileChooser } from './fileChooser'; import type { Progress } from './progress'; import { ProgressController } from './progress'; -import { LongStandingScope, assert, createGuid, trimStringWithEllipsis } from '../utils'; +import { LongStandingScope, assert, compressCallLog, createGuid, trimStringWithEllipsis } from '../utils'; import { ManualPromise } from '../utils/manualPromise'; import { debugLogger } from '../utils/debugLogger'; import type { ImageComparatorOptions } from '../utils/comparators'; @@ -676,7 +676,7 @@ export class Page extends SdkObject { if (e instanceof TimeoutError && intermediateResult?.previous) errorMessage = `Failed to take two consecutive stable screenshots.`; return { - log: e.message ? [...metadata.log, e.message] : metadata.log, + log: compressCallLog(e.message ? [...metadata.log, e.message] : metadata.log), ...intermediateResult, errorMessage, timedOut: (e instanceof TimeoutError), diff --git a/packages/playwright-core/src/utils/stackTrace.ts b/packages/playwright-core/src/utils/stackTrace.ts index 84d08b0184..2e40968ebc 100644 --- a/packages/playwright-core/src/utils/stackTrace.ts +++ b/packages/playwright-core/src/utils/stackTrace.ts @@ -131,7 +131,13 @@ export function splitErrorMessage(message: string): { name: string, message: str export function formatCallLog(log: string[] | undefined): string { if (!log || !log.some(l => !!l)) return ''; + return ` +Call log: +${colors.dim(log.join('\n'))} +`; +} +export function compressCallLog(log: string[]): string[] { const lines: string[] = []; for (const block of findRepeatedSubsequences(log)) { @@ -148,10 +154,7 @@ export function formatCallLog(log: string[] | undefined): string { lines.push(whitespacePrefix + '- ' + line.trim()); } } - return ` -Call log: -${colors.dim(lines.join('\n'))} -`; + return lines; } export type ExpectZone = { diff --git a/tests/page/page-autowaiting-basic.spec.ts b/tests/page/page-autowaiting-basic.spec.ts index a2104530ef..dbfe482c33 100644 --- a/tests/page/page-autowaiting-basic.spec.ts +++ b/tests/page/page-autowaiting-basic.spec.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { stripAnsi } from 'tests/config/utils'; import type { TestServer } from '../config/testserver'; import { test as it, expect } from './pageTest'; @@ -139,3 +140,21 @@ it('should report navigation in the log when clicking anchor', async ({ page, se expect(error.message).toContain('waiting for scheduled navigations to finish'); expect(error.message).toContain(`navigated to "${server.PREFIX + '/frames/one-frame.html'}"`); }); + +it('should report and collapse log in action', async ({ page, server, mode }) => { + await page.setContent(``); + const error = await page.locator('input').click({ timeout: 5000 }).catch(e => e); + const message = stripAnsi(error.message); + expect(message).toContain(`Call log:`); + expect(message).toMatch(/\d+ × waiting for/); + const logLines = message.substring(message.indexOf('Call log:')).split('\n'); + expect(logLines.length).toBeLessThan(30); +}); + +it('should report and collapse log in expect', async ({ page, server, mode }) => { + await page.setContent(``); + const error = await expect(page.locator('input')).toBeVisible({ timeout: 5000 }).catch(e => e); + const message = stripAnsi(error.message); + expect(message).toContain(`Call log:`); + expect(message).toMatch(/\d+ × locator resolved to/); +});