Skip to content

Commit af40eae

Browse files
committed
chore: make noWaitAfter a default
This changes the last actions, namely `click` and `press`, to not wait for the ongoing navigations after the action. Maintaining this behavior becomes tricky, because browsers move away from guaranteed synchronous navigations to optimized performance. See #34377 for more details. This is technically a breaking change. Most of the time, this should not be noticeable, because the next action will auto-wait the navigation and for any required conditions anyway. However, there are some patterns revealed by our tests that are affected: - Calling `goBack/goForward` immediately after an action. This pattern requires `expect(page).toHaveURL()` or a similar check inbetween. - Listening for network events during the action, and immediately asserting after the action. This pattern requires `waitForRequest()` or a similar promise-based waiter as recommended in best practices. We maintain the opt-out `env.PLAYWRIGHT_WAIT_AFTER_CLICK` that reverts to the old behavior for now. Additionally, previous opt-out option `env.PLAYWRIGHT_SKIP_NAVIGATION_CHECK` has been removed, because there have been just a single issue with it, that was immediately addressed in a patch release.
1 parent 96d4dc1 commit af40eae

File tree

9 files changed

+81
-78
lines changed

9 files changed

+81
-78
lines changed

packages/playwright-core/src/server/dom.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -482,18 +482,15 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
482482
if (restoreModifiers)
483483
await this._page.keyboard.ensureModifiers(restoreModifiers);
484484
if (hitTargetInterceptionHandle) {
485+
// We do not want to accidentally stall on non-committed navigation blocking the evaluate.
485486
const stopHitTargetInterception = this._frame.raceAgainstEvaluationStallingEvents(() => {
486487
return hitTargetInterceptionHandle.evaluate(h => h.stop());
487488
}).catch(e => 'done' as const).finally(() => {
488489
hitTargetInterceptionHandle?.dispose();
489490
});
490-
if (options.waitAfter !== false) {
491-
// When noWaitAfter is passed, we do not want to accidentally stall on
492-
// non-committed navigation blocking the evaluate.
493-
const hitTargetResult = await stopHitTargetInterception;
494-
if (hitTargetResult !== 'done')
495-
return hitTargetResult;
496-
}
491+
const hitTargetResult = await stopHitTargetInterception;
492+
if (hitTargetResult !== 'done')
493+
return hitTargetResult;
497494
}
498495
progress.log(` ${options.trial ? 'trial ' : ''}${actionName} action done`);
499496
progress.log(' waiting for scheduled navigations to finish');

packages/playwright-core/src/server/frames.ts

+2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ export class FrameManager {
161161
}
162162

163163
async waitForSignalsCreatedBy<T>(progress: Progress | null, waitAfter: boolean, action: () => Promise<T>): Promise<T> {
164+
if (!process.env.PLAYWRIGHT_WAIT_AFTER_CLICK)
165+
waitAfter = false;
164166
if (!waitAfter)
165167
return action();
166168
const barrier = new SignalBarrier(progress);

packages/playwright-core/src/server/page.ts

-2
Original file line numberDiff line numberDiff line change
@@ -473,8 +473,6 @@ export class Page extends SdkObject {
473473
}
474474

475475
private async _performWaitForNavigationCheck(progress: Progress) {
476-
if (process.env.PLAYWRIGHT_SKIP_NAVIGATION_CHECK)
477-
return;
478476
const mainFrame = this._frameManager.mainFrame();
479477
if (!mainFrame || !mainFrame.pendingDocument())
480478
return;

tests/library/chromium/bfcache.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ test('bindings should work after restoring from bfcache', async ({ page, server
3030

3131
await page.setContent(`<a href='about:blank'}>click me</a>`);
3232
await page.click('a');
33+
await expect(page).toHaveURL('about:blank');
3334

3435
await page.goBack({ waitUntil: 'commit' });
3536
await page.evaluate('window.didShow');

tests/library/har.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ it('should include form params', async ({ contextFactory, server }, testInfo) =>
181181
await page.goto(server.EMPTY_PAGE);
182182
await page.setContent(`<form method='POST' action='/post'><input type='text' name='foo' value='bar'><input type='number' name='baz' value='123'><input type='submit'></form>`);
183183
await page.click('input[type=submit]');
184+
await expect(page).toHaveURL('**/post');
184185
const log = await getLog();
185186
expect(log.entries[1].request.postData).toEqual({
186187
mimeType: 'application/x-www-form-urlencoded',

tests/page/page-autowaiting-basic.spec.ts

+66-62
Original file line numberDiff line numberDiff line change
@@ -19,82 +19,107 @@ import { stripAnsi } from 'tests/config/utils';
1919
import type { TestServer } from '../config/testserver';
2020
import { test as it, expect } from './pageTest';
2121

22-
function initServer(server: TestServer): string[] {
22+
function initStallingServer(server: TestServer, url?: string) {
23+
let release: () => void;
24+
const releasePromise = new Promise<void>(r => release = r);
25+
let route: () => void;
26+
const routePromise = new Promise<void>(r => route = r);
2327
const messages = [];
24-
server.setRoute('/empty.html', async (req, res) => {
28+
server.setRoute(url ?? '/empty.html', async (req, res) => {
2529
messages.push('route');
30+
route();
31+
await releasePromise;
2632
res.setHeader('Content-Type', 'text/html');
27-
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
33+
res.end(`<button onclick="window.__clicked=true">click me</button>`);
2834
});
29-
return messages;
35+
return { messages, release, routed: routePromise };
3036
}
3137

32-
it('should await navigation when clicking anchor', async ({ page, server }) => {
33-
const messages = initServer(server);
34-
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);
35-
await Promise.all([
36-
page.click('a').then(() => messages.push('click')),
37-
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
38-
]);
39-
expect(messages.join('|')).toBe('route|navigated|click');
38+
it('should await navigation before clicking anchor', async ({ page, server }) => {
39+
const { messages, release, routed } = initStallingServer(server);
40+
await page.setContent(`<a href="${server.EMPTY_PAGE}">empty.html</a>`);
41+
42+
await page.click('a');
43+
await routed;
44+
expect(messages.join('|')).toBe('route');
45+
46+
const click2 = page.click('button').then(() => messages.push('click2'));
47+
await page.waitForTimeout(1000);
48+
expect(messages.join('|')).toBe('route');
49+
50+
release();
51+
await click2;
52+
expect(messages.join('|')).toBe('route|click2');
4053
});
4154

4255
it('should not stall on JS navigation link', async ({ page, browserName }) => {
4356
await page.setContent(`<a href="javascript:console.log(1)">console.log</a>`);
4457
await page.click('a');
4558
});
4659

47-
it('should await cross-process navigation when clicking anchor', async ({ page, server }) => {
48-
const messages = initServer(server);
60+
it('should await cross-process navigation before clicking anchor', async ({ page, server }) => {
61+
const { messages, release, routed } = initStallingServer(server);
4962
await page.setContent(`<a href="${server.CROSS_PROCESS_PREFIX + '/empty.html'}">empty.html</a>`);
5063

51-
await Promise.all([
52-
page.click('a').then(() => messages.push('click')),
53-
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
54-
]);
55-
expect(messages.join('|')).toBe('route|navigated|click');
56-
});
64+
await page.click('a');
65+
await routed;
66+
expect(messages.join('|')).toBe('route');
5767

58-
it('should await form-get on click', async ({ page, server }) => {
59-
const messages = [];
60-
server.setRoute('/empty.html?foo=bar', async (req, res) => {
61-
messages.push('route');
62-
res.setHeader('Content-Type', 'text/html');
63-
res.end(`<link rel='stylesheet' href='./one-style.css'>`);
64-
});
68+
const click2 = page.click('button').then(() => messages.push('click2'));
69+
await page.waitForTimeout(1000);
70+
expect(messages.join('|')).toBe('route');
71+
72+
release();
73+
await click2;
74+
expect(messages.join('|')).toBe('route|click2');
75+
});
6576

77+
it('should await form-get navigation before click', async ({ page, server }) => {
78+
const { messages, release, routed } = initStallingServer(server, '/empty.html?foo=bar');
6679
await page.setContent(`
6780
<form action="${server.EMPTY_PAGE}" method="get">
6881
<input name="foo" value="bar">
6982
<input type="submit" value="Submit">
7083
</form>`);
7184

72-
await Promise.all([
73-
page.click('input[type=submit]').then(() => messages.push('click')),
74-
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
75-
]);
76-
expect(messages.join('|')).toBe('route|navigated|click');
85+
await page.click('input[type=submit]');
86+
await routed;
87+
expect(messages.join('|')).toBe('route');
88+
89+
const click2 = page.click('button').then(() => messages.push('click2'));
90+
await page.waitForTimeout(1000);
91+
expect(messages.join('|')).toBe('route');
92+
93+
release();
94+
await click2;
95+
expect(messages.join('|')).toBe('route|click2');
7796
});
7897

79-
it('should await form-post on click', async ({ page, server }) => {
80-
const messages = initServer(server);
98+
it('should await form-post navigation before click', async ({ page, server }) => {
99+
const { messages, release, routed } = initStallingServer(server);
81100
await page.setContent(`
82101
<form action="${server.EMPTY_PAGE}" method="post">
83102
<input name="foo" value="bar">
84103
<input type="submit" value="Submit">
85104
</form>`);
86105

87-
await Promise.all([
88-
page.click('input[type=submit]').then(() => messages.push('click')),
89-
page.waitForEvent('framenavigated').then(() => messages.push('navigated')),
90-
]);
91-
expect(messages.join('|')).toBe('route|navigated|click');
106+
await page.click('input[type=submit]');
107+
await routed;
108+
expect(messages.join('|')).toBe('route');
109+
110+
const click2 = page.click('button').then(() => messages.push('click2'));
111+
await page.waitForTimeout(1000);
112+
expect(messages.join('|')).toBe('route');
113+
114+
release();
115+
await click2;
116+
expect(messages.join('|')).toBe('route|click2');
92117
});
93118

94-
it('should work with noWaitAfter: true', async ({ page, server }) => {
119+
it('should work without noWaitAfter when navigation is stalled', async ({ page, server }) => {
95120
server.setRoute('/empty.html', async () => {});
96121
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);
97-
await page.click('a', { noWaitAfter: true });
122+
await page.click('a');
98123
});
99124

100125
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
103128
await page.dblclick('a');
104129
});
105130

106-
it('should work with waitForLoadState(load)', async ({ page, server }) => {
107-
const messages = initServer(server);
108-
await page.setContent(`<a id="anchor" href="${server.EMPTY_PAGE}">empty.html</a>`);
109-
await Promise.all([
110-
page.click('a').then(() => page.waitForLoadState('load')).then(() => messages.push('clickload')),
111-
page.waitForEvent('load').then(() => messages.push('load')),
112-
]);
113-
expect(messages.join('|')).toBe('route|load|clickload');
114-
});
115-
116131
it('should work with goto following click', async ({ page, server }) => {
117132
server.setRoute('/login.html', async (req, res) => {
118133
res.setHeader('Content-Type', 'text/html');
@@ -130,17 +145,6 @@ it('should work with goto following click', async ({ page, server }) => {
130145
await page.goto(server.EMPTY_PAGE);
131146
});
132147

133-
it('should report navigation in the log when clicking anchor', async ({ page, server, mode }) => {
134-
it.skip(mode !== 'default');
135-
136-
await page.setContent(`<a href="${server.PREFIX + '/frames/one-frame.html'}">click me</a>`);
137-
const __testHookAfterPointerAction = () => new Promise(f => setTimeout(f, 6000));
138-
const error = await page.click('a', { timeout: 5000, __testHookAfterPointerAction } as any).catch(e => e);
139-
expect(error.message).toContain('page.click: Timeout 5000ms exceeded.');
140-
expect(error.message).toContain('waiting for scheduled navigations to finish');
141-
expect(error.message).toContain(`navigated to "${server.PREFIX + '/frames/one-frame.html'}"`);
142-
});
143-
144148
it('should report and collapse log in action', async ({ page, server, mode }) => {
145149
await page.setContent(`<input id='checkbox' type='checkbox' style="visibility: hidden"></input>`);
146150
const error = await page.locator('input').click({ timeout: 5000 }).catch(e => e);

tests/page/page-history.spec.ts

+1
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ it('goBack/goForward should work with bfcache-able pages', async ({ page, server
9595
await page.goto(server.PREFIX + '/cached/bfcached.html');
9696
await page.setContent(`<a href=${JSON.stringify(server.PREFIX + '/cached/bfcached.html?foo')}>click me</a>`);
9797
await page.click('a');
98+
await expect(page).toHaveURL(/.*foo$/);
9899

99100
let response = await page.goBack();
100101
expect(response.url()).toBe(server.PREFIX + '/cached/bfcached.html');

tests/page/page-network-request.spec.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,10 @@ it('should parse the json post data', async ({ page, server }) => {
288288
it('should parse the data if content-type is application/x-www-form-urlencoded', async ({ page, server }) => {
289289
await page.goto(server.EMPTY_PAGE);
290290
server.setRoute('/post', (req, res) => res.end());
291-
let request = null;
292-
page.on('request', r => request = r);
291+
const requestPromise = page.waitForRequest('**/post');
293292
await page.setContent(`<form method='POST' action='/post'><input type='text' name='foo' value='bar'><input type='number' name='baz' value='123'><input type='submit'></form>`);
294293
await page.click('input[type=submit]');
295-
expect(request).toBeTruthy();
294+
const request = await requestPromise;
296295
expect(request.postDataJSON()).toEqual({ 'foo': 'bar', 'baz': '123' });
297296
});
298297

tests/page/page-set-input-files.spec.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -519,12 +519,12 @@ it('should accept single file', async ({ page, asset }) => {
519519
});
520520

521521
it('should detect mime type', async ({ page, server, asset }) => {
522-
523-
let files: Record<string, formidable.File>;
522+
let resolveFiles;
523+
const files = new Promise<Record<string, formidable.File>>(r => resolveFiles = r);
524524
server.setRoute('/upload', async (req, res) => {
525525
const form = new formidable.IncomingForm();
526526
form.parse(req, function(err, fields, f) {
527-
files = f as Record<string, formidable.File>;
527+
resolveFiles(f);
528528
res.end();
529529
});
530530
});
@@ -541,7 +541,7 @@ it('should detect mime type', async ({ page, server, asset }) => {
541541
page.click('input[type=submit]'),
542542
server.waitForRequest('/upload'),
543543
]);
544-
const { file1, file2 } = files;
544+
const { file1, file2 } = await files;
545545
expect(file1.originalFilename).toBe('file-to-upload.txt');
546546
expect(file1.mimetype).toBe('text/plain');
547547
expect(fs.readFileSync(file1.filepath).toString()).toBe(

0 commit comments

Comments
 (0)