summaryrefslogtreecommitdiffstats
path: root/toolkit/components/aboutwebauthn
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/aboutwebauthn
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/aboutwebauthn')
-rw-r--r--toolkit/components/aboutwebauthn/content/aboutWebauthn.css99
-rw-r--r--toolkit/components/aboutwebauthn/content/aboutWebauthn.html322
-rw-r--r--toolkit/components/aboutwebauthn/content/aboutWebauthn.js866
-rw-r--r--toolkit/components/aboutwebauthn/jar.mn8
-rw-r--r--toolkit/components/aboutwebauthn/moz.build13
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/browser.toml21
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_aria_keycontrols.js195
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_bio.js351
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js395
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js218
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js57
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_pin.js218
-rw-r--r--toolkit/components/aboutwebauthn/tests/browser/head.js40
13 files changed, 2803 insertions, 0 deletions
diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.css b/toolkit/components/aboutwebauthn/content/aboutWebauthn.css
new file mode 100644
index 0000000000..5654e4276b
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.css
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@import url("chrome://global/skin/in-content/common.css");
+
+html {
+ height: 100%;
+}
+
+body {
+ display: flex;
+ align-items: stretch;
+ height: 100%;
+}
+
+label {
+ display: block;
+}
+
+#info-text-div {
+ padding: 20px;
+}
+
+#ctap-listen-div {
+ padding-top: 15px;
+}
+#ctap-listen-result {
+ font-weight: 600;
+ font-size: 1.5em;
+ padding-inline-start: 15px;
+ height: 1.5em;
+
+ &.success {
+ background-color: var(--green-50);
+ }
+
+ &.error {
+ background-color: var(--red-50);
+ }
+}
+
+.category {
+ cursor: pointer;
+ /* Center category names */
+ display: flex;
+ align-items: center;
+}
+
+.optional-category {
+ display: none;
+}
+
+.disabled-category {
+ pointer-events: none;
+}
+
+@media (max-width: 830px){
+ #categories > .category {
+ padding-inline-start: 5px;
+ margin-inline-start: 0;
+ }
+}
+
+#main-content {
+ flex: 1;
+}
+
+.token-info-flex-box {
+ display: flex;
+}
+
+.token-info-flex-child {
+ flex: 1;
+}
+
+.token-info-flex-child#authenticator-options {
+ margin-inline-end: 2px;
+}
+
+.bio-enrollment-sample {
+ display: flex;
+ gap: 0.5em;
+}
+
+.button-row {
+ display: inline-block;
+ margin-inline-end: 5px;
+}
+
+.delete-icon {
+ margin-inline-end: 5px;
+ -moz-context-properties: fill;
+ fill: currentColor;
+}
+
+.delete-button {
+ display: inline-flex;
+}
diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.html b/toolkit/components/aboutwebauthn/content/aboutWebauthn.html
new file mode 100644
index 0000000000..5c8485b61f
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.html
@@ -0,0 +1,322 @@
+<!DOCTYPE html>
+
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+<html>
+ <head>
+ <meta
+ http-equiv="Content-Security-Policy"
+ content="default-src chrome:; style-src chrome:; object-src 'none'"
+ />
+ <meta charset="utf-8" />
+ <title id="page-title" data-l10n-id="about-webauthn-page-title"></title>
+ <link rel="stylesheet" href="chrome://global/content/aboutWebauthn.css" />
+ <script src="chrome://global/content/aboutWebauthn.js"></script>
+ <link rel="localization" href="toolkit/about/aboutWebauthn.ftl" />
+ </head>
+
+ <body id="body">
+ <div id="categories" role="tablist" aria-labelledby="page-title">
+ <div
+ id="info-tab-button"
+ class="category"
+ selected="true"
+ role="tab"
+ tabindex="0"
+ aria-selected="true"
+ aria-controls="token-info-section"
+ >
+ <span
+ class="tablinks"
+ data-l10n-id="about-webauthn-info-section-title"
+ ></span>
+ </div>
+ <div
+ id="pin-tab-button"
+ class="category optional-category"
+ role="tab"
+ tabindex="-1"
+ aria-selected="false"
+ aria-controls="set-change-pin-section"
+ >
+ <span
+ class="tablinks"
+ data-l10n-id="about-webauthn-pin-section-title"
+ ></span>
+ </div>
+ <div
+ id="credentials-tab-button"
+ class="category optional-category"
+ role="tab"
+ tabindex="-1"
+ aria-selected="false"
+ aria-controls="credential-management-section"
+ >
+ <span
+ class="tablinks"
+ data-l10n-id="about-webauthn-credential-management-section-title"
+ ></span>
+ </div>
+ <div
+ id="bio-enrollments-tab-button"
+ class="category optional-category"
+ role="tab"
+ tabindex="-1"
+ aria-selected="false"
+ aria-controls="bio-enrollment-section"
+ >
+ <span
+ class="tablinks"
+ data-l10n-id="about-webauthn-bio-enrollment-section-title"
+ ></span>
+ </div>
+ </div>
+
+ <div id="main-content">
+ <div id="ctap-listen-div">
+ <label id="ctap-listen-result"></label>
+ </div>
+
+ <div
+ class="tabcontent token-info-section"
+ id="token-info-section"
+ role="tabpanel"
+ aria-labelledby="info-tab-button"
+ >
+ <h2
+ class="categoryTitle"
+ data-l10n-id="about-webauthn-info-section-title"
+ ></h2>
+
+ <div id="info-text-div">
+ <label
+ id="info-text-field"
+ data-l10n-id="about-webauthn-text-connect-device"
+ ></label>
+ </div>
+
+ <div id="ctap2-token-info" class="token-info-flex-box" display="none">
+ <div id="ctap2-token-info-options" class="token-info-flex-child">
+ <h3 data-l10n-id="about-webauthn-options-subsection-title"></h3>
+ <table id="authenticator-options"></table>
+ </div>
+ <div id="ctap2-token-info-info" class="token-info-flex-child">
+ <h3 data-l10n-id="about-webauthn-info-subsection-title"></h3>
+ <table id="authenticator-info"></table>
+ </div>
+ </div>
+ </div>
+
+ <div
+ hidden
+ class="tabcontent pin-section"
+ id="set-change-pin-section"
+ role="tabpanel"
+ aria-labelledby="pin-tab-button"
+ >
+ <h2
+ class="categoryTitle"
+ data-l10n-id="about-webauthn-pin-section-title"
+ ></h2>
+ <div id="new-pin-div">
+ <label
+ for="new-pin"
+ data-l10n-id="about-webauthn-new-pin-label"
+ ></label>
+ <input type="password" id="new-pin" name="new-pin" required />
+ <label
+ for="new-pin-repeat"
+ data-l10n-id="about-webauthn-repeat-pin-label"
+ ></label>
+ <input
+ type="password"
+ id="new-pin-repeat"
+ name="new-pin-repeat"
+ required
+ />
+ </div>
+ <div id="current-pin-div">
+ <label
+ for="current-pin"
+ data-l10n-id="about-webauthn-current-pin-label"
+ ></label>
+ <input type="password" id="current-pin" name="current-pin" required />
+ </div>
+ <button
+ disabled
+ id="set-pin-button"
+ data-l10n-id="about-webauthn-current-set-pin-button"
+ ></button>
+ <button
+ disabled
+ id="change-pin-button"
+ data-l10n-id="about-webauthn-current-change-pin-button"
+ ></button>
+ <label id="set-change-pin-result" class="ctap-result"></label>
+ </div>
+
+ <div
+ hidden
+ class="tabcontent credential-management-section"
+ id="credential-management-section"
+ role="tabpanel"
+ aria-labelledby="credentials-tab-button"
+ >
+ <h2
+ class="categoryTitle"
+ data-l10n-id="about-webauthn-credential-management-section-title"
+ ></h2>
+ <div
+ hidden
+ id="credential-list-subsection"
+ class="token-info-flex-child"
+ >
+ <h3
+ data-l10n-id="about-webauthn-credential-list-subsection-title"
+ ></h3>
+ <div hidden id="credential-list-empty-label">
+ <label
+ hidden
+ data-l10n-id="about-webauthn-credential-list-empty"
+ ></label>
+ </div>
+ <table id="credential-list"></table>
+ </div>
+ <button
+ class="credentials-button"
+ id="list-credentials-button"
+ data-l10n-id="about-webauthn-list-credentials-button"
+ ></button>
+ </div>
+
+ <div
+ hidden
+ class="tabcontent bio-enrollment-section"
+ id="bio-enrollment-section"
+ role="tabpanel"
+ aria-labelledby="bio-enrollments-tab-button"
+ >
+ <h2
+ class="categoryTitle"
+ data-l10n-id="about-webauthn-bio-enrollment-section-title"
+ ></h2>
+ <div
+ hidden
+ id="bio-enrollment-list-subsection"
+ class="token-info-flex-child"
+ >
+ <h3
+ data-l10n-id="about-webauthn-bio-enrollment-list-subsection-title"
+ ></h3>
+ <div hidden id="bio-enrollment-list-empty-label">
+ <label
+ hidden
+ data-l10n-id="about-webauthn-enrollment-list-empty"
+ ></label>
+ </div>
+ <table id="bio-enrollment-list"></table>
+ </div>
+ <button
+ class="bio-enrollment-button button-row"
+ id="list-bio-enrollments-button"
+ data-l10n-id="about-webauthn-list-bio-enrollments-button"
+ ></button>
+ <button
+ class="bio-enrollment-button button-row"
+ id="add-bio-enrollment-button"
+ data-l10n-id="about-webauthn-add-bio-enrollment-button"
+ ></button>
+ </div>
+ <div
+ hidden
+ class="tabcontent add-bio-enrollment-section"
+ id="add-bio-enrollment-section"
+ >
+ <h2
+ class="categoryTitle"
+ data-l10n-id="about-webauthn-add-bio-enrollment-section-title"
+ ></h2>
+ <label
+ for="enrollment-name"
+ data-l10n-id="about-webauthn-enrollment-name-label"
+ ></label>
+ <input id="enrollment-name" name="enrollment-name" autofocus />
+ <button
+ class="bio-enrollment-button button-row"
+ id="start-enrollment-button"
+ data-l10n-id="about-webauthn-start-enrollment-button"
+ ></button>
+ <button
+ id="cancel-enrollment-button"
+ class="button-row"
+ data-l10n-id="about-webauthn-cancel-button"
+ ></button>
+ <div id="enrollment-update"></div>
+ </div>
+
+ <div
+ hidden
+ class="tabcontent pin-required-section"
+ id="pin-required-section"
+ >
+ <h2
+ class="categoryTitle"
+ data-l10n-id="about-webauthn-pin-required-section-title"
+ ></h2>
+ <div id="pin-div">
+ <label
+ for="pin-required"
+ data-l10n-id="about-webauthn-pin-required-label"
+ ></label>
+ <input
+ type="password"
+ id="pin-required"
+ name="pin-required"
+ required
+ autofocus
+ />
+ </div>
+ <button
+ id="send-pin-button"
+ class="button-row"
+ data-l10n-id="about-webauthn-send-pin-button"
+ ></button>
+ <button
+ id="cancel-send-pin-button"
+ class="button-row"
+ data-l10n-id="about-webauthn-cancel-button"
+ ></button>
+ </div>
+
+ <div
+ hidden
+ class="tabcontent confirm-deletion-section"
+ id="confirm-deletion-section"
+ >
+ <h2
+ class="categoryTitle"
+ data-l10n-id="about-webauthn-confirm-deletion-section-title"
+ ></h2>
+ <div id="confirmation-div">
+ <label
+ for="confirmation-context"
+ data-l10n-id="about-webauthn-confirm-deletion-label"
+ ></label>
+ <label id="confirmation-context"></label>
+ </div>
+ <button
+ id="confirm-deletion-button"
+ class="button-row"
+ data-l10n-id="about-webauthn-delete-button"
+ ></button>
+ <button
+ id="cancel-confirmation-button"
+ class="button-row"
+ data-l10n-id="about-webauthn-cancel-button"
+ ></button>
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/toolkit/components/aboutwebauthn/content/aboutWebauthn.js b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js
new file mode 100644
index 0000000000..9142ed198a
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/content/aboutWebauthn.js
@@ -0,0 +1,866 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let AboutWebauthnService = null;
+
+var AboutWebauthnManagerJS = {
+ _topic: "about-webauthn-prompt",
+ _initialized: false,
+ _l10n: null,
+ _bio_l10n: null,
+ _curr_data: null,
+ _current_tab: "",
+ _previous_tab: "",
+
+ init() {
+ if (this._initialized) {
+ return;
+ }
+ this._l10n = new Localization(["toolkit/about/aboutWebauthn.ftl"], true);
+ this._bio_l10n = new Map();
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpGood",
+ "about-webauthn-ctap2-enroll-feedback-good"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooHigh",
+ "about-webauthn-ctap2-enroll-feedback-too-high"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooLow",
+ "about-webauthn-ctap2-enroll-feedback-too-low"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooLeft",
+ "about-webauthn-ctap2-enroll-feedback-too-left"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooRight",
+ "about-webauthn-ctap2-enroll-feedback-too-right"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooFast",
+ "about-webauthn-ctap2-enroll-feedback-too-fast"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooSlow",
+ "about-webauthn-ctap2-enroll-feedback-too-slow"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpPoorQuality",
+ "about-webauthn-ctap2-enroll-feedback-poor-quality"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooSkewed",
+ "about-webauthn-ctap2-enroll-feedback-too-skewed"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpTooShort",
+ "about-webauthn-ctap2-enroll-feedback-too-short"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpMergeFailure",
+ "about-webauthn-ctap2-enroll-feedback-merge-failure"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackFpExists",
+ "about-webauthn-ctap2-enroll-feedback-exists"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackNoUserActivity",
+ "about-webauthn-ctap2-enroll-feedback-no-user-activity"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackNoUserPresenceTransition",
+ "about-webauthn-ctap2-enroll-feedback-no-user-presence-transition"
+ );
+ this._bio_l10n.set(
+ "Ctap2EnrollFeedbackOther",
+ "about-webauthn-ctap2-enroll-feedback-other"
+ );
+
+ Services.obs.addObserver(this, this._topic);
+ this._initialized = true;
+ reset_page();
+ },
+
+ uninit() {
+ Services.obs.removeObserver(this, this._topic);
+ this._initialized = false;
+ this._l10n = null;
+ this._current_tab = "";
+ this._previous_tab = "";
+ },
+
+ observe(aSubject, aTopic, aData) {
+ let data = JSON.parse(aData);
+
+ // We have token
+ if (data.type == "selected-device") {
+ this._curr_data = data.auth_info;
+ this.show_ui_based_on_authenticator_info(data);
+ fake_click_event_for_id("info-tab-button");
+ } else if (data.type == "select-device") {
+ set_info_text("about-webauthn-text-select-device");
+ } else if (data.type == "pin-required") {
+ open_pin_required_tab();
+ } else if (data.type == "pin-invalid") {
+ let retries = data.retries ? data.retries : 0;
+ show_results_banner(
+ "error",
+ "about-webauthn-results-pin-invalid-error",
+ JSON.stringify({ retriesLeft: retries })
+ );
+ open_pin_required_tab();
+ } else if (data.type == "bio-enrollment-update") {
+ if (data.result.EnrollmentList) {
+ show_results_banner("success", "about-webauthn-results-success");
+ this.show_enrollment_list(data.result.EnrollmentList);
+ bio_enrollment_in_progress(false);
+ } else if (data.result.DeleteSuccess || data.result.AddSuccess) {
+ show_results_banner("success", "about-webauthn-results-success");
+ clear_bio_enrollment_samples();
+ // Update AuthenticatorInfo
+ this._curr_data = data.result.DeleteSuccess ?? data.result.AddSuccess;
+ fake_click_event_for_id("bio-enrollments-tab-button");
+ bio_enrollment_in_progress(false);
+ // If we still have some enrollments to show, update the list
+ // otherwise, remove it.
+ if (
+ this._curr_data.options.bioEnroll === true ||
+ this._curr_data.options.userVerificationMgmtPreview === true
+ ) {
+ list_bio_enrollments();
+ } else {
+ // Hide the list, because it's empty
+ document.getElementById(
+ "bio-enrollment-list-subsection"
+ ).hidden = true;
+ }
+ } else if (data.result.UpdateSuccess) {
+ fake_click_event_for_id("bio-enrollments-tab-button");
+ list_bio_enrollments();
+ } else if (data.result.SampleStatus) {
+ show_add_bio_enrollment_section();
+ let up = document.getElementById("enrollment-update");
+ let sample_update = document.createElement("div");
+ let new_line = document.createElement("label");
+ new_line.classList.add("sample");
+ new_line.setAttribute(
+ "data-l10n-id",
+ this._bio_l10n.get(data.result.SampleStatus[0])
+ );
+ sample_update.appendChild(new_line);
+ let samples_needed_line = document.createElement("label");
+ samples_needed_line.setAttribute(
+ "data-l10n-id",
+ "about-webauthn-samples-still-needed"
+ );
+ samples_needed_line.setAttribute(
+ "data-l10n-args",
+ JSON.stringify({ repeatCount: data.result.SampleStatus[1] })
+ );
+ sample_update.classList.add("bio-enrollment-sample");
+ sample_update.appendChild(samples_needed_line);
+ up.appendChild(sample_update);
+ }
+ } else if (data.type == "credential-management-update") {
+ credential_management_in_progress(false);
+ if (data.result.CredentialList) {
+ show_results_banner("success", "about-webauthn-results-success");
+ this.show_credential_list(data.result.CredentialList.credential_list);
+ } else {
+ // DeleteSuccess or UpdateSuccess
+ show_results_banner("success", "about-webauthn-results-success");
+ list_credentials();
+ }
+ } else if (data.type == "listen-success") {
+ reset_page();
+ // Show results
+ show_results_banner("success", "about-webauthn-results-success");
+ this._reset_in_progress = "";
+ AboutWebauthnService.listen();
+ } else if (data.type == "listen-error") {
+ reset_page();
+
+ if (!data.error) {
+ show_results_banner("error", "about-webauthn-results-general-error");
+ } else if (data.error.type == "pin-auth-blocked") {
+ show_results_banner(
+ "error",
+ "about-webauthn-results-pin-auth-blocked-error"
+ );
+ } else if (data.error.type == "pin-not-set") {
+ show_results_banner(
+ "error",
+ "about-webauthn-results-pin-not-set-error"
+ );
+ } else if (data.error.type == "device-blocked") {
+ show_results_banner(
+ "error",
+ "about-webauthn-results-pin-blocked-error"
+ );
+ } else if (data.error.type == "pin-is-too-short") {
+ show_results_banner(
+ "error",
+ "about-webauthn-results-pin-too-short-error"
+ );
+ } else if (data.error.type == "pin-is-too-long") {
+ show_results_banner(
+ "error",
+ "about-webauthn-results-pin-too-long-error"
+ );
+ } else if (data.error.type == "pin-invalid") {
+ let retries = data.error.retries
+ ? JSON.stringify({ retriesLeft: data.error.retries })
+ : null;
+ show_results_banner(
+ "error",
+ "about-webauthn-results-pin-invalid-error",
+ retries
+ );
+ } else if (data.error.type == "cancel") {
+ show_results_banner(
+ "error",
+ "about-webauthn-results-cancelled-by-user-error"
+ );
+ } else {
+ show_results_banner("error", "about-webauthn-results-general-error");
+ }
+ AboutWebauthnService.listen();
+ }
+ },
+
+ show_authenticator_options(options, element, l10n_base) {
+ let table = document.getElementById(element);
+ var empty_table = document.createElement("table");
+ empty_table.id = element;
+ table.parentNode.replaceChild(empty_table, table);
+ table = document.getElementById(element);
+ for (let key in options) {
+ if (key == "options") {
+ continue;
+ }
+ // Create an empty <tr> element and add it to the 1st position of the table:
+ var row = table.insertRow(0);
+
+ // Insert new cells (<td> elements) at the 1st and 2nd position of the "new" <tr> element:
+ var cell1 = row.insertCell(0);
+ var cell2 = row.insertCell(1);
+
+ // Add some text to the new cells:
+ let key_text = this._l10n.formatValueSync(
+ l10n_base + "-" + key.toLowerCase().replace(/_/g, "-")
+ );
+ var key_node = document.createTextNode(key_text);
+ cell1.appendChild(key_node);
+ var raw_value = JSON.stringify(options[key]);
+ var value = raw_value;
+ if (["true", "false", "null"].includes(raw_value)) {
+ value = this._l10n.formatValueSync(l10n_base + "-" + raw_value);
+ }
+ var value_node = document.createTextNode(value);
+ cell2.appendChild(value_node);
+ }
+ },
+
+ show_ui_based_on_authenticator_info(data) {
+ // Hide the "Please plug in a token"-message
+ document.getElementById("info-text-div").hidden = true;
+ // Show options, based on what the token supports
+ if (data.auth_info) {
+ document.getElementById("ctap2-token-info").style.display = "flex";
+ this.show_authenticator_options(
+ data.auth_info.options,
+ "authenticator-options",
+ "about-webauthn-auth-option"
+ );
+ this.show_authenticator_options(
+ data.auth_info,
+ "authenticator-info",
+ "about-webauthn-auth-info"
+ );
+ // Check if token supports PINs
+ if (data.auth_info.options.clientPin != null) {
+ document.getElementById("pin-tab-button").style.display = "flex";
+ if (data.auth_info.options.clientPin === true) {
+ // It has a Pin set
+ document.getElementById("change-pin-button").style.display = "block";
+ document.getElementById("set-pin-button").style.display = "none";
+ document.getElementById("current-pin-div").hidden = false;
+ } else {
+ // It does not have a Pin set yet
+ document.getElementById("change-pin-button").style.display = "none";
+ document.getElementById("set-pin-button").style.display = "block";
+ document.getElementById("current-pin-div").hidden = true;
+ }
+ } else {
+ document.getElementById("pin-tab-button").style.display = "none";
+ }
+
+ if (
+ data.auth_info.options.credMgmt ||
+ data.auth_info.options.credentialMgmtPreview
+ ) {
+ document.getElementById("credentials-tab-button").style.display =
+ "flex";
+ } else {
+ document.getElementById("credentials-tab-button").style.display =
+ "none";
+ }
+
+ if (
+ data.auth_info.options.bioEnroll != null ||
+ data.auth_info.options.userVerificationMgmtPreview != null
+ ) {
+ document.getElementById("bio-enrollments-tab-button").style.display =
+ "flex";
+ } else {
+ document.getElementById("bio-enrollments-tab-button").style.display =
+ "none";
+ }
+ } else {
+ // Currently auth-rs doesn't send this, because it filters out ctap2-devices.
+ // U2F / CTAP1 tokens can't be managed
+ set_info_text("about-webauthn-text-non-ctap2-device");
+ }
+ },
+
+ show_credential_list(credential_list) {
+ // We may have temporarily hidden the tab when asking the user for a PIN
+ // so we have to show it again.
+ fake_click_event_for_id("credentials-tab-button");
+ document.getElementById("credential-list-subsection").hidden = false;
+ let table = document.getElementById("credential-list");
+ var empty_table = document.createElement("table");
+ empty_table.id = "credential-list";
+ table.parentNode.replaceChild(empty_table, table);
+ if (!credential_list.length) {
+ document.getElementById("credential-list-empty-label").hidden = false;
+ return;
+ }
+ document.getElementById("credential-list-empty-label").hidden = true;
+ table = document.getElementById("credential-list");
+ credential_list.forEach(rp => {
+ // Add some text to the new cells:
+ let key_text = rp.rp.id;
+ rp.credentials.forEach(cred => {
+ let value_text = cred.user.name;
+ // Create an empty <tr> element and add it to the 1st position of the table:
+ var row = table.insertRow(0);
+ var key_node = document.createTextNode(key_text);
+ var value_node = document.createTextNode(value_text);
+ row.insertCell(0).appendChild(key_node);
+ row.insertCell(1).appendChild(value_node);
+ var delete_button = document.createElement("button");
+ delete_button.classList.add("delete-button");
+ delete_button.classList.add("credentials-button");
+ let garbage_icon = document.createElement("img");
+ garbage_icon.setAttribute(
+ "src",
+ "chrome://global/skin/icons/delete.svg"
+ );
+ garbage_icon.classList.add("delete-icon");
+ delete_button.appendChild(garbage_icon);
+ let delete_text = document.createElement("span");
+ delete_text.setAttribute(
+ "data-l10n-id",
+ "about-webauthn-delete-button"
+ );
+ delete_button.appendChild(delete_text);
+ delete_button.addEventListener("click", function () {
+ let context = document.getElementById("confirmation-context");
+ context.textContent = key_text + " - " + value_text;
+ credential_management_in_progress(true);
+ let cmd = {
+ CredentialManagement: { DeleteCredential: cred.credential_id },
+ };
+ context.setAttribute("data-ctap-command", JSON.stringify(cmd));
+ open_delete_confirmation_tab();
+ });
+ row.insertCell(2).appendChild(delete_button);
+ });
+ });
+ },
+
+ show_enrollment_list(enrollment_list) {
+ // We may have temporarily hidden the tab when asking the user for a PIN
+ // so we have to show it again.
+ fake_click_event_for_id("bio-enrollments-tab-button");
+ document.getElementById("bio-enrollment-list-subsection").hidden = false;
+ let table = document.getElementById("bio-enrollment-list");
+ var empty_table = document.createElement("table");
+ empty_table.id = "bio-enrollment-list";
+ table.parentNode.replaceChild(empty_table, table);
+ if (!enrollment_list.length) {
+ document.getElementById("bio-enrollment-list-empty-label").hidden = false;
+ return;
+ }
+ document.getElementById("bio-enrollment-list-empty-label").hidden = true;
+ table = document.getElementById("bio-enrollment-list");
+ enrollment_list.forEach(enrollment => {
+ let key_text = enrollment.template_friendly_name ?? "<unnamed>";
+ var row = table.insertRow(0);
+ var key_node = document.createTextNode(key_text);
+ row.insertCell(0).appendChild(key_node);
+ var delete_button = document.createElement("button");
+ delete_button.classList.add("delete-button");
+ delete_button.classList.add("bio-enrollment-button");
+ let garbage_icon = document.createElement("img");
+ garbage_icon.setAttribute("src", "chrome://global/skin/icons/delete.svg");
+ garbage_icon.classList.add("delete-icon");
+ delete_button.appendChild(garbage_icon);
+ let delete_text = document.createElement("span");
+ delete_text.setAttribute("data-l10n-id", "about-webauthn-delete-button");
+ delete_button.appendChild(delete_text);
+ delete_button.addEventListener("click", function () {
+ let context = document.getElementById("confirmation-context");
+ context.textContent = key_text;
+ bio_enrollment_in_progress(true);
+ let cmd = {
+ BioEnrollment: { DeleteEnrollment: enrollment.template_id },
+ };
+ context.setAttribute("data-ctap-command", JSON.stringify(cmd));
+ open_delete_confirmation_tab();
+ });
+ row.insertCell(1).appendChild(delete_button);
+ });
+ },
+};
+
+function set_info_text(l10nId) {
+ document.getElementById("info-text-div").hidden = false;
+ let field = document.getElementById("info-text-field");
+ field.setAttribute("data-l10n-id", l10nId);
+ document.getElementById("ctap2-token-info").style.display = "none";
+}
+
+function show_results_banner(result, l10n, l10n_args) {
+ let ctap_result = document.getElementById("ctap-listen-result");
+ ctap_result.setAttribute("data-l10n-id", l10n);
+ ctap_result.classList.add(result);
+ if (l10n_args) {
+ ctap_result.setAttribute("data-l10n-args", l10n_args);
+ }
+}
+
+function hide_results_banner() {
+ let res_banner = document.getElementById("ctap-listen-result");
+ let res_div = document.getElementById("ctap-listen-div");
+ let empty_banner = document.createElement("label");
+ empty_banner.id = "ctap-listen-result";
+ res_div.replaceChild(empty_banner, res_banner);
+}
+
+function operation_in_progress(name, in_progress) {
+ let buttons = Array.from(document.getElementsByClassName(name));
+ buttons.forEach(button => {
+ button.disabled = in_progress;
+ });
+}
+
+function credential_management_in_progress(in_progress) {
+ operation_in_progress("credentials-button", in_progress);
+}
+
+function bio_enrollment_in_progress(in_progress) {
+ operation_in_progress("bio-enrollment-button", in_progress);
+}
+
+function clear_bio_enrollment_samples() {
+ // Remove all previous status updates
+ let up = document.getElementById("enrollment-update");
+ while (up.firstChild) {
+ up.removeChild(up.lastChild);
+ }
+ document.getElementById("enrollment-name").value = "";
+}
+
+function fake_click_event_for_id(id) {
+ // Not using document.getElementById(id).click();
+ // here, because we have to add additional data, so we don't
+ // hide the results-div here, if there is any. 'Normal' clicking
+ // by the user will hide it.
+ const evt = new CustomEvent("click", {
+ detail: { skip_results_clearing: true },
+ });
+ document.getElementById(id).dispatchEvent(evt);
+}
+
+function reset_page() {
+ // Hide everything that needs a device to know if it should be displayed
+ document.getElementById("ctap2-token-info").style.display = "none";
+ Array.from(document.getElementsByClassName("optional-category")).forEach(
+ div => {
+ div.style.display = "none";
+ }
+ );
+
+ // Only display the "please connect a device" - text
+ set_info_text("about-webauthn-text-connect-device");
+
+ // Clear results and input fields
+ hide_results_banner();
+ var divs = Array.from(document.getElementsByTagName("input"));
+ divs.forEach(div => {
+ div.value = "";
+ });
+
+ sidebar_set_disabled(false);
+
+ // ListCredentials
+ credential_management_in_progress(false);
+ document.getElementById("credential-list-subsection").hidden = true;
+
+ // BioEnrollment
+ clear_bio_enrollment_samples();
+ document.getElementById("bio-enrollment-list-subsection").hidden = true;
+ bio_enrollment_in_progress(false);
+
+ AboutWebauthnManagerJS._previous_tab = "";
+ AboutWebauthnManagerJS._current_tab = "";
+
+ // Not using `document.getElementById("info-tab-button").click();`
+ // here, because if we were focused on a category-button that got removed (e.g.
+ // when unplugging the device), we have to reset the ARIA-related attributes
+ // first, before we can click on the button, otherwise the a11y-tests
+ // will complain. So we fake the click again.
+ const evt = {
+ detail: {},
+ currentTarget: document.getElementById("info-tab-button"),
+ };
+ open_info_tab(evt);
+}
+
+function sidebar_set_disabled(disabled) {
+ var cats = Array.from(document.getElementsByClassName("category"));
+ cats.forEach(cat => {
+ if (disabled) {
+ cat.classList.add("disabled-category");
+ } else {
+ cat.classList.remove("disabled-category");
+ }
+ });
+}
+
+function check_pin_repeat_is_correct(button) {
+ let pin = document.getElementById("new-pin");
+ let pin_repeat = document.getElementById("new-pin-repeat");
+ let has_current_pin = !document.getElementById("current-pin-div").hidden;
+ let current_pin = document.getElementById("current-pin");
+ let can_enable_button =
+ pin.value != null && pin.value != "" && pin.value == pin_repeat.value;
+ if (has_current_pin && !current_pin.value) {
+ can_enable_button = false;
+ }
+ if (!can_enable_button) {
+ pin.classList.add("different");
+ pin_repeat.classList.add("different");
+ document.getElementById("set-pin-button").disabled = true;
+ document.getElementById("change-pin-button").disabled = true;
+ return false;
+ }
+ pin.classList.remove("different");
+ pin_repeat.classList.remove("different");
+ document.getElementById("set-pin-button").disabled = false;
+ document.getElementById("change-pin-button").disabled = false;
+ return true;
+}
+
+function send_pin() {
+ close_temporary_overlay_tab();
+ let pin = document.getElementById("pin-required").value;
+ AboutWebauthnService.pinCallback(0, pin);
+}
+
+function set_pin() {
+ let pin = document.getElementById("new-pin").value;
+ let cmd = { SetPIN: pin };
+ AboutWebauthnService.runCommand(JSON.stringify(cmd));
+}
+
+function change_pin() {
+ let curr_pin = document.getElementById("current-pin").value;
+ let new_pin = document.getElementById("new-pin").value;
+ let cmd = { ChangePIN: [curr_pin, new_pin] };
+ AboutWebauthnService.runCommand(JSON.stringify(cmd));
+}
+
+function list_credentials() {
+ credential_management_in_progress(true);
+ let cmd = { CredentialManagement: "GetCredentials" };
+ AboutWebauthnService.runCommand(JSON.stringify(cmd));
+}
+
+function list_bio_enrollments() {
+ bio_enrollment_in_progress(true);
+ let cmd = { BioEnrollment: "GetEnrollments" };
+ AboutWebauthnService.runCommand(JSON.stringify(cmd));
+}
+
+function show_add_bio_enrollment_section() {
+ const evt = new CustomEvent("click", {
+ detail: { temporary_overlay: true },
+ });
+ open_tab(evt, "add-bio-enrollment-section");
+ document.getElementById("enrollment-name").focus();
+}
+
+function start_bio_enrollment() {
+ bio_enrollment_in_progress(true);
+ let name = document.getElementById("enrollment-name").value;
+ if (!name) {
+ name = null; // Empty means "Not set"
+ }
+ let cmd = { BioEnrollment: { StartNewEnrollment: name } };
+ AboutWebauthnService.runCommand(JSON.stringify(cmd));
+}
+
+function cancel_transaction() {
+ credential_management_in_progress(false);
+ bio_enrollment_in_progress(false);
+ AboutWebauthnService.cancel(0);
+}
+
+function confirm_deletion() {
+ let context = document.getElementById("confirmation-context");
+ let cmd = context.getAttribute("data-ctap-command");
+ AboutWebauthnService.runCommand(cmd);
+}
+
+function cancel_confirmation() {
+ credential_management_in_progress(false);
+ bio_enrollment_in_progress(false);
+ close_temporary_overlay_tab();
+}
+
+async function onLoad() {
+ document.getElementById("set-pin-button").addEventListener("click", set_pin);
+ document
+ .getElementById("change-pin-button")
+ .addEventListener("click", change_pin);
+ document
+ .getElementById("list-credentials-button")
+ .addEventListener("click", list_credentials);
+ document
+ .getElementById("list-bio-enrollments-button")
+ .addEventListener("click", list_bio_enrollments);
+ document
+ .getElementById("add-bio-enrollment-button")
+ .addEventListener("click", show_add_bio_enrollment_section);
+ document
+ .getElementById("start-enrollment-button")
+ .addEventListener("click", start_bio_enrollment);
+ document
+ .getElementById("new-pin")
+ .addEventListener("input", check_pin_repeat_is_correct);
+ document
+ .getElementById("new-pin-repeat")
+ .addEventListener("input", check_pin_repeat_is_correct);
+ document
+ .getElementById("current-pin")
+ .addEventListener("input", check_pin_repeat_is_correct);
+ let info_button = document.getElementById("info-tab-button");
+ info_button.addEventListener("click", open_info_tab);
+ info_button.addEventListener("keydown", handle_keydowns);
+ let pin_button = document.getElementById("pin-tab-button");
+ pin_button.addEventListener("click", open_pin_tab);
+ pin_button.addEventListener("keydown", handle_keydowns);
+ let credentials_button = document.getElementById("credentials-tab-button");
+ credentials_button.addEventListener("click", open_credentials_tab);
+ credentials_button.addEventListener("keydown", handle_keydowns);
+ let bio_enrollments_button = document.getElementById(
+ "bio-enrollments-tab-button"
+ );
+ bio_enrollments_button.addEventListener("click", open_bio_enrollments_tab);
+ bio_enrollments_button.addEventListener("keydown", handle_keydowns);
+ document
+ .getElementById("send-pin-button")
+ .addEventListener("click", send_pin);
+ document
+ .getElementById("cancel-send-pin-button")
+ .addEventListener("click", cancel_transaction);
+ document
+ .getElementById("cancel-enrollment-button")
+ .addEventListener("click", cancel_transaction);
+ document
+ .getElementById("cancel-confirmation-button")
+ .addEventListener("click", cancel_confirmation);
+ document
+ .getElementById("confirm-deletion-button")
+ .addEventListener("click", confirm_deletion);
+ AboutWebauthnManagerJS.init();
+ try {
+ AboutWebauthnService.listen();
+ } catch (ex) {
+ set_info_text("about-webauthn-text-not-available");
+ AboutWebauthnManagerJS.uninit();
+ }
+}
+
+function handle_keydowns(event) {
+ let index;
+ let event_was_handled = true;
+ let tabs = Array.from(document.getElementsByClassName("category"));
+ if (tabs.length <= 0) {
+ return;
+ }
+
+ switch (event.key) {
+ case "ArrowLeft":
+ case "ArrowUp":
+ if (event.currentTarget === tabs[0]) {
+ event.currentTarget.focus();
+ } else {
+ index = tabs.indexOf(event.currentTarget);
+ tabs[index - 1].focus();
+ }
+ break;
+
+ case "ArrowRight":
+ case "ArrowDown":
+ if (event.currentTarget === tabs[tabs.length - 1]) {
+ event.currentTarget.focus();
+ } else {
+ index = tabs.indexOf(event.currentTarget);
+ tabs[index + 1].focus();
+ }
+ break;
+
+ case "Home":
+ tabs[0].focus();
+ break;
+
+ case "End":
+ tabs[tabs.length - 1].focus();
+ break;
+
+ case "Enter":
+ case " ":
+ event.currentTarget.click();
+ break;
+
+ default:
+ event_was_handled = false;
+ break;
+ }
+
+ if (event_was_handled) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+}
+
+function open_tab(evt, tabName) {
+ var tabcontent, tablinks;
+ // Hide all others
+ tabcontent = Array.from(document.getElementsByClassName("tabcontent"));
+ tabcontent.forEach(tab => {
+ tab.style.display = "none";
+ });
+ // Display the one we selected
+ document.getElementById(tabName).style.display = "block";
+
+ // If this is a temporary overlay, like pin-required, we don't
+ // touch the sidebar and which button is selected.
+ if (!evt.detail.temporary_overlay) {
+ tablinks = Array.from(document.getElementsByClassName("category"));
+ tablinks.forEach(tablink => {
+ tablink.removeAttribute("selected");
+ tablink.setAttribute("aria-selected", "false");
+ tablink.setAttribute("tabindex", "-1");
+ tablink.disabled = false;
+ });
+ evt.currentTarget.setAttribute("selected", "true");
+ evt.currentTarget.setAttribute("tabindex", "0");
+ evt.currentTarget.setAttribute("aria-selected", "true");
+ }
+
+ if (!evt.detail.skip_results_clearing) {
+ hide_results_banner();
+ }
+ sidebar_set_disabled(false);
+ AboutWebauthnManagerJS._previous_tab = AboutWebauthnManagerJS._current_tab;
+ AboutWebauthnManagerJS._current_tab = tabName;
+}
+
+function open_info_tab(evt) {
+ open_tab(evt, "token-info-section");
+}
+function open_pin_tab(evt) {
+ open_tab(evt, "set-change-pin-section");
+}
+function open_credentials_tab(evt) {
+ open_tab(evt, "credential-management-section");
+}
+function open_bio_enrollments_tab(evt) {
+ // We can only list, if there are any registered already
+ if (
+ AboutWebauthnManagerJS._curr_data.options.bioEnroll === true ||
+ AboutWebauthnManagerJS._curr_data.options.userVerificationMgmtPreview ===
+ true
+ ) {
+ document.getElementById("list-bio-enrollments-button").style.display =
+ "inline-block";
+ } else {
+ document.getElementById("list-bio-enrollments-button").style.display =
+ "none";
+ }
+ open_tab(evt, "bio-enrollment-section");
+}
+function open_reset_tab(evt) {
+ open_tab(evt, "reset-token-section");
+}
+function open_pin_required_tab() {
+ // Remove any old value we might have had
+ document.getElementById("pin-required").value = "";
+ const evt = new CustomEvent("click", {
+ detail: {
+ temporary_overlay: true,
+ skip_results_clearing: true, // We might be called multiple times, if PIN was invalid
+ },
+ });
+ open_tab(evt, "pin-required-section");
+ document.getElementById("pin-required").focus();
+ // This is a temporary overlay, so we don't want the
+ // user to click away from it, unless via the Cancel-button.
+ sidebar_set_disabled(true);
+}
+function close_temporary_overlay_tab() {
+ const evt = new CustomEvent("click", {
+ detail: { temporary_overlay: true },
+ });
+ open_tab(evt, AboutWebauthnManagerJS._previous_tab);
+ sidebar_set_disabled(false);
+}
+function open_delete_confirmation_tab() {
+ const evt = new CustomEvent("click", {
+ detail: {
+ temporary_overlay: true,
+ },
+ });
+ open_tab(evt, "confirm-deletion-section");
+ // This is a temporary overlay, so we don't want the
+ // user to click away from it, unless via the Cancel-button.
+ sidebar_set_disabled(true);
+}
+
+try {
+ AboutWebauthnService = Cc["@mozilla.org/webauthn/service;1"].getService(
+ Ci.nsIWebAuthnService
+ );
+ document.addEventListener("DOMContentLoaded", onLoad);
+ window.addEventListener("beforeunload", event => {
+ AboutWebauthnManagerJS.uninit();
+ if (AboutWebauthnService) {
+ AboutWebauthnService.cancel(0);
+ }
+ });
+} catch (ex) {
+ // Do nothing if we fail to create a singleton instance,
+ // showing the default no-module message.
+ console.error(ex);
+}
diff --git a/toolkit/components/aboutwebauthn/jar.mn b/toolkit/components/aboutwebauthn/jar.mn
new file mode 100644
index 0000000000..daf0ff5aaa
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+ content/global/aboutWebauthn.css (content/aboutWebauthn.css)
+ content/global/aboutWebauthn.html (content/aboutWebauthn.html)
+ content/global/aboutWebauthn.js (content/aboutWebauthn.js)
diff --git a/toolkit/components/aboutwebauthn/moz.build b/toolkit/components/aboutwebauthn/moz.build
new file mode 100644
index 0000000000..4cf77ef103
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/moz.build
@@ -0,0 +1,13 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "DOM: Web Authentication")
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] not in ("windows", "android"):
+ BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+JAR_MANIFESTS += ["jar.mn"]
diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser.toml b/toolkit/components/aboutwebauthn/tests/browser/browser.toml
new file mode 100644
index 0000000000..a95e23b956
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/browser.toml
@@ -0,0 +1,21 @@
+[DEFAULT]
+head = "head.js"
+prefs = [
+ "security.webauth.webauthn=true",
+ "security.webauth.webauthn_enable_softtoken=false",
+ "security.webauth.webauthn_enable_android_fido2=false",
+ "security.webauth.webauthn_enable_usbtoken=false",
+ "security.webauthn.ctap2=true",
+]
+
+["browser_aboutwebauthn_aria_keycontrols.js"]
+
+["browser_aboutwebauthn_bio.js"]
+
+["browser_aboutwebauthn_credentials.js"]
+
+["browser_aboutwebauthn_info.js"]
+
+["browser_aboutwebauthn_no_token.js"]
+
+["browser_aboutwebauthn_pin.js"]
diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_aria_keycontrols.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_aria_keycontrols.js
new file mode 100644
index 0000000000..44e7f3dcd3
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_aria_keycontrols.js
@@ -0,0 +1,195 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var doc, tab;
+
+add_setup(async function () {
+ info("Starting about:webauthn");
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:webauthn",
+ waitForLoad: true,
+ });
+
+ doc = tab.linkedBrowser.contentDocument;
+ let ops = {
+ credMgmt: true,
+ clientPin: true,
+ bioEnroll: true,
+ };
+ send_auth_info_and_check_categories(doc, ops);
+});
+
+registerCleanupFunction(async function () {
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function moving_up_and_down() {
+ doc.getElementById("info-tab-button").focus();
+ [
+ ["UP", "DOWN"],
+ ["LEFT", "RIGHT"],
+ ].forEach(dirs => {
+ let up = dirs[0];
+ let down = dirs[1];
+ EventUtils.sendKey(down);
+ is(
+ doc.activeElement,
+ doc.getElementById("pin-tab-button"),
+ "Wrong element active"
+ );
+ EventUtils.sendKey(down);
+ is(
+ doc.activeElement,
+ doc.getElementById("credentials-tab-button"),
+ "Wrong element active"
+ );
+ EventUtils.sendKey(down);
+ is(
+ doc.activeElement,
+ doc.getElementById("bio-enrollments-tab-button"),
+ "Wrong element active"
+ );
+ // Trying to go down further should do nothing
+ EventUtils.sendKey(down);
+ is(
+ doc.activeElement,
+ doc.getElementById("bio-enrollments-tab-button"),
+ "Wrong element active"
+ );
+ EventUtils.sendKey(down);
+ is(
+ doc.activeElement,
+ doc.getElementById("bio-enrollments-tab-button"),
+ "Wrong element active"
+ );
+
+ // Going back up again
+ EventUtils.sendKey(up);
+ is(
+ doc.activeElement,
+ doc.getElementById("credentials-tab-button"),
+ "Wrong element active"
+ );
+ EventUtils.sendKey(up);
+ is(
+ doc.activeElement,
+ doc.getElementById("pin-tab-button"),
+ "Wrong element active"
+ );
+ EventUtils.sendKey(up);
+ is(
+ doc.activeElement,
+ doc.getElementById("info-tab-button"),
+ "Wrong element active"
+ );
+ // Trying to go up further should do nothing
+ EventUtils.sendKey(up);
+ is(
+ doc.activeElement,
+ doc.getElementById("info-tab-button"),
+ "Wrong element active"
+ );
+ EventUtils.sendKey(up);
+ is(
+ doc.activeElement,
+ doc.getElementById("info-tab-button"),
+ "Wrong element active"
+ );
+ });
+});
+
+add_task(async function switching_sections() {
+ doc.getElementById("info-tab-button").focus();
+ EventUtils.sendKey("DOWN"); // Pin section
+ EventUtils.sendKey("DOWN"); // Credentials section
+ EventUtils.sendKey("RETURN");
+
+ let credentials_section = doc.getElementById("credential-management-section");
+ isnot(
+ credentials_section.style.display,
+ "none",
+ "credentials section not visible"
+ );
+
+ EventUtils.sendKey("UP"); // PIN section
+ EventUtils.sendKey("RETURN");
+
+ let pin_section = doc.getElementById("set-change-pin-section");
+ isnot(pin_section.style.display, "none", "pin section not visible");
+
+ EventUtils.sendKey("DOWN"); // Credentials section
+ EventUtils.sendChar(" "); // Try using Space instead of Return
+
+ isnot(
+ credentials_section.style.display,
+ "none",
+ "credentials section not visible"
+ );
+});
+
+add_task(async function jumping_sections() {
+ doc.getElementById("info-tab-button").focus();
+ EventUtils.sendKey("DOWN"); // Pin section
+ EventUtils.sendKey("DOWN"); // Credentials section
+ is(
+ doc.activeElement,
+ doc.getElementById("credentials-tab-button"),
+ "Wrong element active"
+ );
+
+ EventUtils.sendKey("HOME");
+ is(
+ doc.activeElement,
+ doc.getElementById("info-tab-button"),
+ "Wrong element active"
+ );
+
+ // Another hit of the Home-key should change nothing
+ EventUtils.sendKey("HOME");
+ is(
+ doc.activeElement,
+ doc.getElementById("info-tab-button"),
+ "Wrong element active"
+ );
+
+ EventUtils.sendKey("END");
+ is(
+ doc.activeElement,
+ doc.getElementById("bio-enrollments-tab-button"),
+ "Wrong element active"
+ );
+
+ EventUtils.sendKey("HOME");
+ is(
+ doc.activeElement,
+ doc.getElementById("info-tab-button"),
+ "Wrong element active"
+ );
+
+ EventUtils.sendKey("DOWN"); // Pin section
+ EventUtils.sendKey("DOWN"); // Credentials section
+ is(
+ doc.activeElement,
+ doc.getElementById("credentials-tab-button"),
+ "Wrong element active"
+ );
+
+ EventUtils.sendKey("END");
+ is(
+ doc.activeElement,
+ doc.getElementById("bio-enrollments-tab-button"),
+ "Wrong element active"
+ );
+
+ // Another hit of the "End"-key should change nothing
+ EventUtils.sendKey("END");
+ is(
+ doc.activeElement,
+ doc.getElementById("bio-enrollments-tab-button"),
+ "Wrong element active"
+ );
+});
diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_bio.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_bio.js
new file mode 100644
index 0000000000..79ce5723d1
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_bio.js
@@ -0,0 +1,351 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var doc, tab;
+
+add_setup(async function () {
+ info("Starting about:webauthn");
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:webauthn",
+ waitForLoad: true,
+ });
+
+ doc = tab.linkedBrowser.contentDocument;
+});
+
+registerCleanupFunction(async function () {
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+function send_bio_enrollment_command(state) {
+ let msg = JSON.stringify({
+ type: "bio-enrollment-update",
+ result: state,
+ });
+ Services.obs.notifyObservers(null, "about-webauthn-prompt", msg);
+}
+
+function check_bio_buttons_disabled(disabled) {
+ let buttons = Array.from(doc.getElementsByClassName("bio-enrollment-button"));
+ buttons.forEach(button => {
+ is(button.disabled, disabled);
+ });
+ return buttons;
+}
+
+function check_tab_buttons_aria(button_id) {
+ let button = doc.getElementById(button_id);
+ is(button.role, "tab", button_id + " in the sidebar is a tab");
+ ok(
+ button.hasAttribute("aria-controls"),
+ button_id + " in the sidebar is a tab with an assigned tablist"
+ );
+ ok(
+ button.hasAttribute("aria-selected"),
+ button_id + " in the sidebar is a tab that can be or is selected"
+ );
+ ok(
+ button.hasAttribute("tabindex"),
+ button_id + " in the sidebar is a tab with keyboard focusability provided"
+ );
+}
+
+async function send_authinfo_and_open_bio_section(ops) {
+ reset_about_page(doc);
+ send_auth_info_and_check_categories(doc, ops);
+
+ ["pin-tab-button", "credentials-tab-button"].forEach(button_id => {
+ let button = doc.getElementById(button_id);
+ is(button.style.display, "none", button_id + " in the sidebar not hidden");
+ check_tab_buttons_aria(button_id);
+ });
+
+ if (ops.bioEnroll !== null || ops.userVerificationMgmtPreview !== null) {
+ let bio_enrollments_tab_button = doc.getElementById(
+ "bio-enrollments-tab-button"
+ );
+ // Check if bio enrollment section is visible
+ isnot(
+ bio_enrollments_tab_button.style.display,
+ "none",
+ "bio enrollment button in the sidebar not visible"
+ );
+ check_tab_buttons_aria("bio-enrollments-tab-button");
+
+ // Click the section and wait for it to open
+ let bio_enrollment_section = doc.getElementById("bio-enrollment-section");
+ bio_enrollments_tab_button.click();
+ isnot(
+ bio_enrollment_section.style.display,
+ "none",
+ "bio enrollment section not visible"
+ );
+ is(
+ bio_enrollments_tab_button.getAttribute("selected"),
+ "true",
+ "bio enrollment section button not selected"
+ );
+ }
+}
+
+add_task(async function bio_enrollment_not_supported() {
+ send_authinfo_and_open_bio_section({
+ bioEnroll: null,
+ userVerificationMgmtPreview: null,
+ });
+ let bio_enrollments_tab_button = doc.getElementById(
+ "bio-enrollments-tab-button"
+ );
+ is(
+ bio_enrollments_tab_button.style.display,
+ "none",
+ "bio enrollments button in the sidebar visible"
+ );
+});
+
+add_task(async function bio_enrollment_supported() {
+ // Setting bioEnroll should show the button in the sidebar
+ // The function is checking this for us.
+ send_authinfo_and_open_bio_section({
+ bioEnroll: true,
+ userVerificationMgmtPreview: null,
+ });
+ // Setting Preview should show the button in the sidebar
+ send_authinfo_and_open_bio_section({
+ bioEnroll: null,
+ userVerificationMgmtPreview: true,
+ });
+ // Setting both should also work
+ send_authinfo_and_open_bio_section({
+ bioEnroll: true,
+ userVerificationMgmtPreview: true,
+ });
+});
+
+add_task(async function bio_enrollment_empty_list() {
+ send_authinfo_and_open_bio_section({ bioEnroll: true });
+
+ let list_bio_enrollments_button = doc.getElementById(
+ "list-bio-enrollments-button"
+ );
+ let add_bio_enrollment_button = doc.getElementById(
+ "add-bio-enrollment-button"
+ );
+ isnot(
+ list_bio_enrollments_button.style.display,
+ "none",
+ "List bio enrollments button in the sidebar not visible"
+ );
+ isnot(
+ add_bio_enrollment_button.style.display,
+ "none",
+ "Add bio enrollment button in the sidebar not visible"
+ );
+
+ // Bio list should initially not be visible
+ let bio_enrollment_list = doc.getElementById(
+ "bio-enrollment-list-subsection"
+ );
+ is(bio_enrollment_list.hidden, true, "bio enrollment list visible");
+
+ list_bio_enrollments_button.click();
+ is(
+ list_bio_enrollments_button.disabled,
+ true,
+ "List bio enrollments button not disabled while op in progress"
+ );
+ is(
+ add_bio_enrollment_button.disabled,
+ true,
+ "Add bio enrollment button not disabled while op in progress"
+ );
+
+ send_bio_enrollment_command({ EnrollmentList: [] });
+ is(
+ list_bio_enrollments_button.disabled,
+ false,
+ "List bio enrollments button disabled"
+ );
+ is(
+ add_bio_enrollment_button.disabled,
+ false,
+ "Add bio enrollment button disabled"
+ );
+
+ is(bio_enrollment_list.hidden, false, "bio enrollments list visible");
+ let bio_enrollment_list_empty_label = doc.getElementById(
+ "bio-enrollment-list-empty-label"
+ );
+ is(
+ bio_enrollment_list_empty_label.hidden,
+ false,
+ "bio enrollments list empty label not visible"
+ );
+});
+
+add_task(async function bio_enrollment_real_data() {
+ send_authinfo_and_open_bio_section({ bioEnroll: true });
+ let list_bio_enrollments_button = doc.getElementById(
+ "list-bio-enrollments-button"
+ );
+ let add_bio_enrollment_button = doc.getElementById(
+ "add-bio-enrollment-button"
+ );
+ list_bio_enrollments_button.click();
+ send_bio_enrollment_command({ EnrollmentList: REAL_DATA });
+ is(
+ list_bio_enrollments_button.disabled,
+ false,
+ "List bio enrollments button disabled"
+ );
+ is(
+ add_bio_enrollment_button.disabled,
+ false,
+ "Add bio enrollment button disabled"
+ );
+ let bio_enrollment_list = doc.getElementById("bio-enrollment-list");
+ is(
+ bio_enrollment_list.rows.length,
+ 2,
+ "bio enrollment list table doesn't contain 2 bio enrollments"
+ );
+ is(bio_enrollment_list.rows[0].cells[0].textContent, "right-middle");
+ is(bio_enrollment_list.rows[1].cells[0].textContent, "right-index");
+ let buttons = check_bio_buttons_disabled(false);
+ // 2 for each bio + 1 for the list button + 1 for the add button + hidden "start enroll"
+ is(buttons.length, 5);
+
+ list_bio_enrollments_button.click();
+ buttons = check_bio_buttons_disabled(true);
+});
+
+add_task(async function bio_enrollment_add_new() {
+ send_authinfo_and_open_bio_section({ bioEnroll: true });
+ let add_bio_enrollment_button = doc.getElementById(
+ "add-bio-enrollment-button"
+ );
+ add_bio_enrollment_button.click();
+ let add_bio_enrollment_section = doc.getElementById(
+ "add-bio-enrollment-section"
+ );
+ isnot(
+ add_bio_enrollment_section.style.display,
+ "none",
+ "Add bio enrollment section invisible"
+ );
+ let enrollment_name = doc.getElementById("enrollment-name");
+ enrollment_name.value = "Test123";
+
+ let start_enrollment_button = doc.getElementById("start-enrollment-button");
+ start_enrollment_button.click();
+
+ check_bio_buttons_disabled(true);
+ send_bio_enrollment_command({
+ SampleStatus: ["Ctap2EnrollFeedbackFpGood", 3],
+ });
+
+ let enrollment_update = doc.getElementById("enrollment-update");
+ is(enrollment_update.children.length, 1);
+ check_bio_buttons_disabled(true);
+
+ send_bio_enrollment_command({
+ SampleStatus: ["Ctap2EnrollFeedbackFpGood", 2],
+ });
+ send_bio_enrollment_command({
+ SampleStatus: ["Ctap2EnrollFeedbackFpGood", 1],
+ });
+ send_bio_enrollment_command({
+ SampleStatus: ["Ctap2EnrollFeedbackFpGood", 0],
+ });
+ is(enrollment_update.children.length, 4);
+ check_bio_buttons_disabled(true);
+ // We tell the about-page that we still have enrollments (bioEnroll: true).
+ // So it will automatically request a refreshed list from the authenticator, disabling
+ // all the buttons again.
+ send_bio_enrollment_command({ AddSuccess: { options: { bioEnroll: true } } });
+ is(enrollment_update.children.length, 0);
+ is(enrollment_name.value, "", "Enrollment name field did not get cleared");
+ check_bio_buttons_disabled(true);
+});
+
+add_task(async function bio_enrollment_delete() {
+ send_authinfo_and_open_bio_section({ bioEnroll: true });
+ let list_bio_enrollments_button = doc.getElementById(
+ "list-bio-enrollments-button"
+ );
+ list_bio_enrollments_button.click();
+ send_bio_enrollment_command({ EnrollmentList: REAL_DATA });
+ let buttons = check_bio_buttons_disabled(false);
+ buttons[1].click();
+ check_bio_buttons_disabled(true);
+ // Tell the about page, we still have enrollments
+ send_bio_enrollment_command({
+ DeleteSuccess: { options: { bioEnroll: true } },
+ });
+ // This will cause it to automatically request the new list, thus disabling all buttons
+ check_bio_buttons_disabled(true);
+ send_bio_enrollment_command({ EnrollmentList: REAL_DATA });
+ buttons = check_bio_buttons_disabled(false);
+ buttons[1].click();
+
+ // Confirmation dialog should pop open
+ let confirmation_section = doc.getElementById("confirm-deletion-section");
+ isnot(
+ confirmation_section.style.display,
+ "none",
+ "Confirmation section did not open."
+ );
+
+ // Check if the label displays the correct data
+ let confirmation_context = doc.getElementById("confirmation-context");
+ // Items get listed in reverse order (inserRow(0) in a loop), so we use [0] instead of [1] here
+ is(
+ confirmation_context.textContent,
+ REAL_DATA[0].template_friendly_name,
+ "Deletion context show wrong credential name"
+ );
+ // Check if the delete-button has the correct context-data
+ let cmd = {
+ BioEnrollment: {
+ DeleteEnrollment: REAL_DATA[0].template_id,
+ },
+ };
+ is(
+ confirmation_context.getAttribute("data-ctap-command"),
+ JSON.stringify(cmd),
+ "Confirm-button has the wrong context data"
+ );
+
+ let cancel_button = doc.getElementById("cancel-confirmation-button");
+ cancel_button.click();
+ is(
+ confirmation_section.style.display,
+ "none",
+ "Confirmation section did not close."
+ );
+ check_bio_buttons_disabled(false);
+
+ // Click the delete-button again
+ buttons[1].click();
+
+ // Now we tell it that we deleted all enrollments (bioEnroll: false)
+ send_bio_enrollment_command({
+ DeleteSuccess: { options: { bioEnroll: false } },
+ });
+ // Thus, all buttons should get re-enabled and the subsection should be hidden
+ check_bio_buttons_disabled(false);
+
+ let bio_enrollment_subsection = doc.getElementById(
+ "bio-enrollment-list-subsection"
+ );
+ is(bio_enrollment_subsection.hidden, true, "Bio Enrollment List not hidden.");
+});
+
+const REAL_DATA = [
+ { template_id: [248, 82], template_friendly_name: "right-index" },
+ { template_id: [14, 163], template_friendly_name: "right-middle" },
+];
diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js
new file mode 100644
index 0000000000..ff45ea2962
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_credentials.js
@@ -0,0 +1,395 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var doc, tab;
+
+add_setup(async function () {
+ info("Starting about:webauthn");
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:webauthn",
+ waitForLoad: true,
+ });
+
+ doc = tab.linkedBrowser.contentDocument;
+});
+
+registerCleanupFunction(async function () {
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+function send_credential_list(credlist) {
+ let num_of_creds = 0;
+ credlist.forEach(domain => {
+ domain.credentials.forEach(c => {
+ num_of_creds += 1;
+ });
+ });
+ let msg = JSON.stringify({
+ type: "credential-management-update",
+ result: {
+ CredentialList: {
+ existing_resident_credentials_count: num_of_creds,
+ max_possible_remaining_resident_credentials_count: 20,
+ credential_list: credlist,
+ },
+ },
+ });
+ Services.obs.notifyObservers(null, "about-webauthn-prompt", msg);
+}
+
+function check_cred_buttons_disabled(disabled) {
+ let buttons = Array.from(doc.getElementsByClassName("credentials-button"));
+ buttons.forEach(button => {
+ is(button.disabled, disabled);
+ });
+ return buttons;
+}
+
+async function send_authinfo_and_open_cred_section(ops) {
+ reset_about_page(doc);
+ send_auth_info_and_check_categories(doc, ops);
+
+ ["pin-tab-button", "bio-enrollments-tab-button"].forEach(button_id => {
+ let button = doc.getElementById(button_id);
+ is(button.style.display, "none", button_id + " in the sidebar not hidden");
+ });
+
+ if (ops.credMgmt !== null || ops.credentialMgmtPreview !== null) {
+ let credentials_tab_button = doc.getElementById("credentials-tab-button");
+ // Check if credentials section is visible
+ isnot(
+ credentials_tab_button.style.display,
+ "none",
+ "credentials button in the sidebar not visible"
+ );
+
+ // Click the section and wait for it to open
+ let credentials_section = doc.getElementById(
+ "credential-management-section"
+ );
+ credentials_tab_button.click();
+ isnot(
+ credentials_section.style.display,
+ "none",
+ "credentials section not visible"
+ );
+ is(
+ credentials_tab_button.getAttribute("selected"),
+ "true",
+ "credentials section button not selected"
+ );
+ }
+}
+
+add_task(async function cred_mgmt_not_supported() {
+ // Not setting credMgmt at all should lead to not showing it in the sidebar
+ send_authinfo_and_open_cred_section({
+ credMgmt: null,
+ credentialMgmtPreview: null,
+ });
+ let credentials_tab_button = doc.getElementById("credentials-tab-button");
+ is(
+ credentials_tab_button.style.display,
+ "none",
+ "credentials button in the sidebar visible"
+ );
+});
+
+add_task(async function cred_mgmt_supported() {
+ // Setting credMgmt should show the button in the sidebar
+ // The function is checking this for us.
+ send_authinfo_and_open_cred_section({
+ credMgmt: true,
+ credentialMgmtPreview: null,
+ });
+ // Setting credMgmtPreview should show the button in the sidebar
+ send_authinfo_and_open_cred_section({
+ credMgmt: null,
+ credentialMgmtPreview: true,
+ });
+ // Setting both should also work
+ send_authinfo_and_open_cred_section({
+ credMgmt: true,
+ credentialMgmtPreview: true,
+ });
+});
+
+add_task(async function cred_mgmt_empty_list() {
+ send_authinfo_and_open_cred_section({ credMgmt: true });
+
+ let list_credentials_button = doc.getElementById("list-credentials-button");
+ isnot(
+ list_credentials_button.style.display,
+ "none",
+ "credentials button in the sidebar not visible"
+ );
+
+ // Credential list should initially not be visible
+ let credential_list = doc.getElementById("credential-list-subsection");
+ is(credential_list.hidden, true, "credentials list visible");
+
+ list_credentials_button.click();
+ is(
+ list_credentials_button.disabled,
+ true,
+ "credentials button not disabled while op in progress"
+ );
+
+ send_credential_list([]);
+ is(list_credentials_button.disabled, false, "credentials button disabled");
+
+ is(credential_list.hidden, false, "credentials list visible");
+ let credential_list_empty_label = doc.getElementById(
+ "credential-list-empty-label"
+ );
+ is(
+ credential_list_empty_label.hidden,
+ false,
+ "credentials list empty label not visible"
+ );
+});
+
+add_task(async function cred_mgmt_real_data() {
+ send_authinfo_and_open_cred_section({ credMgmt: true });
+ let list_credentials_button = doc.getElementById("list-credentials-button");
+ list_credentials_button.click();
+ send_credential_list(REAL_DATA);
+ is(list_credentials_button.disabled, false, "credentials button disabled");
+ let credential_list = doc.getElementById("credential-list");
+ is(
+ credential_list.rows.length,
+ 4,
+ "credential list table doesn't contain 4 credentials"
+ );
+ is(credential_list.rows[0].cells[0].textContent, "webauthn.io");
+ is(credential_list.rows[0].cells[1].textContent, "fasdfasd");
+ is(credential_list.rows[1].cells[0].textContent, "webauthn.io");
+ is(credential_list.rows[1].cells[1].textContent, "twetwetw");
+ is(credential_list.rows[2].cells[0].textContent, "webauthn.io");
+ is(credential_list.rows[2].cells[1].textContent, "hhhhhg");
+ is(credential_list.rows[3].cells[0].textContent, "example.com");
+ is(credential_list.rows[3].cells[1].textContent, "A. Nother");
+ let buttons = check_cred_buttons_disabled(false);
+ // 4 for each cred + 1 for the list button
+ is(buttons.length, 5);
+
+ list_credentials_button.click();
+ buttons = check_cred_buttons_disabled(true);
+ send_credential_list(REAL_DATA);
+ buttons[2].click();
+ check_cred_buttons_disabled(true);
+
+ // Confirmation section should now be open
+ let credential_section = doc.getElementById("credential-management-section");
+ is(
+ credential_section.style.display,
+ "none",
+ "credential section still visible"
+ );
+ let confirmation_section = doc.getElementById("confirm-deletion-section");
+ isnot(
+ confirmation_section.style.display,
+ "none",
+ "Confirmation section did not open."
+ );
+
+ // Check if the label displays the correct data
+ let confirmation_context = doc.getElementById("confirmation-context");
+ is(
+ confirmation_context.textContent,
+ "webauthn.io - hhhhhg",
+ "Deletion context show wrong credential name"
+ );
+ // Check if the delete-button has the correct context-data
+ let cmd = {
+ CredentialManagement: {
+ DeleteCredential: REAL_DATA[1].credentials[0].credential_id,
+ },
+ };
+ is(
+ confirmation_context.getAttribute("data-ctap-command"),
+ JSON.stringify(cmd),
+ "Confirm-button has the wrong context data"
+ );
+
+ let cancel_button = doc.getElementById("cancel-confirmation-button");
+ cancel_button.click();
+ isnot(
+ credential_section.style.display,
+ "none",
+ "credential section still visible"
+ );
+ is(
+ confirmation_section.style.display,
+ "none",
+ "Confirmation section did not open."
+ );
+ check_cred_buttons_disabled(false);
+});
+
+const REAL_DATA = [
+ {
+ rp: { id: "example.com" },
+ rp_id_hash: [
+ 163, 121, 166, 246, 238, 175, 185, 165, 94, 55, 140, 17, 128, 52, 226,
+ 117, 30, 104, 47, 171, 159, 45, 48, 171, 19, 210, 18, 85, 134, 206, 25,
+ 71,
+ ],
+ credentials: [
+ {
+ user: {
+ id: [65, 46, 32, 78, 111, 116, 104, 101, 114],
+ name: "A. Nother",
+ },
+ credential_id: {
+ id: [
+ 195, 239, 221, 151, 76, 77, 255, 242, 217, 50, 87, 144, 238, 79,
+ 199, 120, 234, 148, 142, 69, 163, 133, 189, 254, 74, 138, 119, 140,
+ 197, 171, 36, 215, 191, 176, 36, 111, 113, 158, 204, 147, 101, 200,
+ 20, 239, 191, 174, 51, 15,
+ ],
+ type: "public-key",
+ },
+ public_key: {
+ 1: 2,
+ 3: -7,
+ "-1": 1,
+ "-2": [
+ 195, 239, 221, 151, 76, 77, 255, 242, 217, 50, 87, 144, 238, 235,
+ 230, 51, 155, 142, 121, 60, 136, 63, 80, 184, 41, 238, 217, 61, 1,
+ 206, 253, 141,
+ ],
+ "-3": [
+ 15, 81, 111, 204, 199, 48, 18, 121, 134, 243, 26, 49, 6, 244, 25,
+ 156, 188, 71, 245, 122, 93, 47, 218, 235, 25, 222, 191, 116, 20, 14,
+ 195, 114,
+ ],
+ },
+ cred_protect: 1,
+ large_blob_key: [
+ 223, 32, 77, 171, 223, 133, 38, 175, 229, 40, 85, 216, 203, 79, 194,
+ 223, 32, 191, 119, 241, 115, 6, 101, 180, 92, 194, 208, 193, 181, 163,
+ 164, 64,
+ ],
+ },
+ ],
+ },
+ {
+ rp: { id: "webauthn.io" },
+ rp_id_hash: [
+ 116, 166, 234, 146, 19, 201, 156, 47, 116, 178, 36, 146, 179, 32, 207, 64,
+ 38, 42, 148, 193, 169, 80, 160, 57, 127, 41, 37, 11, 96, 132, 30, 240,
+ ],
+ credentials: [
+ {
+ user: { id: [97, 71, 104, 111, 97, 71, 104, 110], name: "hhhhhg" },
+ credential_id: {
+ id: [
+ 64, 132, 129, 5, 185, 62, 86, 253, 199, 113, 219, 14, 207, 113, 145,
+ 78, 177, 198, 130, 217, 122, 105, 225, 111, 32, 227, 237, 209, 6,
+ 220, 202, 234, 144, 227, 246, 42, 73, 68, 37, 142, 95, 139, 224, 36,
+ 156, 168, 118, 181,
+ ],
+ type: "public-key",
+ },
+ public_key: {
+ 1: 2,
+ 3: -7,
+ "-1": 1,
+ "-2": [
+ 64, 132, 129, 5, 185, 62, 86, 253, 199, 113, 219, 14, 207, 22, 10,
+ 241, 230, 152, 5, 204, 35, 94, 22, 191, 213, 2, 247, 220, 227, 62,
+ 76, 182,
+ ],
+ "-3": [
+ 13, 30, 30, 149, 170, 118, 78, 115, 101, 218, 245, 52, 154, 242, 67,
+ 146, 17, 184, 112, 225, 51, 47, 242, 157, 195, 80, 76, 101, 147,
+ 161, 3, 185,
+ ],
+ },
+ cred_protect: 1,
+ large_blob_key: [
+ 10, 67, 27, 233, 8, 115, 69, 191, 105, 213, 77, 123, 210, 118, 193,
+ 234, 3, 12, 234, 228, 215, 106, 24, 228, 102, 247, 255, 156, 99, 196,
+ 215, 230,
+ ],
+ },
+ {
+ user: {
+ id: [100, 72, 100, 108, 100, 72, 100, 108, 100, 72, 99],
+ name: "twetwetw",
+ },
+ credential_id: {
+ id: [
+ 29, 8, 25, 57, 66, 234, 22, 27, 227, 141, 77, 93, 233, 234, 251, 61,
+ 100, 199, 176, 97, 112, 48, 172, 118, 145, 0, 156, 76, 156, 215, 18,
+ 25, 118, 32, 241, 127, 13, 177, 249, 101, 26, 209, 142, 116, 74, 95,
+ 117, 29,
+ ],
+ type: "public-key",
+ },
+ public_key: {
+ 1: 2,
+ 3: -7,
+ "-1": 1,
+ "-2": [
+ 29, 8, 25, 57, 66, 234, 22, 27, 227, 141, 77, 93, 233, 154, 113,
+ 177, 251, 161, 54, 76, 150, 15, 6, 143, 117, 214, 232, 215, 118, 41,
+ 116, 19,
+ ],
+ "-3": [
+ 201, 6, 43, 178, 3, 249, 175, 123, 149, 81, 127, 20, 116, 152, 238,
+ 84, 52, 113, 3, 165, 176, 105, 200, 137, 209, 0, 141, 50, 42, 192,
+ 174, 26,
+ ],
+ },
+ cred_protect: 1,
+ large_blob_key: [
+ 125, 175, 155, 1, 14, 247, 182, 241, 234, 66, 115, 236, 200, 223, 176,
+ 88, 88, 88, 202, 173, 147, 217, 9, 193, 114, 7, 29, 169, 224, 179,
+ 187, 188,
+ ],
+ },
+ {
+ user: {
+ id: [90, 109, 70, 122, 90, 71, 90, 104, 99, 50, 81],
+ name: "fasdfasd",
+ },
+ credential_id: {
+ id: [
+ 58, 174, 92, 116, 17, 108, 28, 203, 233, 192, 182, 60, 80, 236, 133,
+ 196, 98, 32, 103, 53, 107, 48, 46, 236, 228, 166, 21, 33, 228, 75,
+ 85, 191, 71, 18, 214, 177, 56, 254, 89, 28, 187, 220, 241, 7, 21,
+ 11, 45, 151,
+ ],
+ type: "public-key",
+ },
+ public_key: {
+ 1: 2,
+ 3: -7,
+ "-1": 1,
+ "-2": [
+ 58, 174, 92, 116, 17, 108, 28, 203, 233, 192, 182, 60, 80, 111, 150,
+ 192, 102, 255, 211, 156, 5, 186, 29, 105, 154, 79, 14, 2, 106, 159,
+ 57, 156,
+ ],
+ "-3": [
+ 182, 164, 251, 221, 237, 36, 239, 109, 146, 184, 146, 29, 143, 16,
+ 35, 188, 84, 148, 247, 83, 181, 40, 88, 111, 245, 13, 254, 206, 242,
+ 164, 234, 159,
+ ],
+ },
+ cred_protect: 1,
+ large_blob_key: [
+ 113, 154, 217, 69, 45, 108, 115, 20, 104, 43, 214, 120, 253, 93, 223,
+ 204, 125, 234, 220, 148, 98, 118, 98, 157, 175, 41, 154, 97, 87, 233,
+ 208, 171,
+ ],
+ },
+ ],
+ },
+];
diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js
new file mode 100644
index 0000000000..f66f5929fc
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_info.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var doc, tab;
+
+add_setup(async function () {
+ info("Starting about:webauthn");
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:webauthn",
+ waitForLoad: true,
+ });
+
+ doc = tab.linkedBrowser.contentDocument;
+});
+
+registerCleanupFunction(async function () {
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+function send_auth_data_and_check(auth_data) {
+ Services.obs.notifyObservers(
+ null,
+ "about-webauthn-prompt",
+ JSON.stringify({ type: "selected-device", auth_info: auth_data })
+ );
+
+ let info_text = doc.getElementById("info-text-div");
+ is(info_text.hidden, true, "Start prompt not hidden");
+
+ let info_section = doc.getElementById("token-info-section");
+ isnot(info_section.style.display, "none", "Info section hidden");
+}
+
+add_task(async function multiple_devices() {
+ Services.obs.notifyObservers(
+ null,
+ "about-webauthn-prompt",
+ JSON.stringify({ type: "select-device" })
+ );
+
+ let info_text = doc.getElementById("info-text-div");
+ is(info_text.hidden, false, "Start prompt hidden");
+ let field = doc.getElementById("info-text-field");
+ is(
+ field.getAttribute("data-l10n-id"),
+ "about-webauthn-text-select-device",
+ "Field does not prompt user to touch device for selection"
+ );
+});
+
+add_task(async function multiple_devices() {
+ send_auth_data_and_check(REAL_AUTH_INFO_1);
+ reset_about_page(doc);
+ send_auth_data_and_check(REAL_AUTH_INFO_2);
+ reset_about_page(doc);
+ send_auth_data_and_check(REAL_AUTH_INFO_3);
+ reset_about_page(doc);
+});
+
+// Yubikey BIO
+const REAL_AUTH_INFO_1 = {
+ versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE", "FIDO_2_1"],
+ extensions: [
+ "credProtect",
+ "hmac-secret",
+ "largeBlobKey",
+ "credBlob",
+ "minPinLength",
+ ],
+ aaguid: [
+ 216, 82, 45, 159, 87, 91, 72, 102, 136, 169, 186, 153, 250, 2, 243, 91,
+ ],
+ options: {
+ plat: false,
+ rk: true,
+ clientPin: true,
+ up: true,
+ uv: true,
+ pinUvAuthToken: true,
+ noMcGaPermissionsWithClientPin: null,
+ largeBlobs: true,
+ ep: null,
+ bioEnroll: true,
+ userVerificationMgmtPreview: true,
+ uvBioEnroll: null,
+ authnrCfg: true,
+ uvAcfg: null,
+ credMgmt: true,
+ credentialMgmtPreview: true,
+ setMinPINLength: true,
+ makeCredUvNotRqd: true,
+ alwaysUv: false,
+ },
+ max_msg_size: 1200,
+ pin_protocols: [2, 1],
+ max_credential_count_in_list: 8,
+ max_credential_id_length: 128,
+ transports: ["usb"],
+ algorithms: [
+ { alg: -7, type: "public-key" },
+ { alg: -8, type: "public-key" },
+ ],
+ max_ser_large_blob_array: 1024,
+ force_pin_change: false,
+ min_pin_length: 4,
+ firmware_version: 328966,
+ max_cred_blob_length: 32,
+ max_rpids_for_set_min_pin_length: 1,
+ preferred_platform_uv_attempts: 3,
+ uv_modality: 2,
+ certifications: null,
+ remaining_discoverable_credentials: 20,
+ vendor_prototype_config_commands: null,
+};
+
+// Yubikey 5
+const REAL_AUTH_INFO_2 = {
+ versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"],
+ extensions: ["credProtect", "hmac-secret"],
+ aaguid: [
+ 47, 192, 87, 159, 129, 19, 71, 234, 177, 22, 187, 90, 141, 185, 32, 42,
+ ],
+ options: {
+ plat: false,
+ rk: true,
+ clientPin: true,
+ up: true,
+ uv: null,
+ pinUvAuthToken: null,
+ noMcGaPermissionsWithClientPin: null,
+ largeBlobs: null,
+ ep: null,
+ bioEnroll: null,
+ userVerificationMgmtPreview: null,
+ uvBioEnroll: null,
+ authnrCfg: null,
+ uvAcfg: null,
+ credMgmt: null,
+ credentialMgmtPreview: true,
+ setMinPINLength: null,
+ makeCredUvNotRqd: null,
+ alwaysUv: null,
+ },
+ max_msg_size: 1200,
+ pin_protocols: [1],
+ max_credential_count_in_list: 8,
+ max_credential_id_length: 128,
+ transports: ["nfc", "usb"],
+ algorithms: [
+ { alg: -7, type: "public-key" },
+ { alg: -8, type: "public-key" },
+ ],
+ max_ser_large_blob_array: null,
+ force_pin_change: null,
+ min_pin_length: null,
+ firmware_version: null,
+ max_cred_blob_length: null,
+ max_rpids_for_set_min_pin_length: null,
+ preferred_platform_uv_attempts: null,
+ uv_modality: null,
+ certifications: null,
+ remaining_discoverable_credentials: null,
+ vendor_prototype_config_commands: null,
+};
+
+// Nitrokey 3
+const REAL_AUTH_INFO_3 = {
+ versions: ["U2F_V2", "FIDO_2_0", "FIDO_2_1_PRE"],
+ extensions: ["credProtect", "hmac-secret"],
+ aaguid: [
+ 47, 192, 87, 159, 129, 19, 71, 234, 177, 22, 187, 90, 141, 185, 32, 42,
+ ],
+ options: {
+ plat: false,
+ rk: true,
+ clientPin: true,
+ up: true,
+ uv: null,
+ pinUvAuthToken: null,
+ noMcGaPermissionsWithClientPin: null,
+ largeBlobs: null,
+ ep: null,
+ bioEnroll: null,
+ userVerificationMgmtPreview: null,
+ uvBioEnroll: null,
+ authnrCfg: null,
+ uvAcfg: null,
+ credMgmt: null,
+ credentialMgmtPreview: true,
+ setMinPINLength: null,
+ makeCredUvNotRqd: null,
+ alwaysUv: null,
+ },
+ max_msg_size: 1200,
+ pin_protocols: [1],
+ max_credential_count_in_list: 8,
+ max_credential_id_length: 128,
+ transports: ["nfc", "usb"],
+ algorithms: [
+ { alg: -7, type: "public-key" },
+ { alg: -8, type: "public-key" },
+ ],
+ max_ser_large_blob_array: null,
+ force_pin_change: null,
+ min_pin_length: null,
+ firmware_version: null,
+ max_cred_blob_length: null,
+ max_rpids_for_set_min_pin_length: null,
+ preferred_platform_uv_attempts: null,
+ uv_modality: null,
+ certifications: null,
+ remaining_discoverable_credentials: null,
+ vendor_prototype_config_commands: null,
+};
diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js
new file mode 100644
index 0000000000..f6da6c7146
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_no_token.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var doc, tab;
+
+add_setup(async function () {
+ info("Starting about:webauthn");
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:webauthn",
+ waitForLoad: true,
+ });
+
+ doc = tab.linkedBrowser.contentDocument;
+});
+
+registerCleanupFunction(async function () {
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function verify_page_no_token() {
+ let info_text = doc.getElementById("info-text-div");
+ is(info_text.hidden, false, "info-text-div should be visible");
+ let categories = doc.getElementById("categories");
+ is(categories.hidden, false, "categories-sidebar should be invisible");
+ let dev_info = doc.getElementById("info-tab-button");
+ is(
+ dev_info.getAttribute("selected"),
+ "true",
+ "token-info-section not selected by default"
+ );
+ let ctap2_info = doc.getElementById("ctap2-token-info");
+ is(ctap2_info.style.display, "none", "ctap2-info-table is visible");
+});
+
+add_task(async function verify_no_auth_info() {
+ let field = doc.getElementById("info-text-field");
+ let promise = BrowserTestUtils.waitForMutationCondition(
+ field,
+ { attributes: true, attributeFilter: ["data-l10n-id"] },
+ () =>
+ field.getAttribute("data-l10n-id") ===
+ "about-webauthn-text-non-ctap2-device"
+ );
+ Services.obs.notifyObservers(
+ null,
+ "about-webauthn-prompt",
+ JSON.stringify({ type: "selected-device", auth_info: null })
+ );
+ await promise;
+
+ let info_text = doc.getElementById("info-text-div");
+ is(info_text.hidden, false);
+});
diff --git a/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_pin.js b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_pin.js
new file mode 100644
index 0000000000..0849b194a4
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/browser_aboutwebauthn_pin.js
@@ -0,0 +1,218 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const ABOUT_URL = "about:webauthn";
+
+var doc, tab;
+
+async function send_authinfo_and_open_pin_section(ops) {
+ reset_about_page(doc);
+ send_auth_info_and_check_categories(doc, ops);
+
+ ["credentials-tab-button", "bio-enrollments-tab-button"].forEach(
+ button_id => {
+ let button = doc.getElementById(button_id);
+ is(
+ button.style.display,
+ "none",
+ button_id + " in the sidebar not hidden"
+ );
+ }
+ );
+
+ if (ops.clientPin !== null) {
+ let pin_tab_button = doc.getElementById("pin-tab-button");
+ // Check if PIN section is visible
+ isnot(
+ pin_tab_button.style.display,
+ "none",
+ "PIN button in the sidebar not visible"
+ );
+
+ // Click the section and wait for it to open
+ let pin_section = doc.getElementById("set-change-pin-section");
+ pin_tab_button.click();
+ isnot(pin_section.style.display, "none", "PIN section not visible");
+ is(
+ pin_tab_button.getAttribute("selected"),
+ "true",
+ "PIN section button not selected"
+ );
+ }
+}
+
+add_setup(async function () {
+ info("Starting about:webauthn");
+ tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:webauthn",
+ waitForLoad: true,
+ });
+
+ doc = tab.linkedBrowser.contentDocument;
+});
+
+registerCleanupFunction(async function () {
+ // Close tab.
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function pin_not_supported() {
+ // Not setting clientPIN at all should lead to not showing it in the sidebar
+ send_authinfo_and_open_pin_section({ uv: true, clientPin: null });
+ // Check if PIN section is invisible
+ let pin_tab_button = doc.getElementById("pin-tab-button");
+ is(pin_tab_button.style.display, "none", "PIN button in the sidebar visible");
+});
+
+add_task(async function pin_already_set() {
+ send_authinfo_and_open_pin_section({ clientPin: true });
+
+ let set_pin_button = doc.getElementById("set-pin-button");
+ is(set_pin_button.style.display, "none", "Set PIN button visible");
+
+ let change_pin_button = doc.getElementById("change-pin-button");
+ isnot(
+ change_pin_button.style.display,
+ "none",
+ "Change PIN button not visible"
+ );
+
+ let current_pin_div = doc.getElementById("current-pin-div");
+ is(current_pin_div.hidden, false, "Current PIN field not visible");
+
+ // Test that the button is only active if both inputs are set the same
+ let new_pin = doc.getElementById("new-pin");
+ let new_pin_repeat = doc.getElementById("new-pin-repeat");
+ let current_pin = doc.getElementById("current-pin");
+
+ set_text(new_pin, "abcdefg");
+ is(change_pin_button.disabled, true, "Change PIN button not disabled");
+ set_text(new_pin_repeat, "abcde");
+ is(change_pin_button.disabled, true, "Change PIN button not disabled");
+ set_text(new_pin_repeat, "abcdefg");
+ is(change_pin_button.disabled, true, "Change PIN button not disabled");
+ set_text(current_pin, "1234567");
+ is(change_pin_button.disabled, false, "Change PIN button disabled");
+});
+
+add_task(async function pin_not_yet_set() {
+ send_authinfo_and_open_pin_section({ clientPin: false });
+
+ let set_pin_button = doc.getElementById("set-pin-button");
+ isnot(set_pin_button.style.display, "none", "Set PIN button not visible");
+
+ let change_pin_button = doc.getElementById("change-pin-button");
+ is(change_pin_button.style.display, "none", "Change PIN button visible");
+
+ let current_pin_div = doc.getElementById("current-pin-div");
+ is(current_pin_div.hidden, true, "Current PIN field visible");
+
+ // Test that the button is only active if both inputs are set the same
+ let new_pin = doc.getElementById("new-pin");
+ let new_pin_repeat = doc.getElementById("new-pin-repeat");
+
+ set_text(new_pin, "abcdefg");
+ is(set_pin_button.disabled, true, "Set PIN button not disabled");
+ set_text(new_pin_repeat, "abcde");
+ is(set_pin_button.disabled, true, "Set PIN button not disabled");
+ set_text(new_pin_repeat, "abcdefg");
+ is(set_pin_button.disabled, false, "Set PIN button disabled");
+});
+
+add_task(async function pin_switch_back_and_forth() {
+ // This will click the PIN section button
+ send_authinfo_and_open_pin_section({ clientPin: false });
+
+ let pin_tab_button = doc.getElementById("pin-tab-button");
+ let pin_section = doc.getElementById("set-change-pin-section");
+ let info_tab_button = doc.getElementById("info-tab-button");
+ let info_section = doc.getElementById("token-info-section");
+
+ // a11y-tree is racy here, so we have to wait a tick for it to get up to date
+ await TestUtils.waitForTick();
+ // Now click the "info"-button and verify the correct buttons are highlighted
+ info_tab_button.click();
+ // await info_promise;
+ isnot(info_section.style.display, "none", "info section not visible");
+ is(
+ info_tab_button.getAttribute("selected"),
+ "true",
+ "Info tab button not selected"
+ );
+ isnot(
+ pin_tab_button.getAttribute("selected"),
+ "true",
+ "PIN tab button selected"
+ );
+
+ // Click back to the PIN section
+ pin_tab_button.click();
+ isnot(pin_section.style.display, "none", "PIN section not visible");
+ is(
+ pin_tab_button.getAttribute("selected"),
+ "true",
+ "PIN tab button not selected"
+ );
+ isnot(
+ info_tab_button.getAttribute("selected"),
+ "true",
+ "Info button selected"
+ );
+});
+
+add_task(async function invalid_pin() {
+ send_authinfo_and_open_pin_section({ clientPin: true });
+ let pin_tab_button = doc.getElementById("pin-tab-button");
+ // Click the section and wait for it to open
+ pin_tab_button.click();
+ is(
+ pin_tab_button.getAttribute("selected"),
+ "true",
+ "PIN section button not selected"
+ );
+
+ let change_pin_button = doc.getElementById("change-pin-button");
+
+ // Test that the button is only active if both inputs are set the same
+ let new_pin = doc.getElementById("new-pin");
+ let new_pin_repeat = doc.getElementById("new-pin-repeat");
+ let current_pin = doc.getElementById("current-pin");
+
+ // Needed to activate change_pin_button
+ set_text(new_pin, "abcdefg");
+ set_text(new_pin_repeat, "abcdefg");
+ set_text(current_pin, "1234567");
+
+ // This should silently error out since we have no authenticator
+ change_pin_button.click();
+
+ // Now we fake a response from the authenticator, saying the PIN was invalid
+ let pin_required = doc.getElementById("pin-required-section");
+ let msg = JSON.stringify({ type: "pin-required" });
+ Services.obs.notifyObservers(null, "about-webauthn-prompt", msg);
+ isnot(pin_required.style.display, "none", "PIN required dialog not visible");
+
+ let info_tab_button = doc.getElementById("info-tab-button");
+ is(
+ info_tab_button.classList.contains("disabled-category"),
+ true,
+ "Sidebar not disabled"
+ );
+
+ let pin_field = doc.getElementById("pin-required");
+ let send_pin_button = doc.getElementById("send-pin-button");
+
+ set_text(pin_field, "654321");
+ send_pin_button.click();
+
+ is(
+ pin_required.style.display,
+ "none",
+ "PIN required dialog did not disappear"
+ );
+ let pin_section = doc.getElementById("set-change-pin-section");
+ isnot(pin_section.style.display, "none", "PIN section did not reappear");
+});
diff --git a/toolkit/components/aboutwebauthn/tests/browser/head.js b/toolkit/components/aboutwebauthn/tests/browser/head.js
new file mode 100644
index 0000000000..f6ea51ab3a
--- /dev/null
+++ b/toolkit/components/aboutwebauthn/tests/browser/head.js
@@ -0,0 +1,40 @@
+function set_text(field, text) {
+ field.value = text;
+ field.dispatchEvent(new Event("input"));
+}
+
+async function reset_about_page(doc) {
+ let info_text = doc.getElementById("info-text-div");
+ let msg = JSON.stringify({ type: "listen-success" });
+ let promise = BrowserTestUtils.waitForMutationCondition(
+ info_text,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => info_text.hidden !== false
+ );
+ Services.obs.notifyObservers(null, "about-webauthn-prompt", msg);
+ await promise;
+}
+
+async function send_auth_info_and_check_categories(doc, ops) {
+ let info_text = doc.getElementById("info-text-div");
+ let msg = JSON.stringify({
+ type: "selected-device",
+ auth_info: { options: ops },
+ });
+
+ let promise = BrowserTestUtils.waitForMutationCondition(
+ info_text,
+ { attributes: true, attributeFilter: ["hidden"] },
+ () => info_text.hidden
+ );
+ Services.obs.notifyObservers(null, "about-webauthn-prompt", msg);
+ await promise;
+
+ // Info should be shown always, so we use it as a canary
+ let info_tab_button = doc.getElementById("info-tab-button");
+ isnot(
+ info_tab_button.style.display,
+ "none",
+ "Info button in the sidebar not visible"
+ );
+}