diff --git a/package.json b/package.json index 0833782fbf..1483882b14 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "main": "index.js", "playwright": { "chromium_revision": "740289", - "firefox_revision": "1025", + "firefox_revision": "1028", "webkit_revision": "1141" }, "scripts": { diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 82cea3e284..6fc992abb5 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -18,11 +18,11 @@ import { Browser, createPageInNewContext } from '../browser'; import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { Events } from '../events'; -import { assert, helper, RegisteredListener } from '../helper'; +import { assert, helper, RegisteredListener, debugError } from '../helper'; import * as network from '../network'; import * as types from '../types'; import { Page } from '../page'; -import { ConnectionEvents, FFConnection, FFSessionEvents } from './ffConnection'; +import { ConnectionEvents, FFConnection, FFSessionEvents, FFSession } from './ffConnection'; import { FFPage } from './ffPage'; import * as platform from '../platform'; import { Protocol } from './protocol'; @@ -68,8 +68,17 @@ export class FFBrowser extends platform.EventEmitter implements Browser { } async newContext(options: BrowserContextOptions = {}): Promise { + const viewport = options.viewport ? { + viewportSize: { width: options.viewport.width, height: options.viewport.height }, + isMobile: !!options.viewport.isMobile, + deviceScaleFactor: options.viewport.deviceScaleFactor || 1, + hasTouch: !!options.viewport.isMobile, + } : undefined; const {browserContextId} = await this._connection.send('Target.createBrowserContext', { - userAgent: options.userAgent + userAgent: options.userAgent, + bypassCSP: options.bypassCSP, + javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined, + viewport, }); // TODO: move ignoreHTTPSErrors to browser context level. if (options.ignoreHTTPSErrors) @@ -121,14 +130,6 @@ export class FFBrowser extends platform.EventEmitter implements Browser { const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext; const target = new Target(this._connection, this, context, targetId, type, url, openerId); this._targets.set(targetId, target); - const opener = target.opener(); - if (opener && opener._pagePromise) { - const openerPage = await opener._pagePromise; - if (openerPage.listenerCount(Events.Page.Popup)) { - const popupPage = await target.page(); - openerPage.emit(Events.Page.Popup, popupPage); - } - } } _onTargetDestroyed(payload: Protocol.Target.targetDestroyedPayload) { @@ -144,11 +145,18 @@ export class FFBrowser extends platform.EventEmitter implements Browser { target._url = url; } - _onAttachedToTarget(payload: Protocol.Target.attachedToTargetPayload) { - const {targetId, type} = payload.targetInfo; + async _onAttachedToTarget(payload: Protocol.Target.attachedToTargetPayload) { + const {targetId} = payload.targetInfo; const target = this._targets.get(targetId)!; - if (type === 'page') - target.page(); + target._initPagePromise(this._connection.getSession(payload.sessionId)!); + const opener = target.opener(); + if (opener && opener._pagePromise) { + const openerPage = await opener._pagePromise; + if (openerPage.listenerCount(Events.Page.Popup)) { + const popupPage = await target.page(); + openerPage.emit(Events.Page.Popup, popupPage); + } + } } async close() { @@ -278,25 +286,27 @@ class Target { return this._context; } - page(): Promise { + async page(): Promise { if (this._type !== 'page') throw new Error(`Cannot create page for "${this._type}" target`); - if (!this._pagePromise) { - this._pagePromise = new Promise(async f => { - const session = await this._connection.createSession(this._targetId); - this._ffPage = new FFPage(session, this._context, async () => { - const openerTarget = this.opener(); - if (!openerTarget) - return null; - return await openerTarget.page(); - }); - const page = this._ffPage._page; - session.once(FFSessionEvents.Disconnected, () => page._didDisconnect()); - await this._ffPage._initialize(); - f(page); + if (!this._pagePromise) + await this._connection.send('Target.attachToTarget', {targetId: this._targetId}); + return this._pagePromise!; + } + + _initPagePromise(session: FFSession) { + this._pagePromise = new Promise(async f => { + this._ffPage = new FFPage(session, this._context, async () => { + const openerTarget = this.opener(); + if (!openerTarget) + return null; + return await openerTarget.page(); }); - } - return this._pagePromise; + const page = this._ffPage._page; + session.once(FFSessionEvents.Disconnected, () => page._didDisconnect()); + await this._ffPage._initialize().catch(debugError); + f(page); + }); } browser() { diff --git a/src/firefox/ffConnection.ts b/src/firefox/ffConnection.ts index 8ccb0f1c97..206ff0f710 100644 --- a/src/firefox/ffConnection.ts +++ b/src/firefox/ffConnection.ts @@ -143,9 +143,8 @@ export class FFConnection extends platform.EventEmitter { this._transport.close(); } - async createSession(targetId: string): Promise { - const {sessionId} = await this.send('Target.attachToTarget', {targetId}); - return this._sessions.get(sessionId)!; + getSession(sessionId: string): FFSession | null { + return this._sessions.get(sessionId) || null; } } diff --git a/src/firefox/ffPage.ts b/src/firefox/ffPage.ts index 96e58f6e20..4780a528de 100644 --- a/src/firefox/ffPage.ts +++ b/src/firefox/ffPage.ts @@ -77,26 +77,13 @@ export class FFPage implements PageDelegate { } async _initialize() { - const promises: Promise[] = [ - this._session.send('Runtime.enable').then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), - this._session.send('Network.enable'), - this._session.send('Page.enable'), - ]; - const options = this._page.context()._options; - if (options.viewport) - promises.push(this._updateViewport()); - if (options.bypassCSP) - promises.push(this._session.send('Page.setBypassCSP', { enabled: true })); - if (options.javaScriptEnabled === false) - promises.push(this._session.send('Page.setJavascriptEnabled', { enabled: false })); - await Promise.all(promises); - } - - async _ensureIsolatedWorld(name: string) { - await this._session.send('Page.addScriptToEvaluateOnNewDocument', { - script: '', - worldName: name, - }); + await Promise.all([ + this._session.send('Page.addScriptToEvaluateOnNewDocument', { + script: '', + worldName: UTILITY_WORLD_NAME, + }), + new Promise(f => this._session.once('Page.ready', f)), + ]); } _onExecutionContextCreated(payload: Protocol.Runtime.executionContextCreatedPayload) { @@ -268,22 +255,10 @@ export class FFPage implements PageDelegate { async setViewportSize(viewportSize: types.Size): Promise { assert(this._page._state.viewportSize === viewportSize); - await this._updateViewport(); - } - - async _updateViewport() { - let viewport = this._page.context()._options.viewport || { width: 0, height: 0 }; - const viewportSize = this._page._state.viewportSize; - if (viewportSize) - viewport = { ...viewport, ...viewportSize }; - await this._session.send('Page.setViewport', { - viewport: { - width: viewport.width, - height: viewport.height, - isMobile: !!viewport.isMobile, - deviceScaleFactor: viewport.deviceScaleFactor || 1, - hasTouch: !!viewport.isMobile, - isLandscape: viewport.width > viewport.height + await this._session.send('Page.setViewportSize', { + viewportSize: { + width: viewportSize.width, + height: viewportSize.height, }, }); } @@ -373,7 +348,7 @@ export class FFPage implements PageDelegate { } async resetViewport(): Promise { - await this._session.send('Page.setViewport', { viewport: null }); + await this._session.send('Page.setViewportSize', { viewportSize: null }); } async getContentFrame(handle: dom.ElementHandle): Promise { diff --git a/test/browsercontext.spec.js b/test/browsercontext.spec.js index 6dc1e8a326..afc2bef868 100644 --- a/test/browsercontext.spec.js +++ b/test/browsercontext.spec.js @@ -34,19 +34,6 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE await context.close(); expect(browser.contexts().length).toBe(0); }); - it.skip(CHROMIUM)('popup should inherit user agent', async function({newContext, server}) { - const context = await newContext({ - userAgent: 'hey' - }); - const page = await context.newPage(); - await page.goto(server.EMPTY_PAGE); - const evaluatePromise = page.evaluate(url => window.open(url), server.PREFIX + '/dummy.html'); - const popupPromise = page.waitForEvent('popup'); - const request = await server.waitForRequest('/dummy.html'); - await evaluatePromise; - await popupPromise; - expect(request.headers['user-agent']).toBe('hey'); - }); it('window.open should use parent tab context', async function({newContext, server}) { const context = await newContext(); const page = await context.newPage(); diff --git a/test/launcher.spec.js b/test/launcher.spec.js index b097b427f3..08e87b7518 100644 --- a/test/launcher.spec.js +++ b/test/launcher.spec.js @@ -154,7 +154,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() }); const page = await remote.newPage(); const watchdog = page.waitForSelector('div', { timeout: 60000 }).catch(e => e); - + // Make sure the previous waitForSelector has time to make it to the browser before we disconnect. await page.waitForSelector('body'); diff --git a/test/page.spec.js b/test/page.spec.js index e42d215218..3a8a176a72 100644 --- a/test/page.spec.js +++ b/test/page.spec.js @@ -125,70 +125,6 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF }); }); - describe('Page.Events.Popup', function() { - it('should work', async({page}) => { - const [popup] = await Promise.all([ - new Promise(x => page.once('popup', x)), - page.evaluate(() => window.open('about:blank')), - ]); - expect(await page.evaluate(() => !!window.opener)).toBe(false); - expect(await popup.evaluate(() => !!window.opener)).toBe(true); - }); - it('should work with noopener', async({page}) => { - const [popup] = await Promise.all([ - new Promise(x => page.once('popup', x)), - page.evaluate(() => window.open('about:blank', null, 'noopener')), - ]); - expect(await page.evaluate(() => !!window.opener)).toBe(false); - expect(await popup.evaluate(() => !!window.opener)).toBe(false); - }); - it.skip(FFOX)('should work with clicking target=_blank', async({page, server}) => { - await page.goto(server.EMPTY_PAGE); - await page.setContent('yo'); - const [popup] = await Promise.all([ - page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), - page.click('a'), - ]); - expect(await page.evaluate(() => !!window.opener)).toBe(false); - expect(await popup.evaluate(() => !!window.opener)).toBe(true); - }); - it.skip(FFOX)('should work with fake-clicking target=_blank and rel=noopener', async({page, server}) => { - // TODO: FFOX sends events for "one-style.html" request to both pages. - await page.goto(server.EMPTY_PAGE); - await page.setContent('yo'); - const [popup] = await Promise.all([ - page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), - page.$eval('a', a => a.click()), - ]); - expect(await page.evaluate(() => !!window.opener)).toBe(false); - // TODO: At this point popup might still have about:blank as the current document. - // FFOX is slow enough to trigger this. We should do something about popups api. - expect(await popup.evaluate(() => !!window.opener)).toBe(false); - }); - it.skip(FFOX)('should work with clicking target=_blank and rel=noopener', async({page, server}) => { - await page.goto(server.EMPTY_PAGE); - await page.setContent('yo'); - const [popup] = await Promise.all([ - page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), - page.click('a'), - ]); - expect(await page.evaluate(() => !!window.opener)).toBe(false); - expect(await popup.evaluate(() => !!window.opener)).toBe(false); - }); - it.skip(FFOX)('should not treat navigations as new popups', async({page, server}) => { - await page.goto(server.EMPTY_PAGE); - await page.setContent('yo'); - const [popup] = await Promise.all([ - page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), - page.click('a'), - ]); - let badSecondPopup = false; - page.on('popup', () => badSecondPopup = true); - await popup.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); - expect(badSecondPopup).toBe(false); - }); - }); - describe('Page.opener', function() { it('should provide access to the opener page', async({page}) => { const [popup] = await Promise.all([ @@ -306,8 +242,9 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF describe('Page.Events.DOMContentLoaded', function() { it('should fire when expected', async({page, server}) => { - page.goto('about:blank'); + const navigatedPromise = page.goto('about:blank'); await waitEvent(page, 'domcontentloaded'); + await navigatedPromise; }); }); diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 9385c7c093..63aedfebc1 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -197,6 +197,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { testRunner.loadTests(require('./browser.spec.js'), testOptions); testRunner.loadTests(require('./browsercontext.spec.js'), testOptions); testRunner.loadTests(require('./ignorehttpserrors.spec.js'), testOptions); + testRunner.loadTests(require('./popup.spec.js'), testOptions); }); // Top-level tests that launch Browser themselves. diff --git a/test/popup.spec.js b/test/popup.spec.js new file mode 100644 index 0000000000..8a2364e8ca --- /dev/null +++ b/test/popup.spec.js @@ -0,0 +1,158 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WEBKIT, FFOX}) { + const {describe, xdescribe, fdescribe} = testRunner; + const {it, fit, xit, dit} = testRunner; + const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; + + describe('window.open', function() { + it.skip(CHROMIUM)('should inherit user agent from browser context', async function({newContext, server}) { + const context = await newContext({ + userAgent: 'hey' + }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const evaluatePromise = page.evaluate(url => window.open(url), server.PREFIX + '/dummy.html'); + const popupPromise = page.waitForEvent('popup'); + const request = await server.waitForRequest('/dummy.html'); + await evaluatePromise; + await popupPromise; + await context.close(); + expect(request.headers['user-agent']).toBe('hey'); + }); + it.skip(CHROMIUM)('should inherit touch support from browser context', async function({newContext, server}) { + const context = await newContext({ + viewport: { width: 400, height: 500, isMobile: true } + }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const hasTouch = await page.evaluate(() => { + const win = window.open(''); + return 'ontouchstart' in win; + }); + await context.close(); + expect(hasTouch).toBe(true); + }); + it.skip(CHROMIUM || WEBKIT)('should inherit viewport size from browser context', async function({newContext, server}) { + const context = await newContext({ + viewport: { width: 400, height: 500, isMobile: true } + }); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + const size = await page.evaluate(() => { + const win = window.open('about:blank'); + return { width: win.innerWidth, height: win.innerHeight }; + }); + await context.close(); + expect(size).toEqual({width: 400, height: 500}); + }); + }); + + describe('Page.Events.Popup', function() { + it('should work', async({newContext}) => { + const context = await newContext(); + const page = await context.newPage(); + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.evaluate(() => window.__popup = window.open('about:blank')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + await context.close(); + }); + it.skip(CHROMIUM)('should work with empty url', async({newContext}) => { + const context = await newContext(); + const page = await context.newPage(); + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.evaluate(() => window.__popup = window.open('')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + await context.close(); + }); + it('should work with noopener', async({newContext}) => { + const context = await newContext(); + const page = await context.newPage(); + const [popup] = await Promise.all([ + new Promise(x => page.once('popup', x)), + page.evaluate(() => window.__popup = window.open('about:blank', null, 'noopener')), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + await context.close(); + }); + it.skip(FFOX)('should work with clicking target=_blank', async({newContext, server}) => { + const context = await newContext(); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(true); + await context.close(); + }); + it.skip(FFOX)('should work with fake-clicking target=_blank and rel=noopener', async({newContext, server}) => { + const context = await newContext(); + const page = await context.newPage(); + // TODO: FFOX sends events for "one-style.html" request to both pages. + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), + page.$eval('a', a => a.click()), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + // TODO: At this point popup might still have about:blank as the current document. + // FFOX is slow enough to trigger this. We should do something about popups api. + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + await context.close(); + }); + it.skip(FFOX)('should work with clicking target=_blank and rel=noopener', async({newContext, server}) => { + const context = await newContext(); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), + page.click('a'), + ]); + expect(await page.evaluate(() => !!window.opener)).toBe(false); + expect(await popup.evaluate(() => !!window.opener)).toBe(false); + await context.close(); + }); + it.skip(FFOX)('should not treat navigations as new popups', async({newContext, server}) => { + const context = await newContext(); + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent('yo'); + const [popup] = await Promise.all([ + page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }), + page.click('a'), + ]); + let badSecondPopup = false; + page.on('popup', () => badSecondPopup = true); + await popup.goto(server.CROSS_PROCESS_PREFIX + '/empty.html'); + await context.close(); + expect(badSecondPopup).toBe(false); + }); + }); + +};