diff --git a/common/src/web/fedcm/fedcm_async_js.html b/common/src/web/fedcm/fedcm_async_js.html new file mode 100644 index 0000000000000..d1411be0a4cec --- /dev/null +++ b/common/src/web/fedcm/fedcm_async_js.html @@ -0,0 +1,40 @@ + + + + FedCM Example + + + +
+ + + + \ No newline at end of file diff --git a/javascript/node/selenium-webdriver/BUILD.bazel b/javascript/node/selenium-webdriver/BUILD.bazel index bc6e68042597d..75c855c641ec8 100644 --- a/javascript/node/selenium-webdriver/BUILD.bazel +++ b/javascript/node/selenium-webdriver/BUILD.bazel @@ -33,6 +33,7 @@ js_library( "io/*.js", "lib/*.js", "lib/atoms/bidi-mutation-listener.js", + "lib/fedcm/*.js", "net/*.js", "remote/*.js", "testing/*.js", diff --git a/javascript/node/selenium-webdriver/lib/command.js b/javascript/node/selenium-webdriver/lib/command.js index 5f7f481f77bf6..5d74b955bd4e6 100644 --- a/javascript/node/selenium-webdriver/lib/command.js +++ b/javascript/node/selenium-webdriver/lib/command.js @@ -185,6 +185,17 @@ const Name = { GET_DOWNLOADABLE_FILES: 'getDownloadableFiles', DOWNLOAD_FILE: 'downloadFile', DELETE_DOWNLOADABLE_FILES: 'deleteDownloadableFiles', + + // Federated Credential Management API + // https://www.w3.org/TR/fedcm/#automation + CANCEL_DIALOG: 'cancelDialog', + SELECT_ACCOUNT: 'selectAccount', + GET_ACCOUNTS: 'getAccounts', + GET_FEDCM_TITLE: 'getFedCmTitle', + GET_FEDCM_DIALOG_TYPE: 'getFedCmDialogType', + SET_DELAY_ENABLED: 'setDelayEnabled', + RESET_COOLDOWN: 'resetCooldown', + CLICK_DIALOG_BUTTON: 'clickdialogbutton', } /** diff --git a/javascript/node/selenium-webdriver/lib/fedcm/account.js b/javascript/node/selenium-webdriver/lib/fedcm/account.js new file mode 100644 index 0000000000000..451f192a69f54 --- /dev/null +++ b/javascript/node/selenium-webdriver/lib/fedcm/account.js @@ -0,0 +1,78 @@ +// 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. + +class Account { + constructor( + accountId, + email, + name, + givenName, + pictureUrl, + idpConfigUrl, + loginState, + termsOfServiceUrl, + privacyPolicyUrl, + ) { + this._accountId = accountId + this._email = email + this._name = name + this._givenName = givenName + this._pictureUrl = pictureUrl + this._idpConfigUrl = idpConfigUrl + this._loginState = loginState + this._termsOfServiceUrl = termsOfServiceUrl + this._privacyPolicyUrl = privacyPolicyUrl + } + + get accountId() { + return this._accountId + } + + get email() { + return this._email + } + + get name() { + return this._name + } + + get givenName() { + return this._givenName + } + + get pictureUrl() { + return this._pictureUrl + } + + get idpConfigUrl() { + return this._idpConfigUrl + } + + get loginState() { + return this._loginState + } + + get termsOfServiceUrl() { + return this._termsOfServiceUrl + } + + get privacyPolicyUrl() { + return this._privacyPolicyUrl + } +} + +module.exports = Account diff --git a/javascript/node/selenium-webdriver/lib/fedcm/dialog.js b/javascript/node/selenium-webdriver/lib/fedcm/dialog.js new file mode 100644 index 0000000000000..cec84a78e5d6d --- /dev/null +++ b/javascript/node/selenium-webdriver/lib/fedcm/dialog.js @@ -0,0 +1,76 @@ +// 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 command = require('../command') +const Account = require('./account') + +class Dialog { + constructor(driver) { + this._driver = driver + } + + async title() { + const result = await this._driver.execute(new command.Command(command.Name.GET_FEDCM_TITLE)) + + return result.title + } + + subtitle() { + return this._driver.execute(new command.Command(command.Name.GET_FEDCM_TITLE)) + } + + type() { + return this._driver.execute(new command.Command(command.Name.GET_FEDCM_DIALOG_TYPE)) + } + + async accounts() { + const result = await this._driver.execute(new command.Command(command.Name.GET_ACCOUNTS)) + + const accountArray = [] + + result.forEach((account) => { + const acc = new Account( + account.accountId, + account.email, + account.name, + account.givenName, + account.pictureUrl, + account.idpConfigUrl, + account.loginState, + account.termsOfServiceUrl, + account.privacyPolicyUrl, + ) + accountArray.push(acc) + }) + + return accountArray + } + + selectAccount(index) { + return this._driver.execute(new command.Command(command.Name.SELECT_ACCOUNT).setParameter('accountIndex', index)) + } + + accept() { + return this._driver.execute(new command.Command(command.Name.CLICK_DIALOG_BUTTON)) + } + + dismiss() { + return this._driver.execute(new command.Command(command.Name.CANCEL_DIALOG)) + } +} + +module.exports = Dialog diff --git a/javascript/node/selenium-webdriver/lib/http.js b/javascript/node/selenium-webdriver/lib/http.js index eea7a4fe58d9f..2dd38c43b7169 100644 --- a/javascript/node/selenium-webdriver/lib/http.js +++ b/javascript/node/selenium-webdriver/lib/http.js @@ -320,6 +320,16 @@ const W3C_COMMAND_MAP = new Map([ [cmd.Name.GET_DOWNLOADABLE_FILES, get('/session/:sessionId/se/files')], [cmd.Name.DOWNLOAD_FILE, post(`/session/:sessionId/se/files`)], [cmd.Name.DELETE_DOWNLOADABLE_FILES, del(`/session/:sessionId/se/files`)], + + // Federated Credential Management Command + [cmd.Name.CANCEL_DIALOG, post(`/session/:sessionId/fedcm/canceldialog`)], + [cmd.Name.SELECT_ACCOUNT, post(`/session/:sessionId/fedcm/selectaccount`)], + [cmd.Name.GET_FEDCM_TITLE, get(`/session/:sessionId/fedcm/gettitle`)], + [cmd.Name.GET_FEDCM_DIALOG_TYPE, get('/session/:sessionId/fedcm/getdialogtype')], + [cmd.Name.SET_DELAY_ENABLED, post(`/session/:sessionId/fedcm/setdelayenabled`)], + [cmd.Name.RESET_COOLDOWN, post(`/session/:sessionId/fedcm/resetcooldown`)], + [cmd.Name.CLICK_DIALOG_BUTTON, post(`/session/:sessionId/fedcm/clickdialogbutton`)], + [cmd.Name.GET_ACCOUNTS, get(`/session/:sessionId/fedcm/accountlist`)], ]) /** diff --git a/javascript/node/selenium-webdriver/lib/test/fileserver.js b/javascript/node/selenium-webdriver/lib/test/fileserver.js index 337c1b9205137..d215971f754a2 100644 --- a/javascript/node/selenium-webdriver/lib/test/fileserver.js +++ b/javascript/node/selenium-webdriver/lib/test/fileserver.js @@ -118,7 +118,7 @@ const Pages = (function () { addPage('emptyText', 'bidi/emptyText.txt') addPage('redirectedHttpEquiv', 'bidi/redirected_http_equiv.html') addPage('releaseAction', 'bidi/release_action.html') - + addPage('fedcm', 'fedcm/fedcm_async_js.html') return pages })() diff --git a/javascript/node/selenium-webdriver/lib/webdriver.js b/javascript/node/selenium-webdriver/lib/webdriver.js index 2c6a9aa48e259..d910f54d09bf5 100644 --- a/javascript/node/selenium-webdriver/lib/webdriver.js +++ b/javascript/node/selenium-webdriver/lib/webdriver.js @@ -45,6 +45,7 @@ const { PinnedScript } = require('./pinnedScript') const JSZip = require('jszip') const Script = require('./script') const Network = require('./network') +const Dialog = require('./fedcm/dialog') // Capability names that are defined in the W3C spec. const W3C_CAPABILITY_NAMES = new Set([ @@ -1106,6 +1107,18 @@ class WebDriver { return this.execute(new command.Command(command.Name.SCREENSHOT)) } + setDelayEnabled(enabled) { + return this.execute(new command.Command(command.Name.SET_DELAY_ENABLED).setParameter('enabled', enabled)) + } + + resetCooldown() { + return this.execute(new command.Command(command.Name.RESET_COOLDOWN)) + } + + getFederalCredentialManagementDialog() { + return new Dialog(this) + } + /** @override */ manage() { return new Options(this) diff --git a/javascript/node/selenium-webdriver/test/fedcm/fedcm_test.js b/javascript/node/selenium-webdriver/test/fedcm/fedcm_test.js new file mode 100644 index 0000000000000..4ae3ac70b5706 --- /dev/null +++ b/javascript/node/selenium-webdriver/test/fedcm/fedcm_test.js @@ -0,0 +1,183 @@ +// 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 { Browser } = require('selenium-webdriver') +const { Pages, suite } = require('../../lib/test') +const { By } = require('selenium-webdriver/index') + +suite( + function (env) { + let driver + + beforeEach(async function () { + driver = await env.builder().build() + }) + + afterEach(async function () { + await driver.quit() + }) + + describe('Federated Credential Management Test', function () { + it('credential mangement dialog should appear', async function () { + await driver.get(Pages.fedcm) + + let triggerButton = await driver.findElement(By.id('triggerButton')) + await triggerButton.click() + + let dialog + + await driver.wait( + async () => { + try { + dialog = await driver.getFederalCredentialManagementDialog() + return (await dialog.type()) === 'AccountChooser' + } catch (error) { + return false + } + }, + 10000, + 'Expected dialog type to be "AccountChooser"', + 2000, + ) + + assert.equal(await dialog.type(), 'AccountChooser') + let title = await dialog.title() + assert.equal(title.includes('Sign in to'), true) + }) + + it('can dismiss dialog', async function () { + await driver.get(Pages.fedcm) + + let triggerButton = await driver.findElement(By.id('triggerButton')) + await triggerButton.click() + + let dialog = await driver.getFederalCredentialManagementDialog() + + await driver.wait( + async () => { + try { + return (await dialog.type()) === 'AccountChooser' + } catch (error) { + return false + } + }, + 10000, + 'Expected dialog type to be "AccountChooser"', + 2000, + ) + + assert.equal(await dialog.type(), 'AccountChooser') + let title = await dialog.title() + assert.equal(title.includes('Sign in to'), true) + + await dialog.dismiss() + + try { + await dialog.type() + assert.fail('Above command should throw error') + } catch (error) { + assert.equal(error.message.includes('no such alert'), true) + } + }) + + it('can select account', async function () { + await driver.get(Pages.fedcm) + + let triggerButton = await driver.findElement(By.id('triggerButton')) + await triggerButton.click() + + let dialog = await driver.getFederalCredentialManagementDialog() + + await driver.wait( + async () => { + try { + return (await dialog.type()) === 'AccountChooser' + } catch (error) { + return false + } + }, + 10000, + 'Expected dialog type to be "AccountChooser"', + 2000, + ) + + assert.equal(await dialog.type(), 'AccountChooser') + let title = await dialog.title() + assert.equal(title.includes('Sign in to'), true) + + await dialog.selectAccount(1) + }) + + it('can get account list', async function () { + await driver.get(Pages.fedcm) + + let triggerButton = await driver.findElement(By.id('triggerButton')) + await triggerButton.click() + + let dialog = await driver.getFederalCredentialManagementDialog() + + await driver.wait( + async () => { + try { + return (await dialog.type()) === 'AccountChooser' + } catch (error) { + return false + } + }, + 10000, + 'Expected dialog type to be "AccountChooser"', + 2000, + ) + + assert.equal(await dialog.type(), 'AccountChooser') + let title = await dialog.title() + assert.equal(title.includes('Sign in to'), true) + + const accounts = await dialog.accounts() + + assert.equal(accounts.length, 2) + + const account1 = accounts[0] + const account2 = accounts[1] + + assert.strictEqual(account1.name, 'John Doe') + assert.strictEqual(account1.email, 'john_doe@idp.example') + assert.strictEqual(account1.accountId, '1234') + assert.strictEqual(account1.givenName, 'John') + assert(account1.idpConfigUrl.includes('/fedcm/config.json'), true) + assert.strictEqual(account1.pictureUrl, 'https://idp.example/profile/123') + assert.strictEqual(account1.loginState, 'SignUp') + assert.strictEqual(account1.termsOfServiceUrl, 'https://rp.example/terms_of_service.html') + assert.strictEqual(account1.privacyPolicyUrl, 'https://rp.example/privacy_policy.html') + + assert.strictEqual(account2.name, 'Aisha Ahmad') + assert.strictEqual(account2.email, 'aisha@idp.example') + assert.strictEqual(account2.accountId, '5678') + assert.strictEqual(account2.givenName, 'Aisha') + assert(account2.idpConfigUrl.includes('/fedcm/config.json'), true) + assert.strictEqual(account2.pictureUrl, 'https://idp.example/profile/567') + assert.strictEqual(account2.loginState, 'SignUp') + assert.strictEqual(account2.termsOfServiceUrl, 'https://rp.example/terms_of_service.html') + assert.strictEqual(account2.privacyPolicyUrl, 'https://rp.example/privacy_policy.html') + }) + }) + }, + { browsers: [Browser.CHROME, Browser.EDGE] }, +)