diff --git a/examples/todomvc/tests/integration.spec.ts b/examples/todomvc/tests/integration.spec.ts index 007896bb2cd5e..16c51e2b0831f 100644 --- a/examples/todomvc/tests/integration.spec.ts +++ b/examples/todomvc/tests/integration.spec.ts @@ -17,8 +17,9 @@ const TODO_ITEMS = [ test.describe('New Todo', () => { test('should allow me to add todo items', async ({ page }) => { + test.setTimeout(5000); // create a new todo locator - const newTodo = page.getByPlaceholder('What needs to be done?'); + const newTodo = page.getByPlaceholder('What needs to be completed?'); // Create 1st todo. await newTodo.fill(TODO_ITEMS[0]); await newTodo.press('Enter'); diff --git a/packages/playwright-core/src/client/browser.ts b/packages/playwright-core/src/client/browser.ts index e36cb0d168627..279902de5c53b 100644 --- a/packages/playwright-core/src/client/browser.ts +++ b/packages/playwright-core/src/client/browser.ts @@ -72,6 +72,12 @@ export class Browser extends ChannelOwner implements ap }, true); } + async _stopPendingOperations(reason: string) { + return await this._wrapApiCall(async () => { + await this._channel.stopPendingOperations({ reason }); + }, true); + } + async _innerNewContext(options: BrowserContextOptions = {}, forReuse: boolean): Promise { options = { ...this._browserType._defaultContextOptions, ...options }; const contextOptions = await prepareBrowserContextParams(options); diff --git a/packages/playwright-core/src/client/connection.ts b/packages/playwright-core/src/client/connection.ts index 37f41856ac033..bc64cdba92168 100644 --- a/packages/playwright-core/src/client/connection.ts +++ b/packages/playwright-core/src/client/connection.ts @@ -35,7 +35,7 @@ import { WritableStream } from './writableStream'; import { debugLogger } from '../common/debugLogger'; import { SelectorsOwner } from './selectors'; import { Android, AndroidSocket, AndroidDevice } from './android'; -import { captureLibraryStackText, stringifyStackFrames } from '../utils/stackTrace'; +import { captureLibraryStackText } from '../utils/stackTrace'; import { Artifact } from './artifact'; import { EventEmitter } from 'events'; import { JsonPipe } from './jsonPipe'; @@ -65,7 +65,7 @@ export class Connection extends EventEmitter { readonly _objects = new Map(); onmessage = (message: object): void => {}; private _lastId = 0; - private _callbacks = new Map void, reject: (a: Error) => void, apiName: string | undefined, frames: channels.StackFrame[], type: string, method: string }>(); + private _callbacks = new Map void, reject: (a: Error) => void, apiName: string | undefined, type: string, method: string }>(); private _rootObject: Root; private _closedErrorMessage: string | undefined; private _isRemote = false; @@ -98,16 +98,6 @@ export class Connection extends EventEmitter { return await this._rootObject.initialize(); } - pendingProtocolCalls(): String { - const lines: string[] = []; - for (const call of this._callbacks.values()) { - if (!call.apiName) - continue; - lines.push(` - ${call.apiName}\n${stringifyStackFrames(call.frames)}\n`); - } - return lines.length ? 'Pending operations:\n' + lines.join('\n') : ''; - } - getObjectWithKnownName(guid: string): any { return this._objects.get(guid)!; } @@ -129,16 +119,16 @@ export class Connection extends EventEmitter { const type = object._type; const id = ++this._lastId; const message = { id, guid, method, params }; - if (debugLogger.isEnabled('channel:command')) { + if (debugLogger.isEnabled('channel')) { // Do not include metadata in debug logs to avoid noise. - debugLogger.log('channel:command', JSON.stringify(message)); + debugLogger.log('channel', 'SEND> ' + JSON.stringify(message)); } const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined; const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName }; if (this._tracingCount && frames && type !== 'LocalUtils') this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {}); this.onmessage({ ...message, metadata }); - return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, frames, type, method })); + return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method })); } dispatch(message: object) { @@ -147,8 +137,8 @@ export class Connection extends EventEmitter { const { id, guid, method, params, result, error } = message as any; if (id) { - if (debugLogger.isEnabled('channel:response')) - debugLogger.log('channel:response', JSON.stringify(message)); + if (debugLogger.isEnabled('channel')) + debugLogger.log('channel', ' setTimeout(f, 0)); } static reusableContextHash(params: channels.BrowserNewContextForReuseParams): string { diff --git a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts index 85e609747003a..c5622a3e9c9b7 100644 --- a/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/browserDispatcher.ts @@ -52,6 +52,10 @@ export class BrowserDispatcher extends Dispatcher { + await this._object.stopPendingOperations(params.reason); + } + async close(): Promise { await this._object.close(); } @@ -113,6 +117,10 @@ export class ConnectedBrowserDispatcher extends Dispatcher { + await this._object.stopPendingOperations(params.reason); + } + async close(): Promise { // Client should not send us Browser.close. } diff --git a/packages/playwright/src/index.ts b/packages/playwright/src/index.ts index 4046594ceb14c..66f78699cc027 100644 --- a/packages/playwright/src/index.ts +++ b/packages/playwright/src/index.ts @@ -330,8 +330,6 @@ const playwrightFixtures: Fixtures = ({ return context; }); - const prependToError = testInfoImpl._didTimeout ? (browser as any)._connection.pendingProtocolCalls() : ''; - let counter = 0; await Promise.all([...contexts.keys()].map(async context => { (context as any)[kStartedContextTearDown] = true; @@ -356,8 +354,6 @@ const playwrightFixtures: Fixtures = ({ } })); - if (prependToError) - testInfo.errors.push({ message: prependToError }); }, { scope: 'test', _title: 'context' } as any], _contextReuseMode: process.env.PW_TEST_REUSE_CONTEXT === 'when-possible' ? 'when-possible' : (process.env.PW_TEST_REUSE_CONTEXT ? 'force' : 'none'), @@ -378,6 +374,7 @@ const playwrightFixtures: Fixtures = ({ const context = await (browser as any)._newContextForReuse(defaultContextOptions); (context as any)[kIsReusedContext] = true; await use(context); + await (browser as any)._stopPendingOperations('Test ended'); }, page: async ({ context, _reuseContext }, use) => { diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index d4ba75c58e701..92fa7a4d9e4c6 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -1085,6 +1085,7 @@ export interface BrowserChannel extends BrowserEventTarget, Channel { defaultUserAgentForTest(params?: BrowserDefaultUserAgentForTestParams, metadata?: CallMetadata): Promise; newContext(params: BrowserNewContextParams, metadata?: CallMetadata): Promise; newContextForReuse(params: BrowserNewContextForReuseParams, metadata?: CallMetadata): Promise; + stopPendingOperations(params: BrowserStopPendingOperationsParams, metadata?: CallMetadata): Promise; newBrowserCDPSession(params?: BrowserNewBrowserCDPSessionParams, metadata?: CallMetadata): Promise; startTracing(params: BrowserStartTracingParams, metadata?: CallMetadata): Promise; stopTracing(params?: BrowserStopTracingParams, metadata?: CallMetadata): Promise; @@ -1339,6 +1340,13 @@ export type BrowserNewContextForReuseOptions = { export type BrowserNewContextForReuseResult = { context: BrowserContextChannel, }; +export type BrowserStopPendingOperationsParams = { + reason: string, +}; +export type BrowserStopPendingOperationsOptions = { + +}; +export type BrowserStopPendingOperationsResult = void; export type BrowserNewBrowserCDPSessionParams = {}; export type BrowserNewBrowserCDPSessionOptions = {}; export type BrowserNewBrowserCDPSessionResult = { diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index d1519caff646f..a58144939e65f 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -955,6 +955,10 @@ Browser: returns: context: BrowserContext + stopPendingOperations: + parameters: + reason: string + newBrowserCDPSession: returns: session: CDPSession diff --git a/tests/playwright-test/expect.spec.ts b/tests/playwright-test/expect.spec.ts index 7a0df898bb358..0833db485f17d 100644 --- a/tests/playwright-test/expect.spec.ts +++ b/tests/playwright-test/expect.spec.ts @@ -630,7 +630,6 @@ test('should print pending operations for toHaveText', async ({ runInlineTest }) expect(result.failed).toBe(1); expect(result.exitCode).toBe(1); const output = result.output; - expect(output).toContain('Pending operations:'); expect(output).toContain(`expect(locator).toHaveText(expected)`); expect(output).toContain('Expected string: "Text"'); expect(output).toContain('Received string: ""'); diff --git a/tests/playwright-test/playwright.spec.ts b/tests/playwright-test/playwright.spec.ts index e276374713d2b..d86358eb7c493 100644 --- a/tests/playwright-test/playwright.spec.ts +++ b/tests/playwright-test/playwright.spec.ts @@ -339,13 +339,8 @@ test('should report error and pending operations on timeout', async ({ runInline expect(result.exitCode).toBe(1); expect(result.passed).toBe(0); expect(result.failed).toBe(1); - expect(result.output).toContain('Pending operations:'); - expect(result.output).toContain('- locator.click'); - expect(result.output).toContain('a.test.ts:6:37'); - expect(result.output).toContain('- locator.textContent'); + expect(result.output).toContain('Error: locator.textContent: Page closed'); expect(result.output).toContain('a.test.ts:7:42'); - expect(result.output).toContain('waiting for'); - expect(result.output).toContain(`7 | page.getByText('More missing').textContent(),`); }); test('should report error on timeout with shared page', async ({ runInlineTest }) => { @@ -411,8 +406,7 @@ test('should not report waitForEventInfo as pending', async ({ runInlineTest }) expect(result.exitCode).toBe(1); expect(result.passed).toBe(0); expect(result.failed).toBe(1); - expect(result.output).toContain('Pending operations:'); - expect(result.output).toContain('- page.click'); + expect(result.output).toContain('page.click'); expect(result.output).toContain('a.test.ts:6:20'); expect(result.output).not.toContain('- page.waitForLoadState'); }); diff --git a/tests/playwright-test/timeout.spec.ts b/tests/playwright-test/timeout.spec.ts index d0a350cfae64a..741b10e62f59f 100644 --- a/tests/playwright-test/timeout.spec.ts +++ b/tests/playwright-test/timeout.spec.ts @@ -273,16 +273,16 @@ test('fixture timeout in beforeAll hook should not affect test', async ({ runInl import { test as base, expect } from '@playwright/test'; const test = base.extend({ fixture: [async ({}, use) => { - await new Promise(f => setTimeout(f, 500)); + await new Promise(f => setTimeout(f, 1000)); await use('hey'); - }, { timeout: 800 }], + }, { timeout: 1600 }], }); test.beforeAll(async ({ fixture }) => { // Nothing to see here. }); test('test ok', async ({}) => { - test.setTimeout(1000); - await new Promise(f => setTimeout(f, 800)); + test.setTimeout(2000); + await new Promise(f => setTimeout(f, 1600)); }); ` });