diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index acbfeee9ab..4d128ecccd 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; import { Events } from './events'; import { Events as CommonEvents } from '../events'; import { assert, helper } from '../helper'; @@ -156,11 +155,11 @@ export class CRBrowser extends browser.Browser { }); await this._client.send('Browser.grantPermissions', { origin, browserContextId: contextId || undefined, permissions: filtered }); }, - + clearPermissions: async () => { await this._client.send('Browser.resetPermissions', { browserContextId: contextId || undefined }); } - + }, options); overrides = new CROverrides(context); (context as any).overrides = overrides; diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index f5c9ab7ac1..dad354f4a6 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -210,11 +210,11 @@ export class FFBrowser extends browser.Browser { }); await this._connection.send('Browser.grantPermissions', {origin, browserContextId: browserContextId || undefined, permissions: filtered}); }, - + clearPermissions: async () => { await this._connection.send('Browser.resetPermissions', { browserContextId: browserContextId || undefined }); } - + }, options); return context; } diff --git a/src/frames.ts b/src/frames.ts index 6830ad5fdd..b685fc149e 100644 --- a/src/frames.ts +++ b/src/frames.ts @@ -336,6 +336,17 @@ export class Frame { return watcher.navigationResponse(); } + async waitForLoadState(options?: NavigateOptions): Promise { + const watcher = new LifecycleWatcher(this, options, false /* supportUrlMatch */); + const error = await Promise.race([ + watcher.timeoutOrTerminationPromise, + watcher.lifecyclePromise + ]); + watcher.dispose(); + if (error) + throw error; + } + _context(contextType: ContextType): Promise { if (this._detached) throw new Error(`Execution Context is not available in detached frame "${this.url()}" (are you trying to evaluate?)`); @@ -886,7 +897,7 @@ class LifecycleWatcher { this._checkLifecycleComplete(); } - private _urlMatches(urlString: string): boolean { + _urlMatches(urlString: string): boolean { return !this._urlMatch || helper.urlMatches(urlString, this._urlMatch); } @@ -969,12 +980,13 @@ class LifecycleWatcher { } private _checkLifecycleComplete() { - // We expect navigation to commit. if (!this._checkLifecycleRecursively(this._frame, this._expectedLifecycle)) return; - this._lifecycleCallback(); - if (this._hasSameDocumentNavigation) - this._sameDocumentNavigationCompleteCallback(); + if (this._urlMatches(this._frame.url())) { + this._lifecycleCallback(); + if (this._hasSameDocumentNavigation) + this._sameDocumentNavigationCompleteCallback(); + } if (this._frame._lastDocumentId === this._expectedDocumentId) this._newDocumentNavigationCompleteCallback(); } diff --git a/src/page.ts b/src/page.ts index 4ee26a24c9..e31b2ffac1 100644 --- a/src/page.ts +++ b/src/page.ts @@ -301,6 +301,10 @@ export class Page extends EventEmitter { return this.mainFrame().waitForNavigation(options); } + async waitForLoadState(options?: frames.NavigateOptions): Promise { + return this.mainFrame().waitForLoadState(options); + } + async waitForEvent(event: string, options: Function | (types.TimeoutOptions & { predicate?: Function }) = {}): Promise { if (typeof options === 'function') options = { predicate: options }; diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index 0c1f405e19..9def4ba524 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -210,7 +210,7 @@ export class WKBrowser extends browser.Browser { setPermissions: async (origin: string, permissions: string[]): Promise => { throw new Error('Permissions are not supported in WebKit'); }, - + clearPermissions: async () => { throw new Error('Permissions are not supported in WebKit'); } diff --git a/test/navigation.spec.js b/test/navigation.spec.js index af1c622f41..eab97cb736 100644 --- a/test/navigation.spec.js +++ b/test/navigation.spec.js @@ -698,6 +698,65 @@ module.exports.describe = function({testRunner, expect, playwright, FFOX, CHROME expect(response2.url()).toBe(server.PREFIX + '/frame.html'); expect(response3.url()).toBe(server.PREFIX + '/frame.html?foo=bar'); }); + it('should work with url match for same document navigations', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let resolved = false; + const waitPromise = page.waitForNavigation({ url: /third\.html/ }).then(() => resolved = true); + expect(resolved).toBe(false); + await page.evaluate(() => { + history.pushState({}, '', '/first.html'); + }); + expect(resolved).toBe(false); + await page.evaluate(() => { + history.pushState({}, '', '/second.html'); + }); + expect(resolved).toBe(false); + await page.evaluate(() => { + history.pushState({}, '', '/third.html'); + }); + await waitPromise; + expect(resolved).toBe(true); + }); + }); + + describe('Page.waitForLoadState', () => { + it('should pick up ongoing navigation', async({page, server}) => { + let response = null; + server.setRoute('/one-style.css', (req, res) => response = res); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html'); + await server.waitForRequest('/one-style.css'); + const waitPromise = page.waitForLoadState(); + response.statusCode = 404; + response.end('Not found'); + await waitPromise; + await navigationPromise; + }); + it('should respect timeout', async({page, server}) => { + let response = null; + server.setRoute('/one-style.css', (req, res) => response = res); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html'); + await server.waitForRequest('/one-style.css'); + const error = await page.waitForLoadState({ timeout: 1 }).catch(e => e); + expect(error.message).toBe('Navigation timeout of 1 ms exceeded'); + response.statusCode = 404; + response.end('Not found'); + await navigationPromise; + }); + it('should resolve immediately if loaded', async({page, server}) => { + await page.goto(server.PREFIX + '/one-style.html'); + await page.waitForLoadState(); + }); + it('should resolve immediately if load state matches', async({page, server}) => { + await page.goto(server.EMPTY_PAGE); + let response = null; + server.setRoute('/one-style.css', (req, res) => response = res); + const navigationPromise = page.goto(server.PREFIX + '/one-style.html'); + await server.waitForRequest('/one-style.css'); + await page.waitForLoadState({ waitUntil: 'domcontentloaded' }); + response.statusCode = 404; + response.end('Not found'); + await navigationPromise; + }); }); describe('Page.goBack', function() {