Skip to content

Commit da30847

Browse files
authored
feat(firefox): apply emulation to all pages in the browser context (#931)
1 parent 90367c1 commit da30847

File tree

9 files changed

+218
-151
lines changed

9 files changed

+218
-151
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"main": "index.js",
1010
"playwright": {
1111
"chromium_revision": "740289",
12-
"firefox_revision": "1025",
12+
"firefox_revision": "1028",
1313
"webkit_revision": "1141"
1414
},
1515
"scripts": {

src/firefox/ffBrowser.ts

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@
1818
import { Browser, createPageInNewContext } from '../browser';
1919
import { BrowserContext, BrowserContextOptions } from '../browserContext';
2020
import { Events } from '../events';
21-
import { assert, helper, RegisteredListener } from '../helper';
21+
import { assert, helper, RegisteredListener, debugError } from '../helper';
2222
import * as network from '../network';
2323
import * as types from '../types';
2424
import { Page } from '../page';
25-
import { ConnectionEvents, FFConnection, FFSessionEvents } from './ffConnection';
25+
import { ConnectionEvents, FFConnection, FFSessionEvents, FFSession } from './ffConnection';
2626
import { FFPage } from './ffPage';
2727
import * as platform from '../platform';
2828
import { Protocol } from './protocol';
@@ -68,8 +68,17 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
6868
}
6969

7070
async newContext(options: BrowserContextOptions = {}): Promise<BrowserContext> {
71+
const viewport = options.viewport ? {
72+
viewportSize: { width: options.viewport.width, height: options.viewport.height },
73+
isMobile: !!options.viewport.isMobile,
74+
deviceScaleFactor: options.viewport.deviceScaleFactor || 1,
75+
hasTouch: !!options.viewport.isMobile,
76+
} : undefined;
7177
const {browserContextId} = await this._connection.send('Target.createBrowserContext', {
72-
userAgent: options.userAgent
78+
userAgent: options.userAgent,
79+
bypassCSP: options.bypassCSP,
80+
javaScriptDisabled: options.javaScriptEnabled === false ? true : undefined,
81+
viewport,
7382
});
7483
// TODO: move ignoreHTTPSErrors to browser context level.
7584
if (options.ignoreHTTPSErrors)
@@ -121,14 +130,6 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
121130
const context = browserContextId ? this._contexts.get(browserContextId)! : this._defaultContext;
122131
const target = new Target(this._connection, this, context, targetId, type, url, openerId);
123132
this._targets.set(targetId, target);
124-
const opener = target.opener();
125-
if (opener && opener._pagePromise) {
126-
const openerPage = await opener._pagePromise;
127-
if (openerPage.listenerCount(Events.Page.Popup)) {
128-
const popupPage = await target.page();
129-
openerPage.emit(Events.Page.Popup, popupPage);
130-
}
131-
}
132133
}
133134

134135
_onTargetDestroyed(payload: Protocol.Target.targetDestroyedPayload) {
@@ -144,11 +145,18 @@ export class FFBrowser extends platform.EventEmitter implements Browser {
144145
target._url = url;
145146
}
146147

147-
_onAttachedToTarget(payload: Protocol.Target.attachedToTargetPayload) {
148-
const {targetId, type} = payload.targetInfo;
148+
async _onAttachedToTarget(payload: Protocol.Target.attachedToTargetPayload) {
149+
const {targetId} = payload.targetInfo;
149150
const target = this._targets.get(targetId)!;
150-
if (type === 'page')
151-
target.page();
151+
target._initPagePromise(this._connection.getSession(payload.sessionId)!);
152+
const opener = target.opener();
153+
if (opener && opener._pagePromise) {
154+
const openerPage = await opener._pagePromise;
155+
if (openerPage.listenerCount(Events.Page.Popup)) {
156+
const popupPage = await target.page();
157+
openerPage.emit(Events.Page.Popup, popupPage);
158+
}
159+
}
152160
}
153161

154162
async close() {
@@ -278,25 +286,27 @@ class Target {
278286
return this._context;
279287
}
280288

281-
page(): Promise<Page> {
289+
async page(): Promise<Page> {
282290
if (this._type !== 'page')
283291
throw new Error(`Cannot create page for "${this._type}" target`);
284-
if (!this._pagePromise) {
285-
this._pagePromise = new Promise(async f => {
286-
const session = await this._connection.createSession(this._targetId);
287-
this._ffPage = new FFPage(session, this._context, async () => {
288-
const openerTarget = this.opener();
289-
if (!openerTarget)
290-
return null;
291-
return await openerTarget.page();
292-
});
293-
const page = this._ffPage._page;
294-
session.once(FFSessionEvents.Disconnected, () => page._didDisconnect());
295-
await this._ffPage._initialize();
296-
f(page);
292+
if (!this._pagePromise)
293+
await this._connection.send('Target.attachToTarget', {targetId: this._targetId});
294+
return this._pagePromise!;
295+
}
296+
297+
_initPagePromise(session: FFSession) {
298+
this._pagePromise = new Promise(async f => {
299+
this._ffPage = new FFPage(session, this._context, async () => {
300+
const openerTarget = this.opener();
301+
if (!openerTarget)
302+
return null;
303+
return await openerTarget.page();
297304
});
298-
}
299-
return this._pagePromise;
305+
const page = this._ffPage._page;
306+
session.once(FFSessionEvents.Disconnected, () => page._didDisconnect());
307+
await this._ffPage._initialize().catch(debugError);
308+
f(page);
309+
});
300310
}
301311

302312
browser() {

src/firefox/ffConnection.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,8 @@ export class FFConnection extends platform.EventEmitter {
143143
this._transport.close();
144144
}
145145

146-
async createSession(targetId: string): Promise<FFSession> {
147-
const {sessionId} = await this.send('Target.attachToTarget', {targetId});
148-
return this._sessions.get(sessionId)!;
146+
getSession(sessionId: string): FFSession | null {
147+
return this._sessions.get(sessionId) || null;
149148
}
150149
}
151150

src/firefox/ffPage.ts

Lines changed: 12 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -77,26 +77,13 @@ export class FFPage implements PageDelegate {
7777
}
7878

7979
async _initialize() {
80-
const promises: Promise<any>[] = [
81-
this._session.send('Runtime.enable').then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)),
82-
this._session.send('Network.enable'),
83-
this._session.send('Page.enable'),
84-
];
85-
const options = this._page.context()._options;
86-
if (options.viewport)
87-
promises.push(this._updateViewport());
88-
if (options.bypassCSP)
89-
promises.push(this._session.send('Page.setBypassCSP', { enabled: true }));
90-
if (options.javaScriptEnabled === false)
91-
promises.push(this._session.send('Page.setJavascriptEnabled', { enabled: false }));
92-
await Promise.all(promises);
93-
}
94-
95-
async _ensureIsolatedWorld(name: string) {
96-
await this._session.send('Page.addScriptToEvaluateOnNewDocument', {
97-
script: '',
98-
worldName: name,
99-
});
80+
await Promise.all([
81+
this._session.send('Page.addScriptToEvaluateOnNewDocument', {
82+
script: '',
83+
worldName: UTILITY_WORLD_NAME,
84+
}),
85+
new Promise(f => this._session.once('Page.ready', f)),
86+
]);
10087
}
10188

10289
_onExecutionContextCreated(payload: Protocol.Runtime.executionContextCreatedPayload) {
@@ -268,22 +255,10 @@ export class FFPage implements PageDelegate {
268255

269256
async setViewportSize(viewportSize: types.Size): Promise<void> {
270257
assert(this._page._state.viewportSize === viewportSize);
271-
await this._updateViewport();
272-
}
273-
274-
async _updateViewport() {
275-
let viewport = this._page.context()._options.viewport || { width: 0, height: 0 };
276-
const viewportSize = this._page._state.viewportSize;
277-
if (viewportSize)
278-
viewport = { ...viewport, ...viewportSize };
279-
await this._session.send('Page.setViewport', {
280-
viewport: {
281-
width: viewport.width,
282-
height: viewport.height,
283-
isMobile: !!viewport.isMobile,
284-
deviceScaleFactor: viewport.deviceScaleFactor || 1,
285-
hasTouch: !!viewport.isMobile,
286-
isLandscape: viewport.width > viewport.height
258+
await this._session.send('Page.setViewportSize', {
259+
viewportSize: {
260+
width: viewportSize.width,
261+
height: viewportSize.height,
287262
},
288263
});
289264
}
@@ -373,7 +348,7 @@ export class FFPage implements PageDelegate {
373348
}
374349

375350
async resetViewport(): Promise<void> {
376-
await this._session.send('Page.setViewport', { viewport: null });
351+
await this._session.send('Page.setViewportSize', { viewportSize: null });
377352
}
378353

379354
async getContentFrame(handle: dom.ElementHandle): Promise<frames.Frame | null> {

test/browsercontext.spec.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,6 @@ module.exports.describe = function({testRunner, expect, playwright, CHROMIUM, WE
3434
await context.close();
3535
expect(browser.contexts().length).toBe(0);
3636
});
37-
it.skip(CHROMIUM)('popup should inherit user agent', async function({newContext, server}) {
38-
const context = await newContext({
39-
userAgent: 'hey'
40-
});
41-
const page = await context.newPage();
42-
await page.goto(server.EMPTY_PAGE);
43-
const evaluatePromise = page.evaluate(url => window.open(url), server.PREFIX + '/dummy.html');
44-
const popupPromise = page.waitForEvent('popup');
45-
const request = await server.waitForRequest('/dummy.html');
46-
await evaluatePromise;
47-
await popupPromise;
48-
expect(request.headers['user-agent']).toBe('hey');
49-
});
5037
it('window.open should use parent tab context', async function({newContext, server}) {
5138
const context = await newContext();
5239
const page = await context.newPage();

test/launcher.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ module.exports.describe = function({testRunner, expect, defaultBrowserOptions, p
154154
const remote = await playwright.connect({ wsEndpoint: browserServer.wsEndpoint() });
155155
const page = await remote.newPage();
156156
const watchdog = page.waitForSelector('div', { timeout: 60000 }).catch(e => e);
157-
157+
158158
// Make sure the previous waitForSelector has time to make it to the browser before we disconnect.
159159
await page.waitForSelector('body');
160160

test/page.spec.js

Lines changed: 2 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -125,70 +125,6 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
125125
});
126126
});
127127

128-
describe('Page.Events.Popup', function() {
129-
it('should work', async({page}) => {
130-
const [popup] = await Promise.all([
131-
new Promise(x => page.once('popup', x)),
132-
page.evaluate(() => window.open('about:blank')),
133-
]);
134-
expect(await page.evaluate(() => !!window.opener)).toBe(false);
135-
expect(await popup.evaluate(() => !!window.opener)).toBe(true);
136-
});
137-
it('should work with noopener', async({page}) => {
138-
const [popup] = await Promise.all([
139-
new Promise(x => page.once('popup', x)),
140-
page.evaluate(() => window.open('about:blank', null, 'noopener')),
141-
]);
142-
expect(await page.evaluate(() => !!window.opener)).toBe(false);
143-
expect(await popup.evaluate(() => !!window.opener)).toBe(false);
144-
});
145-
it.skip(FFOX)('should work with clicking target=_blank', async({page, server}) => {
146-
await page.goto(server.EMPTY_PAGE);
147-
await page.setContent('<a target=_blank rel="opener" href="/one-style.html">yo</a>');
148-
const [popup] = await Promise.all([
149-
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
150-
page.click('a'),
151-
]);
152-
expect(await page.evaluate(() => !!window.opener)).toBe(false);
153-
expect(await popup.evaluate(() => !!window.opener)).toBe(true);
154-
});
155-
it.skip(FFOX)('should work with fake-clicking target=_blank and rel=noopener', async({page, server}) => {
156-
// TODO: FFOX sends events for "one-style.html" request to both pages.
157-
await page.goto(server.EMPTY_PAGE);
158-
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
159-
const [popup] = await Promise.all([
160-
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
161-
page.$eval('a', a => a.click()),
162-
]);
163-
expect(await page.evaluate(() => !!window.opener)).toBe(false);
164-
// TODO: At this point popup might still have about:blank as the current document.
165-
// FFOX is slow enough to trigger this. We should do something about popups api.
166-
expect(await popup.evaluate(() => !!window.opener)).toBe(false);
167-
});
168-
it.skip(FFOX)('should work with clicking target=_blank and rel=noopener', async({page, server}) => {
169-
await page.goto(server.EMPTY_PAGE);
170-
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
171-
const [popup] = await Promise.all([
172-
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
173-
page.click('a'),
174-
]);
175-
expect(await page.evaluate(() => !!window.opener)).toBe(false);
176-
expect(await popup.evaluate(() => !!window.opener)).toBe(false);
177-
});
178-
it.skip(FFOX)('should not treat navigations as new popups', async({page, server}) => {
179-
await page.goto(server.EMPTY_PAGE);
180-
await page.setContent('<a target=_blank rel=noopener href="/one-style.html">yo</a>');
181-
const [popup] = await Promise.all([
182-
page.waitForEvent('popup').then(async popup => { await popup.waitForLoadState(); return popup; }),
183-
page.click('a'),
184-
]);
185-
let badSecondPopup = false;
186-
page.on('popup', () => badSecondPopup = true);
187-
await popup.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
188-
expect(badSecondPopup).toBe(false);
189-
});
190-
});
191-
192128
describe('Page.opener', function() {
193129
it('should provide access to the opener page', async({page}) => {
194130
const [popup] = await Promise.all([
@@ -306,8 +242,9 @@ module.exports.describe = function({testRunner, expect, headless, playwright, FF
306242

307243
describe('Page.Events.DOMContentLoaded', function() {
308244
it('should fire when expected', async({page, server}) => {
309-
page.goto('about:blank');
245+
const navigatedPromise = page.goto('about:blank');
310246
await waitEvent(page, 'domcontentloaded');
247+
await navigatedPromise;
311248
});
312249
});
313250

test/playwright.spec.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => {
197197
testRunner.loadTests(require('./browser.spec.js'), testOptions);
198198
testRunner.loadTests(require('./browsercontext.spec.js'), testOptions);
199199
testRunner.loadTests(require('./ignorehttpserrors.spec.js'), testOptions);
200+
testRunner.loadTests(require('./popup.spec.js'), testOptions);
200201
});
201202

202203
// Top-level tests that launch Browser themselves.

0 commit comments

Comments
 (0)