diff --git a/packages/playwright-core/src/server/dom.ts b/packages/playwright-core/src/server/dom.ts index 288b8c2c8d1ac..269fb91b5a794 100644 --- a/packages/playwright-core/src/server/dom.ts +++ b/packages/playwright-core/src/server/dom.ts @@ -482,18 +482,15 @@ export class ElementHandle extends js.JSHandle { if (restoreModifiers) await this._page.keyboard.ensureModifiers(restoreModifiers); if (hitTargetInterceptionHandle) { + // We do not want to accidentally stall on non-committed navigation blocking the evaluate. const stopHitTargetInterception = this._frame.raceAgainstEvaluationStallingEvents(() => { return hitTargetInterceptionHandle.evaluate(h => h.stop()); }).catch(e => 'done' as const).finally(() => { hitTargetInterceptionHandle?.dispose(); }); - if (options.waitAfter !== false) { - // When noWaitAfter is passed, we do not want to accidentally stall on - // non-committed navigation blocking the evaluate. - const hitTargetResult = await stopHitTargetInterception; - if (hitTargetResult !== 'done') - return hitTargetResult; - } + const hitTargetResult = await stopHitTargetInterception; + if (hitTargetResult !== 'done') + return hitTargetResult; } progress.log(` ${options.trial ? 'trial ' : ''}${actionName} action done`); progress.log(' waiting for scheduled navigations to finish'); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index 1570df577085e..4421a766203d3 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -161,6 +161,8 @@ export class FrameManager { } async waitForSignalsCreatedBy(progress: Progress | null, waitAfter: boolean, action: () => Promise): Promise { + if (!process.env.PLAYWRIGHT_WAIT_AFTER_CLICK) + waitAfter = false; if (!waitAfter) return action(); const barrier = new SignalBarrier(progress); diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 9b85837b652f6..a21f75b656215 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -473,8 +473,6 @@ export class Page extends SdkObject { } private async _performWaitForNavigationCheck(progress: Progress) { - if (process.env.PLAYWRIGHT_SKIP_NAVIGATION_CHECK) - return; const mainFrame = this._frameManager.mainFrame(); if (!mainFrame || !mainFrame.pendingDocument()) return; diff --git a/tests/library/chromium/bfcache.spec.ts b/tests/library/chromium/bfcache.spec.ts index c60f4befaf475..3ae6e03a5f67a 100644 --- a/tests/library/chromium/bfcache.spec.ts +++ b/tests/library/chromium/bfcache.spec.ts @@ -30,6 +30,7 @@ test('bindings should work after restoring from bfcache', async ({ page, server await page.setContent(`click me`); await page.click('a'); + await expect(page).toHaveURL('about:blank'); await page.goBack({ waitUntil: 'commit' }); await page.evaluate('window.didShow'); diff --git a/tests/library/har.spec.ts b/tests/library/har.spec.ts index 6d920d133a093..7eb4def9efa4a 100644 --- a/tests/library/har.spec.ts +++ b/tests/library/har.spec.ts @@ -181,6 +181,7 @@ it('should include form params', async ({ contextFactory, server }, testInfo) => await page.goto(server.EMPTY_PAGE); await page.setContent(`
`); await page.click('input[type=submit]'); + await expect(page).toHaveURL('**/post'); const log = await getLog(); expect(log.entries[1].request.postData).toEqual({ mimeType: 'application/x-www-form-urlencoded', diff --git a/tests/page/page-autowaiting-basic.spec.ts b/tests/page/page-autowaiting-basic.spec.ts index dbfe482c33492..1f43278e193e8 100644 --- a/tests/page/page-autowaiting-basic.spec.ts +++ b/tests/page/page-autowaiting-basic.spec.ts @@ -19,24 +19,37 @@ import { stripAnsi } from 'tests/config/utils'; import type { TestServer } from '../config/testserver'; import { test as it, expect } from './pageTest'; -function initServer(server: TestServer): string[] { +function initStallingServer(server: TestServer, url?: string) { + let release: () => void; + const releasePromise = new Promise(r => release = r); + let route: () => void; + const routePromise = new Promise(r => route = r); const messages = []; - server.setRoute('/empty.html', async (req, res) => { + server.setRoute(url ?? '/empty.html', async (req, res) => { messages.push('route'); + route(); + await releasePromise; res.setHeader('Content-Type', 'text/html'); - res.end(``); + res.end(``); }); - return messages; + return { messages, release, routed: routePromise }; } -it('should await navigation when clicking anchor', async ({ page, server }) => { - const messages = initServer(server); - await page.setContent(`empty.html`); - await Promise.all([ - page.click('a').then(() => messages.push('click')), - page.waitForEvent('framenavigated').then(() => messages.push('navigated')), - ]); - expect(messages.join('|')).toBe('route|navigated|click'); +it('should await navigation before clicking anchor', async ({ page, server }) => { + const { messages, release, routed } = initStallingServer(server); + await page.setContent(`empty.html`); + + await page.click('a'); + await routed; + expect(messages.join('|')).toBe('route'); + + const click2 = page.click('button').then(() => messages.push('click2')); + await page.waitForTimeout(1000); + expect(messages.join('|')).toBe('route'); + + release(); + await click2; + expect(messages.join('|')).toBe('route|click2'); }); it('should not stall on JS navigation link', async ({ page, browserName }) => { @@ -44,57 +57,69 @@ it('should not stall on JS navigation link', async ({ page, browserName }) => { await page.click('a'); }); -it('should await cross-process navigation when clicking anchor', async ({ page, server }) => { - const messages = initServer(server); +it('should await cross-process navigation before clicking anchor', async ({ page, server }) => { + const { messages, release, routed } = initStallingServer(server); await page.setContent(`empty.html`); - await Promise.all([ - page.click('a').then(() => messages.push('click')), - page.waitForEvent('framenavigated').then(() => messages.push('navigated')), - ]); - expect(messages.join('|')).toBe('route|navigated|click'); -}); + await page.click('a'); + await routed; + expect(messages.join('|')).toBe('route'); -it('should await form-get on click', async ({ page, server }) => { - const messages = []; - server.setRoute('/empty.html?foo=bar', async (req, res) => { - messages.push('route'); - res.setHeader('Content-Type', 'text/html'); - res.end(``); - }); + const click2 = page.click('button').then(() => messages.push('click2')); + await page.waitForTimeout(1000); + expect(messages.join('|')).toBe('route'); + + release(); + await click2; + expect(messages.join('|')).toBe('route|click2'); +}); +it('should await form-get navigation before click', async ({ page, server }) => { + const { messages, release, routed } = initStallingServer(server, '/empty.html?foo=bar'); await page.setContent(`
`); - await Promise.all([ - page.click('input[type=submit]').then(() => messages.push('click')), - page.waitForEvent('framenavigated').then(() => messages.push('navigated')), - ]); - expect(messages.join('|')).toBe('route|navigated|click'); + await page.click('input[type=submit]'); + await routed; + expect(messages.join('|')).toBe('route'); + + const click2 = page.click('button').then(() => messages.push('click2')); + await page.waitForTimeout(1000); + expect(messages.join('|')).toBe('route'); + + release(); + await click2; + expect(messages.join('|')).toBe('route|click2'); }); -it('should await form-post on click', async ({ page, server }) => { - const messages = initServer(server); +it('should await form-post navigation before click', async ({ page, server }) => { + const { messages, release, routed } = initStallingServer(server); await page.setContent(`
`); - await Promise.all([ - page.click('input[type=submit]').then(() => messages.push('click')), - page.waitForEvent('framenavigated').then(() => messages.push('navigated')), - ]); - expect(messages.join('|')).toBe('route|navigated|click'); + await page.click('input[type=submit]'); + await routed; + expect(messages.join('|')).toBe('route'); + + const click2 = page.click('button').then(() => messages.push('click2')); + await page.waitForTimeout(1000); + expect(messages.join('|')).toBe('route'); + + release(); + await click2; + expect(messages.join('|')).toBe('route|click2'); }); -it('should work with noWaitAfter: true', async ({ page, server }) => { +it('should work without noWaitAfter when navigation is stalled', async ({ page, server }) => { server.setRoute('/empty.html', async () => {}); await page.setContent(`empty.html`); - await page.click('a', { noWaitAfter: true }); + await page.click('a'); }); it('should work with dblclick without noWaitAfter when navigation is stalled', async ({ page, server }) => { @@ -103,16 +128,6 @@ it('should work with dblclick without noWaitAfter when navigation is stalled', a await page.dblclick('a'); }); -it('should work with waitForLoadState(load)', async ({ page, server }) => { - const messages = initServer(server); - await page.setContent(`empty.html`); - await Promise.all([ - page.click('a').then(() => page.waitForLoadState('load')).then(() => messages.push('clickload')), - page.waitForEvent('load').then(() => messages.push('load')), - ]); - expect(messages.join('|')).toBe('route|load|clickload'); -}); - it('should work with goto following click', async ({ page, server }) => { server.setRoute('/login.html', async (req, res) => { res.setHeader('Content-Type', 'text/html'); @@ -130,17 +145,6 @@ it('should work with goto following click', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); }); -it('should report navigation in the log when clicking anchor', async ({ page, server, mode }) => { - it.skip(mode !== 'default'); - - await page.setContent(`click me`); - const __testHookAfterPointerAction = () => new Promise(f => setTimeout(f, 6000)); - const error = await page.click('a', { timeout: 5000, __testHookAfterPointerAction } as any).catch(e => e); - expect(error.message).toContain('page.click: Timeout 5000ms exceeded.'); - 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); diff --git a/tests/page/page-history.spec.ts b/tests/page/page-history.spec.ts index a709ade4cac69..3de76490ac7ea 100644 --- a/tests/page/page-history.spec.ts +++ b/tests/page/page-history.spec.ts @@ -95,6 +95,7 @@ it('goBack/goForward should work with bfcache-able pages', async ({ page, server await page.goto(server.PREFIX + '/cached/bfcached.html'); await page.setContent(`click me`); await page.click('a'); + await expect(page).toHaveURL(/.*foo$/); let response = await page.goBack(); expect(response.url()).toBe(server.PREFIX + '/cached/bfcached.html'); diff --git a/tests/page/page-network-request.spec.ts b/tests/page/page-network-request.spec.ts index f84a569c5ca5f..2341285a8292e 100644 --- a/tests/page/page-network-request.spec.ts +++ b/tests/page/page-network-request.spec.ts @@ -288,11 +288,10 @@ it('should parse the json post data', async ({ page, server }) => { it('should parse the data if content-type is application/x-www-form-urlencoded', async ({ page, server }) => { await page.goto(server.EMPTY_PAGE); server.setRoute('/post', (req, res) => res.end()); - let request = null; - page.on('request', r => request = r); + const requestPromise = page.waitForRequest('**/post'); await page.setContent(`
`); await page.click('input[type=submit]'); - expect(request).toBeTruthy(); + const request = await requestPromise; expect(request.postDataJSON()).toEqual({ 'foo': 'bar', 'baz': '123' }); }); diff --git a/tests/page/page-set-input-files.spec.ts b/tests/page/page-set-input-files.spec.ts index eaf1316f5cbaf..2aa7cc7942ae3 100644 --- a/tests/page/page-set-input-files.spec.ts +++ b/tests/page/page-set-input-files.spec.ts @@ -519,12 +519,12 @@ it('should accept single file', async ({ page, asset }) => { }); it('should detect mime type', async ({ page, server, asset }) => { - - let files: Record; + let resolveFiles; + const files = new Promise>(r => resolveFiles = r); server.setRoute('/upload', async (req, res) => { const form = new formidable.IncomingForm(); form.parse(req, function(err, fields, f) { - files = f as Record; + resolveFiles(f); res.end(); }); }); @@ -541,7 +541,7 @@ it('should detect mime type', async ({ page, server, asset }) => { page.click('input[type=submit]'), server.waitForRequest('/upload'), ]); - const { file1, file2 } = files; + const { file1, file2 } = await files; expect(file1.originalFilename).toBe('file-to-upload.txt'); expect(file1.mimetype).toBe('text/plain'); expect(fs.readFileSync(file1.filepath).toString()).toBe(