diff --git a/javascript/node/selenium-webdriver/BUILD.bazel b/javascript/node/selenium-webdriver/BUILD.bazel index 4eb990028eee8..766dbf02d7953 100644 --- a/javascript/node/selenium-webdriver/BUILD.bazel +++ b/javascript/node/selenium-webdriver/BUILD.bazel @@ -38,6 +38,7 @@ js_library( "devtools/*.js", "common/*.js", "bidi/*.js", + "bidi/external/*.js", ]), deps = [ ":node_modules/@bazel/runfiles", diff --git a/javascript/node/selenium-webdriver/bidi/external/permissions.js b/javascript/node/selenium-webdriver/bidi/external/permissions.js new file mode 100644 index 0000000000000..70b70c6e6500b --- /dev/null +++ b/javascript/node/selenium-webdriver/bidi/external/permissions.js @@ -0,0 +1,73 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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. + +const PermissionState = Object.freeze({ + GRANTED: 'granted', + DENIED: 'denied', + PROMPT: 'prompt', +}) + +class Permission { + constructor(driver) { + this._driver = driver + } + + async init() { + if (!(await this._driver.getCapabilities()).get('webSocketUrl')) { + throw Error('WebDriver instance must support BiDi protocol') + } + + this.bidi = await this._driver.getBidi() + } + + /** + * Sets a permission state for a given permission descriptor. + * @param {Object} permissionDescriptor The permission descriptor. + * @param {string} state The permission state (granted, denied, prompt). + * @param {string} origin The origin for which the permission is set. + * @param {string} [userContext] The user context id (optional). + * @returns {Promise} + */ + async setPermission(permissionDescriptor, state, origin, userContext = null) { + if (!Object.values(PermissionState).includes(state)) { + throw new Error(`Invalid permission state. Must be one of: ${Object.values(PermissionState).join(', ')}`) + } + + const command = { + method: 'permissions.setPermission', + params: { + descriptor: permissionDescriptor, + state: state, + origin: origin, + }, + } + + if (userContext) { + command.params.userContext = userContext + } + + await this.bidi.send(command) + } +} + +async function getPermissionInstance(driver) { + let instance = new Permission(driver) + await instance.init() + return instance +} + +module.exports = { getPermissionInstance, PermissionState } diff --git a/javascript/node/selenium-webdriver/test/bidi/permissions_test.js b/javascript/node/selenium-webdriver/test/bidi/permissions_test.js new file mode 100644 index 0000000000000..e79b7f41346fc --- /dev/null +++ b/javascript/node/selenium-webdriver/test/bidi/permissions_test.js @@ -0,0 +1,147 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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. + +'use strict' + +const assert = require('node:assert') +const { Pages, suite } = require('../../lib/test') +const { Browser } = require('selenium-webdriver') +const BrowserBiDi = require('selenium-webdriver/bidi/browser') +const getScriptManager = require('selenium-webdriver/bidi/scriptManager') +const { getPermissionInstance, PermissionState } = require('selenium-webdriver/bidi/external/permissions') +const BrowsingContext = require('selenium-webdriver/bidi/browsingContext') +const { CreateContextParameters } = require('selenium-webdriver/bidi/createContextParameters') + +suite( + function (env) { + describe('BiDi Permissions', function () { + let driver, permission, browser, script + + const GET_GEOLOCATION_PERMISSION = + "async () => { const perm = await navigator.permissions.query({ name: 'geolocation' }); return perm.state; }" + const GET_ORIGIN = '() => {return window.location.origin;}' + + beforeEach(async function () { + driver = await env.builder().build() + permission = await getPermissionInstance(driver) + browser = await BrowserBiDi(driver) + script = await getScriptManager([], driver) + }) + + afterEach(function () { + return driver.quit() + }) + + it('can set permission to granted', async function () { + const context = await BrowsingContext(driver, { type: 'tab' }) + await context.navigate(Pages.blankPage, 'complete') + + const origin = await script.callFunctionInBrowsingContext(context.id, GET_ORIGIN, true, []) + const originValue = origin.result.value + + await permission.setPermission({ name: 'geolocation' }, PermissionState.GRANTED, originValue) + + const result = await script.callFunctionInBrowsingContext(context.id, GET_GEOLOCATION_PERMISSION, true, []) + assert.strictEqual(result.result.value, PermissionState.GRANTED) + }) + + it('can set permission to denied', async function () { + const context = await BrowsingContext(driver, { type: 'tab' }) + await context.navigate(Pages.blankPage, 'complete') + + const origin = await script.callFunctionInBrowsingContext(context.id, GET_ORIGIN, true, []) + + const originValue = origin.result.value + await permission.setPermission({ name: 'geolocation' }, PermissionState.DENIED, originValue) + + const result = await script.callFunctionInBrowsingContext(context.id, GET_GEOLOCATION_PERMISSION, true, []) + assert.strictEqual(result.result.value, PermissionState.DENIED) + }) + + it('can set permission to prompt', async function () { + const context = await BrowsingContext(driver, { type: 'tab' }) + await context.navigate(Pages.blankPage, 'complete') + + const origin = await script.callFunctionInBrowsingContext(context.id, GET_ORIGIN, true, []) + + const originValue = origin.result.value + await permission.setPermission({ name: 'geolocation' }, PermissionState.DENIED, originValue) + + await permission.setPermission({ name: 'geolocation' }, PermissionState.PROMPT, originValue) + + const result = await script.callFunctionInBrowsingContext(context.id, GET_GEOLOCATION_PERMISSION, true, []) + assert.strictEqual(result.result.value, PermissionState.PROMPT) + }) + + it('can set permission for a user context', async function () { + const userContext = await browser.createUserContext() + + const context1 = await BrowsingContext(driver, { type: 'tab' }) + const context2 = await BrowsingContext(driver, { + type: 'tab', + createParameters: new CreateContextParameters().userContext(userContext), + }) + + await context1.navigate(Pages.blankPage, 'complete') + await context2.navigate(Pages.blankPage, 'complete') + + const origin = await script.callFunctionInBrowsingContext(context1.id, GET_ORIGIN, true, []) + const originValue = origin.result.value + + // Get the actual permission states from each context + const originalTabPermission = await script.callFunctionInBrowsingContext( + context1.id, + GET_GEOLOCATION_PERMISSION, + true, + [], + ) + + const newTabPermission = await script.callFunctionInBrowsingContext( + context2.id, + GET_GEOLOCATION_PERMISSION, + true, + [], + ) + + const originalTabState = originalTabPermission.result.value + const newTabState = newTabPermission.result.value + + // Set permission only for the user context + await permission.setPermission({ name: 'geolocation' }, PermissionState.GRANTED, originValue, userContext) + + // Check that the original tab's permission state hasn't changed + const originalTabUpdatedPermission = await script.callFunctionInBrowsingContext( + context1.id, + GET_GEOLOCATION_PERMISSION, + true, + [], + ) + assert.strictEqual(originalTabUpdatedPermission.result.value, originalTabState) + + // Check that the new tab's permission state has been updated to GRANTED + const newTabUpdatedPermission = await script.callFunctionInBrowsingContext( + context2.id, + GET_GEOLOCATION_PERMISSION, + true, + [], + ) + assert.strictEqual(newTabUpdatedPermission.result.value, PermissionState.GRANTED) + }) + }) + }, + { browsers: [Browser.FIREFOX, Browser.CHROME, Browser.EDGE] }, +)