diff --git a/docs/api.md b/docs/api.md index 7658d8ae7a..07c1f802dd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -21,6 +21,7 @@ * [browserContext.newPage()](#browsercontextnewpage) * [browserContext.pages()](#browsercontextpages) * [browserContext.setCookies(cookies)](#browsercontextsetcookiescookies) + * [browserContext.setGeolocation(options)](#browsercontextsetgeolocationoptions) * [browserContext.setPermissions(origin, permissions[])](#browsercontextsetpermissionsorigin-permissions) - [class: BrowserFetcher](#class-browserfetcher) * [browserFetcher.canDownload(revision)](#browserfetchercandownloadrevision) @@ -235,8 +236,6 @@ * [chromiumCoverage.startJSCoverage([options])](#chromiumcoveragestartjscoverageoptions) * [chromiumCoverage.stopCSSCoverage()](#chromiumcoveragestopcsscoverage) * [chromiumCoverage.stopJSCoverage()](#chromiumcoveragestopjscoverage) -- [class: ChromiumOverrides](#class-chromiumoverrides) - * [chromiumOverrides.setGeolocation(options)](#chromiumoverridessetgeolocationoptions) - [class: ChromiumPlaywright](#class-chromiumplaywright) * [chromiumPlaywright.connect(options)](#chromiumplaywrightconnectoptions) * [chromiumPlaywright.createBrowserFetcher([options])](#chromiumplaywrightcreatebrowserfetcheroptions) @@ -465,6 +464,21 @@ An array of all pages inside the browser context. await browserContext.setCookies([cookieObject1, cookieObject2]); ``` +#### browserContext.setGeolocation(options) +- `options` <[Object]> + - `latitude` <[number]> Latitude between -90 and 90. + - `longitude` <[number]> Longitude between -180 and 180. + - `accuracy` <[number]> Optional non-negative accuracy value. +- returns: <[Promise]> + +Sets the page's geolocation. Passing null or undefined emulates position unavailable. + +```js +await browserContext.setGeolocation({latitude: 59.95, longitude: 30.31667}); +``` + +> **NOTE** Consider using [browserContext.setPermissions](#browsercontextsetpermissions-permissions) to grant permissions for the page to read its geolocation. + #### browserContext.setPermissions(origin, permissions[]) - `origin` <[string]> The [origin] to grant permissions to, e.g. "https://example.com". - `permissions` <[Array]<[string]>> An array of permissions to grant. All permissions that are not listed here will be automatically denied. Permissions can be one of the following values: @@ -3147,23 +3161,6 @@ _To output coverage in a form consumable by [Istanbul](https://github.com/istanb > **NOTE** JavaScript Coverage doesn't include anonymous scripts by default. However, scripts with sourceURLs are reported. -### class: ChromiumOverrides - -#### chromiumOverrides.setGeolocation(options) -- `options` <[Object]> - - `latitude` <[number]> Latitude between -90 and 90. - - `longitude` <[number]> Longitude between -180 and 180. - - `accuracy` <[number]> Optional non-negative accuracy value. -- returns: <[Promise]> - -Sets the page's geolocation. - -```js -await browserContext.overrides.setGeolocation({latitude: 59.95, longitude: 30.31667}); -``` - -> **NOTE** Consider using [browserContext.setPermissions](#browsercontextsetpermissions-permissions) to grant permissions for the page to read its geolocation. - ### class: ChromiumPlaywright Playwright module provides a method to launch a Chromium instance. diff --git a/src/browserContext.ts b/src/browserContext.ts index 28f983dc8d..439c8e13ed 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -31,6 +31,8 @@ export interface BrowserContextDelegate { setPermissions(origin: string, permissions: string[]): Promise; clearPermissions(): Promise; + + setGeolocation(geolocation: types.Geolocation | null): Promise; } export type BrowserContextOptions = { @@ -41,7 +43,8 @@ export type BrowserContextOptions = { mediaType?: input.MediaType, colorScheme?: input.ColorScheme, userAgent?: string, - timezoneId?: string + timezoneId?: string, + geolocation?: types.Geolocation }; export class BrowserContext { @@ -84,6 +87,21 @@ export class BrowserContext { await this._delegate.clearPermissions(); } + async setGeolocation(geolocation: types.Geolocation | null): Promise { + if (geolocation) { + geolocation.accuracy = geolocation.accuracy || 0; + const { longitude, latitude, accuracy } = geolocation; + if (longitude < -180 || longitude > 180) + throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`); + if (latitude < -90 || latitude > 90) + throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`); + if (accuracy < 0) + throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`); + } + this._options.geolocation = geolocation; + await this._delegate.setGeolocation(geolocation); + } + async close() { if (this._closed) return; diff --git a/src/chromium/crApi.ts b/src/chromium/crApi.ts index f9647824ad..31b208f66f 100644 --- a/src/chromium/crApi.ts +++ b/src/chromium/crApi.ts @@ -8,5 +8,4 @@ export { CRPlaywright as ChromiumPlaywright } from './crPlaywright'; export { CRTarget as ChromiumTarget } from './crTarget'; export { CRAccessibility as ChromiumAccessibility } from './features/crAccessibility'; export { CRCoverage as ChromiumCoverage } from './features/crCoverage'; -export { CROverrides as ChromiumOverrides } from './features/crOverrides'; export { CRWorker as ChromiumWorker } from './features/crWorkers'; diff --git a/src/chromium/crBrowser.ts b/src/chromium/crBrowser.ts index ee1cc50fcc..1feb4b78c7 100644 --- a/src/chromium/crBrowser.ts +++ b/src/chromium/crBrowser.ts @@ -26,7 +26,7 @@ import { Protocol } from './protocol'; import { CRPage } from './crPage'; import * as browser from '../browser'; import * as network from '../network'; -import { CROverrides } from './features/crOverrides'; +import * as types from '../types'; import { CRWorker } from './features/crWorkers'; import { ConnectionTransport } from '../transport'; import { readProtocolStream } from './crProtocolHelper'; @@ -137,10 +137,13 @@ export class CRBrowser extends browser.Browser { clearPermissions: async () => { await this._client.send('Browser.resetPermissions', { browserContextId: contextId || undefined }); - } + }, + setGeolocation: async (geolocation: types.Geolocation | null): Promise => { + for (const page of await context.pages()) + await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', geolocation || {}); + } }, options); - (context as any).overrides = new CROverrides(context); return context; } diff --git a/src/chromium/crPage.ts b/src/chromium/crPage.ts index b30af7b70c..b0be76f9bf 100644 --- a/src/chromium/crPage.ts +++ b/src/chromium/crPage.ts @@ -38,7 +38,6 @@ import { BrowserContext } from '../browserContext'; import * as types from '../types'; import * as input from '../input'; import { ConsoleMessage } from '../console'; -import { CROverrides } from './features/crOverrides'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -93,7 +92,6 @@ export class CRPage implements PageDelegate { this._client.send('Page.setLifecycleEventsEnabled', { enabled: true }), this._client.send('Runtime.enable', {}).then(() => this._ensureIsolatedWorld(UTILITY_WORLD_NAME)), this._networkManager.initialize(), - ((this._page.browserContext() as any).overrides as CROverrides)._applyOverrides(this), ]; const options = this._page.browserContext()._options; if (options.bypassCSP) @@ -112,6 +110,8 @@ export class CRPage implements PageDelegate { } if (options.timezoneId) promises.push(emulateTimezone(this._client, options.timezoneId)); + if (options.geolocation) + promises.push(this._client.send('Emulation.setGeolocationOverride', options.geolocation)); await Promise.all(promises); } diff --git a/src/chromium/features/crOverrides.ts b/src/chromium/features/crOverrides.ts deleted file mode 100644 index 7d8e2cca8f..0000000000 --- a/src/chromium/features/crOverrides.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright 2017 Google Inc. All rights reserved. - * Modifications 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. - */ - -import { BrowserContext } from '../../browserContext'; -import { CRPage } from '../crPage'; - -export class CROverrides { - private _context: BrowserContext; - private _geolocation: { longitude?: number; latitude?: number; accuracy?: number; } | null = null; - - constructor(context: BrowserContext) { - this._context = context; - } - - async setGeolocation(options: { longitude?: number; latitude?: number; accuracy?: (number | undefined); } | null) { - if (!options) { - for (const page of await this._context.pages()) - await (page._delegate as CRPage)._client.send('Emulation.clearGeolocationOverride', {}); - this._geolocation = null; - return; - } - - const { longitude, latitude, accuracy = 0} = options; - if (longitude < -180 || longitude > 180) - throw new Error(`Invalid longitude "${longitude}": precondition -180 <= LONGITUDE <= 180 failed.`); - if (latitude < -90 || latitude > 90) - throw new Error(`Invalid latitude "${latitude}": precondition -90 <= LATITUDE <= 90 failed.`); - if (accuracy < 0) - throw new Error(`Invalid accuracy "${accuracy}": precondition 0 <= ACCURACY failed.`); - this._geolocation = { longitude, latitude, accuracy }; - for (const page of await this._context.pages()) - await (page._delegate as CRPage)._client.send('Emulation.setGeolocationOverride', this._geolocation); - } - - async _applyOverrides(page: CRPage): Promise { - if (this._geolocation) - await page._client.send('Emulation.setGeolocationOverride', this._geolocation); - } -} diff --git a/src/firefox/ffBrowser.ts b/src/firefox/ffBrowser.ts index 211ab1eb30..6618c2c313 100644 --- a/src/firefox/ffBrowser.ts +++ b/src/firefox/ffBrowser.ts @@ -20,6 +20,7 @@ import { BrowserContext, BrowserContextOptions } from '../browserContext'; import { Events } from '../events'; import { assert, helper, RegisteredListener } from '../helper'; import * as network from '../network'; +import * as types from '../types'; import { Page } from '../page'; import { ConnectionTransport } from '../transport'; import { ConnectionEvents, FFConnection, FFSessionEvents } from './ffConnection'; @@ -199,8 +200,11 @@ export class FFBrowser extends browser.Browser { clearPermissions: async () => { await this._connection.send('Browser.resetPermissions', { browserContextId: browserContextId || undefined }); - } + }, + setGeolocation: async (geolocation: types.Geolocation | null): Promise => { + throw new Error('Geolocation emulation is not supported in Firefox'); + } }, options); return context; } diff --git a/src/types.ts b/src/types.ts index d6c94d714e..99d9cb87e3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,3 +56,9 @@ export type Credentials = { username: string; password: string; } + +export type Geolocation = { + longitude?: number; + latitude?: number; + accuracy?: number | undefined; +} diff --git a/src/webkit/wkBrowser.ts b/src/webkit/wkBrowser.ts index a2a7c92302..c89daf6016 100644 --- a/src/webkit/wkBrowser.ts +++ b/src/webkit/wkBrowser.ts @@ -19,6 +19,7 @@ import { EventEmitter } from 'events'; import { helper, RegisteredListener, debugError, assert } from '../helper'; import * as browser from '../browser'; import * as network from '../network'; +import * as types from '../types'; import { WKConnection, WKConnectionEvents, WKTargetSession } from './wkConnection'; import { Page } from '../page'; import { WKTarget } from './wkTarget'; @@ -203,12 +204,27 @@ export class WKBrowser extends browser.Browser { await this._connection.send('Browser.setCookies', { cookies: cc, browserContextId }); }, + setPermissions: async (origin: string, permissions: string[]): Promise => { - throw new Error('Permissions are not supported in WebKit'); + const webPermissionToProtocol = new Map([ + ['geolocation', 'geolocation'], + ]); + const filtered = permissions.map(permission => { + const protocolPermission = webPermissionToProtocol.get(permission); + if (!protocolPermission) + throw new Error('Unknown permission: ' + permission); + return protocolPermission; + }); + await this._connection.send('Browser.grantPermissions', { origin, browserContextId, permissions: filtered }); }, clearPermissions: async () => { - throw new Error('Permissions are not supported in WebKit'); + await this._connection.send('Browser.resetPermissions', { browserContextId }); + }, + + setGeolocation: async (geolocation: types.Geolocation | null): Promise => { + const payload: any = geolocation ? { ...geolocation, timestamp: Date.now() } : undefined; + await this._connection.send('Browser.setGeolocationOverride', { browserContextId, geolocation: payload }); } }, options); return context; diff --git a/test/chromium/geolocation.spec.js b/test/geolocation.spec.js similarity index 86% rename from test/chromium/geolocation.spec.js rename to test/geolocation.spec.js index 5fc51ee5e9..f978d85e5d 100644 --- a/test/chromium/geolocation.spec.js +++ b/test/geolocation.spec.js @@ -15,18 +15,18 @@ * limitations under the License. */ -module.exports.describe = function ({ testRunner, expect }) { +module.exports.describe = function ({ testRunner, expect, FFOX }) { const {describe, xdescribe, fdescribe} = testRunner; const {it, fit, xit, dit} = testRunner; const {beforeAll, beforeEach, afterAll, afterEach} = testRunner; // FIXME: not supported in WebKit (as well as Emulation domain in general). // It was removed from WebKit in https://webkit.org/b/126630 - describe('Overrides.setGeolocation', function() { + describe.skip(FFOX)('Overrides.setGeolocation', function() { it('should work', async({page, server, context}) => { await context.setPermissions(server.PREFIX, ['geolocation']); await page.goto(server.EMPTY_PAGE); - await context.overrides.setGeolocation({longitude: 10, latitude: 10}); + await context.setGeolocation({longitude: 10, latitude: 10}); const geolocation = await page.evaluate(() => new Promise(resolve => navigator.geolocation.getCurrentPosition(position => { resolve({latitude: position.coords.latitude, longitude: position.coords.longitude}); }))); @@ -38,7 +38,7 @@ module.exports.describe = function ({ testRunner, expect }) { it('should throw when invalid longitude', async({context}) => { let error = null; try { - await context.overrides.setGeolocation({longitude: 200, latitude: 10}); + await context.setGeolocation({longitude: 200, latitude: 10}); } catch (e) { error = e; } diff --git a/test/playwright.spec.js b/test/playwright.spec.js index 3f472e4ee7..b5f08ead9b 100644 --- a/test/playwright.spec.js +++ b/test/playwright.spec.js @@ -166,11 +166,11 @@ module.exports.describe = ({testRunner, product, playwrightPath}) => { testRunner.loadTests(require('./screenshot.spec.js'), testOptions); testRunner.loadTests(require('./waittask.spec.js'), testOptions); testRunner.loadTests(require('./interception.spec.js'), testOptions); + testRunner.loadTests(require('./geolocation.spec.js'), testOptions); if (CHROME) { testRunner.loadTests(require('./chromium/chromium.spec.js'), testOptions); testRunner.loadTests(require('./chromium/coverage.spec.js'), testOptions); - testRunner.loadTests(require('./chromium/geolocation.spec.js'), testOptions); testRunner.loadTests(require('./chromium/pdf.spec.js'), testOptions); testRunner.loadTests(require('./chromium/session.spec.js'), testOptions); testRunner.loadTests(require('./chromium/workers.spec.js'), testOptions);