Skip to content

Commit a1c6ab2

Browse files
authored
fix(reload): do not throw when reload is racing with navigation (#5113) (#5171)
When `page.reload()` is racing against the renderer-initiated navigation, we might end up with `waitForNavigation()` being rejected before the reload implementation is able to catch it. To avoid that, carefully use Promise.all and await `waitForNavigation` from the get go. Same happens to `page.goForward()` and `page.goBack()`.
1 parent 0e216bc commit a1c6ab2

File tree

2 files changed

+95
-13
lines changed

2 files changed

+95
-13
lines changed

src/server/page.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -293,9 +293,13 @@ export class Page extends EventEmitter {
293293
async reload(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
294294
this.mainFrame().setupNavigationProgressController(controller);
295295
const response = await controller.run(async progress => {
296-
const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
297-
await this._delegate.reload();
298-
return waitPromise;
296+
// Note: waitForNavigation may fail before we get response to reload(),
297+
// so we should await it immediately.
298+
const [response] = await Promise.all([
299+
this.mainFrame()._waitForNavigation(progress, options),
300+
this._delegate.reload(),
301+
]);
302+
return response;
299303
}, this._timeoutSettings.navigationTimeout(options));
300304
await this._doSlowMo();
301305
return response;
@@ -304,13 +308,20 @@ export class Page extends EventEmitter {
304308
async goBack(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
305309
this.mainFrame().setupNavigationProgressController(controller);
306310
const response = await controller.run(async progress => {
307-
const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
311+
// Note: waitForNavigation may fail before we get response to goBack,
312+
// so we should catch it immediately.
313+
let error: Error | undefined;
314+
const waitPromise = this.mainFrame()._waitForNavigation(progress, options).catch(e => {
315+
error = e;
316+
return null;
317+
});
308318
const result = await this._delegate.goBack();
309-
if (!result) {
310-
waitPromise.catch(() => {});
319+
if (!result)
311320
return null;
312-
}
313-
return waitPromise;
321+
const response = await waitPromise;
322+
if (error)
323+
throw error;
324+
return response;
314325
}, this._timeoutSettings.navigationTimeout(options));
315326
await this._doSlowMo();
316327
return response;
@@ -319,13 +330,20 @@ export class Page extends EventEmitter {
319330
async goForward(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
320331
this.mainFrame().setupNavigationProgressController(controller);
321332
const response = await controller.run(async progress => {
322-
const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
333+
// Note: waitForNavigation may fail before we get response to goForward,
334+
// so we should catch it immediately.
335+
let error: Error | undefined;
336+
const waitPromise = this.mainFrame()._waitForNavigation(progress, options).catch(e => {
337+
error = e;
338+
return null;
339+
});
323340
const result = await this._delegate.goForward();
324-
if (!result) {
325-
waitPromise.catch(() => {});
341+
if (!result)
326342
return null;
327-
}
328-
return waitPromise;
343+
const response = await waitPromise;
344+
if (error)
345+
throw error;
346+
return response;
329347
}, this._timeoutSettings.navigationTimeout(options));
330348
await this._doSlowMo();
331349
return response;

test/page-history.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,67 @@ it('page.reload should work with data url', async ({page, server}) => {
9292
expect(await page.reload()).toBe(null);
9393
expect(await page.content()).toContain('hello');
9494
});
95+
96+
it('page.reload during renderer-initiated navigation', async ({page, server}) => {
97+
await page.goto(server.PREFIX + '/one-style.html');
98+
await page.setContent(`<form method='POST' action='/post'>Form is here<input type='submit'></form>`);
99+
server.setRoute('/post', (req, res) => {});
100+
101+
let callback;
102+
const reloadFailedPromise = new Promise(f => callback = f);
103+
page.once('request', async () => {
104+
await page.reload().catch(e => {});
105+
callback();
106+
});
107+
const clickPromise = page.click('input[type=submit]').catch(e => {});
108+
await reloadFailedPromise;
109+
await clickPromise;
110+
111+
// Form submit should be canceled, and reload should eventually arrive
112+
// to the original one-style.html.
113+
await page.waitForSelector('text=hello');
114+
});
115+
116+
it('page.goBack during renderer-initiated navigation', async ({page, server}) => {
117+
await page.goto(server.PREFIX + '/one-style.html');
118+
await page.goto(server.EMPTY_PAGE);
119+
await page.setContent(`<form method='POST' action='/post'>Form is here<input type='submit'></form>`);
120+
server.setRoute('/post', (req, res) => {});
121+
122+
let callback;
123+
const reloadFailedPromise = new Promise(f => callback = f);
124+
page.once('request', async () => {
125+
await page.goBack().catch(e => {});
126+
callback();
127+
});
128+
const clickPromise = page.click('input[type=submit]').catch(e => {});
129+
await reloadFailedPromise;
130+
await clickPromise;
131+
132+
// Form submit should be canceled, and goBack should eventually arrive
133+
// to the original one-style.html.
134+
await page.waitForSelector('text=hello');
135+
});
136+
137+
it('page.goForward during renderer-initiated navigation', async ({page, server}) => {
138+
await page.goto(server.EMPTY_PAGE);
139+
await page.goto(server.PREFIX + '/one-style.html');
140+
await page.goBack();
141+
142+
await page.setContent(`<form method='POST' action='/post'>Form is here<input type='submit'></form>`);
143+
server.setRoute('/post', (req, res) => {});
144+
145+
let callback;
146+
const reloadFailedPromise = new Promise(f => callback = f);
147+
page.once('request', async () => {
148+
await page.goForward().catch(e => {});
149+
callback();
150+
});
151+
const clickPromise = page.click('input[type=submit]').catch(e => {});
152+
await reloadFailedPromise;
153+
await clickPromise;
154+
155+
// Form submit should be canceled, and goForward should eventually arrive
156+
// to the original one-style.html.
157+
await page.waitForSelector('text=hello');
158+
});

0 commit comments

Comments
 (0)