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] },
+)