From f37661f1a048386aec25cc8d0ae3fd19178f755b Mon Sep 17 00:00:00 2001 From: Vineet Deo Date: Sat, 2 Nov 2024 12:48:18 +0200 Subject: [PATCH] Bug 1919657 - Account Hub Auto Discovery Options. r=aleca,freaktechnik **How To Test** - Open account hub from the developer console (openAccountHub(); then enter) - Fill name and you can use either an existing email or the test exchange email testaccounts@o365.thunderbird.net - You can close the auth modal - Flip through the different config options to ensure the selected config data is right - Check against the dark mode colors as well Differential Revision: https://phabricator.services.mozilla.com/D226576 --HG-- extra : amend_source : c4f4367c00638bd77562bed4efdc4666728bb08b --- .../content/widgets/account-hub-footer.mjs | 4 + .../content/widgets/email-auto-form.mjs | 13 + .../content/widgets/email-config-found.mjs | 161 +++++++ ...countHubEmailConfigFoundTemplate.inc.xhtml | 113 ++++- .../accountHubFooterTemplate.inc.xhtml | 3 +- .../accountcreation/views/email.mjs | 195 ++++++-- .../messenger/accountcreation/accountHub.ftl | 27 +- .../browser/account/browser_accountHub.js | 436 ++++++++++++++++++ mail/themes/shared/jar.inc.mn | 2 + mail/themes/shared/mail/accountHub.css | 8 +- mail/themes/shared/mail/accountHubForms.css | 214 +++++++++ .../shared/mail/icons/new/normal/incoming.svg | 25 + .../shared/mail/icons/new/normal/outgoing.svg | 26 ++ 13 files changed, 1170 insertions(+), 57 deletions(-) create mode 100644 mail/themes/shared/mail/icons/new/normal/incoming.svg create mode 100644 mail/themes/shared/mail/icons/new/normal/outgoing.svg diff --git a/mail/components/accountcreation/content/widgets/account-hub-footer.mjs b/mail/components/accountcreation/content/widgets/account-hub-footer.mjs index 12d590bef9..a6dcec993f 100644 --- a/mail/components/accountcreation/content/widgets/account-hub-footer.mjs +++ b/mail/components/accountcreation/content/widgets/account-hub-footer.mjs @@ -45,6 +45,10 @@ class AccountHubFooter extends HTMLElement { this.querySelector("#forward").hidden = !val; } + toggleForwardDisabled(val) { + this.querySelector("#forward").disabled = val; + } + canCustom(val) { const customAction = this.querySelector("#custom"); customAction.hidden = !val; diff --git a/mail/components/accountcreation/content/widgets/email-auto-form.mjs b/mail/components/accountcreation/content/widgets/email-auto-form.mjs index 6bf790971a..52ff844e69 100644 --- a/mail/components/accountcreation/content/widgets/email-auto-form.mjs +++ b/mail/components/accountcreation/content/widgets/email-auto-form.mjs @@ -72,6 +72,11 @@ class EmailAutoForm extends AccountHubStep { }); } + resetState() { + this.querySelector("#autoConfigEmailForm").reset(); + this.#currentConfig = {}; + } + /** * Check whether the user entered the minimum amount of information needed to * update the hostname and domain for the complete form. @@ -79,6 +84,14 @@ class EmailAutoForm extends AccountHubStep { #checkValidEmailForm() { const isValidForm = this.#email.checkValidity() && this.#realName.checkValidity(); + + this.dispatchEvent( + new CustomEvent("config-updated", { + bubbles: true, + detail: { completed: isValidForm }, + }) + ); + const domain = isValidForm ? this.#email.value.split("@")[1].toLowerCase() : ""; diff --git a/mail/components/accountcreation/content/widgets/email-config-found.mjs b/mail/components/accountcreation/content/widgets/email-config-found.mjs index a57b46544f..714c764aed 100644 --- a/mail/components/accountcreation/content/widgets/email-config-found.mjs +++ b/mail/components/accountcreation/content/widgets/email-config-found.mjs @@ -4,11 +4,30 @@ import { AccountHubStep } from "./account-hub-step.mjs"; +const { Sanitizer } = ChromeUtils.importESModule( + "resource:///modules/accountcreation/Sanitizer.sys.mjs" +); + /** * Account Hub Config Found Template * Template ID: #accountHubConfigFoundTemplate (from accountHubConfigFoundTemplate.inc.xhtml) */ + class EmailConfigFound extends AccountHubStep { + /** + * The current email auto config form inputs. + * + * @type {AccountConfig} + */ + #currentConfig; + + /** + * The email auto config form. + * + * @type {HTMLElement} + */ + #protocolForm; + connectedCallback() { if (this.hasConnected) { super.connectedCallback(); @@ -22,6 +41,148 @@ class EmailConfigFound extends AccountHubStep { .getElementById("accountHubEmailConfigFoundTemplate") .content.cloneNode(true); this.appendChild(template); + + this.#protocolForm = this.querySelector("#protocolForm"); + + this.#protocolForm.addEventListener("change", event => { + // Remove 'selected' class from all label elements. + this.querySelectorAll("label.selected").forEach(label => { + label.classList.remove("selected"); + }); + + // Add 'selected' class to the parent label of the selected radio button. + event.target.closest("label").classList.add("selected"); + this.#selectConfig(event.target.value); + }); + + this.#currentConfig = {}; + } + + /** + * Return the current state of the email setup form. + */ + captureState() { + return this.#currentConfig; + } + + /** + * Sets the state of the email config found state. + * + * @param {AccountConfig} configData - Applies the config data to this state. + */ + setState(configData) { + this.#currentConfig = configData; + this.#updateFields(); + } + + /** + * Updates the select config options. + */ + #updateFields() { + if (!this.#currentConfig) { + return; + } + + const configLabels = [ + this.querySelector("#imap"), + this.querySelector("#pop3"), + this.querySelector("#exchange"), + ]; + + const alternatives = this.#currentConfig.incomingAlternatives.map( + a => a.type + ); + + // Initially hide all config options and reset recommended class. + for (const config of configLabels) { + config.hidden = + config.id !== this.#currentConfig.incoming.type && + !alternatives.includes(config.id); + config.classList.toggle( + "recommended-protocol", + config.id === this.#currentConfig.incoming.type + ); + config.querySelector("input").checked = + config.id === this.#currentConfig.incoming.type; + } + + // Dispatch a change event so config selection logic can run. + const recommendedTypeLabel = this.querySelector( + `#${this.#currentConfig.incoming.type}` + ); + const event = new Event("change", { bubbles: true }); + recommendedTypeLabel.querySelector("input").dispatchEvent(event); + } + + /** + * Sets the current selected config. + * + * @param {String} configType - The config type (imap, pop3, exchange). + */ + #selectConfig(configType) { + const username = this.#currentConfig.incoming.username; + + // Grab the config from the list of configs in #currentConfig. + const incoming = [ + this.#currentConfig.incoming, + ...this.#currentConfig.incomingAlternatives, + ].find(({ type }) => type === configType); + + const outgoing = this.#currentConfig.outgoing; + + this.querySelector("#incomingType").textContent = incoming.type; + this.querySelector("#incomingHost").textContent = incoming.hostname; + this.querySelector("#incomingUsername").textContent = username; + this.querySelector("#incomingType").title = incoming.type; + this.querySelector("#incomingHost").title = incoming.hostname; + this.querySelector("#incomingUsername").title = username; + const incomingSSL = Sanitizer.translate(incoming.socketType, { + 0: "no-encryption", + 2: "starttls", + 3: "ssl", + }); + document.l10n.setAttributes( + this.querySelector("#incomingAuth"), + `account-setup-result-${incomingSSL}` + ); + + // Hide outgoing config details if unavailable. + if (!outgoing || incoming.type === "exchange") { + this.querySelector("#outgoingConfigType").hidden = true; + this.querySelector("#outgoingConfig").hidden = true; + this.querySelector("#configSelection").classList.add("single"); + + document.l10n.setAttributes( + this.querySelector("#incomingTypeText"), + "account-hub-result-ews-text" + ); + + return; + } + + this.querySelector("#configSelection").classList.remove("single"); + document.l10n.setAttributes( + this.querySelector("#incomingTypeText"), + "account-hub-result-incoming-server-legend" + ); + this.querySelector("#outgoingConfigType").hidden = false; + this.querySelector("#outgoingConfig").hidden = false; + + this.querySelector("#outgoingType").textContent = outgoing.type; + this.querySelector("#outgoingHost").textContent = outgoing.hostname; + this.querySelector("#outgoingUsername").textContent = outgoing.username; + this.querySelector("#outgoingType").title = outgoing.type; + this.querySelector("#outgoingHost").title = outgoing.hostname; + this.querySelector("#outgoingUsername").title = outgoing.username; + const outgoingSsl = Sanitizer.translate(outgoing.socketType, { + 0: "no-encryption", + 2: "starttls", + 3: "ssl", + }); + document.l10n.setAttributes( + this.querySelector("#outgoingAuth"), + `account-setup-result-${outgoingSsl}` + ); } } diff --git a/mail/components/accountcreation/templates/accountHubEmailConfigFoundTemplate.inc.xhtml b/mail/components/accountcreation/templates/accountHubEmailConfigFoundTemplate.inc.xhtml index 8d9ea0b27d..b4de950a3b 100644 --- a/mail/components/accountcreation/templates/accountHubEmailConfigFoundTemplate.inc.xhtml +++ b/mail/components/accountcreation/templates/accountHubEmailConfigFoundTemplate.inc.xhtml @@ -3,5 +3,116 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. -
+
+ +
diff --git a/mail/components/accountcreation/templates/accountHubFooterTemplate.inc.xhtml b/mail/components/accountcreation/templates/accountHubFooterTemplate.inc.xhtml index 531d1ccab7..b21373090d 100644 --- a/mail/components/accountcreation/templates/accountHubFooterTemplate.inc.xhtml +++ b/mail/components/accountcreation/templates/accountHubFooterTemplate.inc.xhtml @@ -23,7 +23,8 @@ diff --git a/mail/components/accountcreation/views/email.mjs b/mail/components/accountcreation/views/email.mjs index 5a07c7c8a4..76fc63e37d 100644 --- a/mail/components/accountcreation/views/email.mjs +++ b/mail/components/accountcreation/views/email.mjs @@ -29,12 +29,8 @@ const { FindConfig } = ChromeUtils.importESModule( "resource:///modules/accountcreation/FindConfig.sys.mjs" ); -const { - CancelledException, - gAccountSetupLogger, - SuccessiveAbortable, - UserCancelledException, -} = AccountCreationUtils; +const { gAccountSetupLogger, SuccessiveAbortable, UserCancelledException } = + AccountCreationUtils; import "chrome://messenger/content/accountcreation/content/widgets/account-hub-step.mjs"; // eslint-disable-line import/no-unassigned-import import "chrome://messenger/content/accountcreation/content/widgets/account-hub-footer.mjs"; // eslint-disable-line import/no-unassigned-import @@ -136,6 +132,20 @@ class AccountHubEmail extends HTMLElement { */ #hasCancelled; + /** + * The email for the current user. + * + * @type {String} + */ + #email; + + /** + * The real name for the current user. + * + * @type {String} + */ + #realName; + /** * States of the email setup flow, based on the ID's of the steps in the * flow. @@ -147,6 +157,7 @@ class AccountHubEmail extends HTMLElement { id: "emailAutoConfigSubview", nextStep: "emailConfigFoundSubview", previousStep: "", + forwardEnabled: false, customActionFluentID: "", subview: {}, templateId: "email-auto-form", @@ -155,6 +166,7 @@ class AccountHubEmail extends HTMLElement { id: "emailConfigFoundSubview", nextStep: "emailPasswordSubview", previousStep: "autoConfigSubview", + forwardEnabled: true, customActionFluentID: "", subview: {}, templateId: "email-config-found", @@ -163,6 +175,7 @@ class AccountHubEmail extends HTMLElement { id: "emailPasswordSubview", nextStep: "emailSyncAccountsSubview", previousStep: "emailConfigFoundSubview", + forwardEnabled: false, customActionFluentID: "", subview: {}, templateId: "", @@ -171,6 +184,7 @@ class AccountHubEmail extends HTMLElement { id: "emailSyncAccountsSubview", nextStep: "emailAddedSubview", previousStep: "", + forwardEnabled: true, customActionFluentID: "", subview: {}, templateId: "", @@ -179,14 +193,16 @@ class AccountHubEmail extends HTMLElement { id: "emailIncomingConfigSubview", nextStep: "emailOutgoingConfigSubview", previousStep: "", + forwardEnabled: true, customActionFluentID: "", subview: {}, - templateId: "account-hub-email-manual-incoming-form", + templateId: "email-manual-incoming-form", }, outgoingConfigSubview: { id: "emailOutgoingConfigSubview", nextStep: "emailAddedSubview", previousStep: "emailIncomingConfigSubview", + forwardEnabled: true, customActionFluentID: "account-hub-test-configuration", subview: {}, templateId: "email-manual-outgoing-form", @@ -195,6 +211,7 @@ class AccountHubEmail extends HTMLElement { id: "emailAddedSubview", nextStep: "", previousStep: "", + forwardEnabled: true, customActionFluentID: "account-hub-add-new-email", subview: {}, templateId: "", @@ -246,9 +263,13 @@ class AccountHubEmail extends HTMLElement { this.#emailFooter.addEventListener("back", this); this.#emailFooter.addEventListener("forward", this); this.#emailFooter.addEventListener("custom", this); + this.#emailAutoConfigSubview.addEventListener("config-updated", this); this.abortable = null; this.#hasCancelled = false; + this.#currentConfig = {}; + this.#email = ""; + this.#realName = ""; await this.#initUI("autoConfigSubview"); } @@ -315,6 +336,11 @@ class AccountHubEmail extends HTMLElement { this.#emailFooter.canBack(stateDetails.previousStep); this.#emailFooter.canForward(stateDetails.nextStep); this.#emailFooter.canCustom(stateDetails.customActionFluentID); + + // The footer forward button is disabled by default. + if (stateDetails.forwardEnabled) { + this.#emailFooter.toggleForwardDisabled(false); + } } async handleEvent(event) { @@ -338,12 +364,12 @@ class AccountHubEmail extends HTMLElement { this.#hasCancelled = false; const stateData = stateDetails.subview.captureState(); await this.#handleForwardAction(this.#currentState, stateData); - if (!this.#hasCancelled) { - this.#initUI(stateDetails.nextStep); - } else { - this.#hasCancelled = false; - } + // Apply the new state data to the new state. + this.#states[this.#currentState].subview.setState( + this.#currentConfig + ); } catch (error) { + this.#handleAbortable(); stateDetails.subview.showErrorNotification(error.title, error.text); } break; @@ -354,6 +380,9 @@ class AccountHubEmail extends HTMLElement { stateDetails.subview.showErrorNotification(error.title, error.text); } break; + case "config-updated": + this.#emailFooter.toggleForwardDisabled(!event.detail.completed); + break; default: break; } @@ -387,17 +416,40 @@ class AccountHubEmail extends HTMLElement { * button is pressed. * * @param {String} currentState - The current state of the email flow. - * @param {String} stateData - The current state data of the email flow. + * @param {Object} stateData - The current state data of the email flow. */ async #handleForwardAction(currentState, stateData) { switch (currentState) { case "autoConfigSubview": try { this.#emailFooter.canBack(true); - await this.#findConfig(stateData); + this.#email = stateData.email; + this.#realName = stateData.realName; + const config = await this.#findConfig(); + // The config is null if guessConfig couldn't find anything, or is + // cancelled. At this point we determine if the user actually + // cancelled, move to the manual config form to get them to fill in + // details, or move forward to the next step. + if (!config) { + if (!this.#hasCancelled) { + this.#currentConfig = this.#fillAccountConfig( + new AccountConfig() + ); + await this.#initUI("incomingConfigSubview"); + } else { + this.#hasCancelled = false; + break; + } + } else { + this.#currentConfig = this.#fillAccountConfig(config); + await this.#initUI(this.#states[this.#currentState].nextStep); + } } catch (error) { this.#emailFooter.canBack(false); - throw error; + if (!(error instanceof UserCancelledException)) { + // TODO: Throw proper error here; + throw error; + } } break; case "incomingConfigSubview": @@ -440,35 +492,35 @@ class AccountHubEmail extends HTMLElement { #handleAbortable() { if (this.abortable) { this.abortable.cancel(new UserCancelledException()); + this.abortable = null; } } /** * Finds an account configuration from the provided data if available. * - * @param {String} configData - The form config data from initial email form. */ - async #findConfig(configData) { + async #findConfig() { if (this.abortable) { this.#handleAbortable(); } - const accountConfig = new AccountConfig(); - const emailSplit = configData.email.split("@"); - const emailLocal = Sanitizer.nonemptystring(emailSplit[0]); - accountConfig.incoming.username = emailLocal; - accountConfig.outgoing.username = emailLocal; + const emailSplit = this.#email.split("@"); const domain = emailSplit[1]; + const initialConfig = new AccountConfig(); + const emailLocal = Sanitizer.nonemptystring(emailSplit[0]); + initialConfig.incoming.username = emailLocal; + initialConfig.outgoing.username = emailLocal; gAccountSetupLogger.debug("findConfig()"); this.abortable = new SuccessiveAbortable(); - let config = null; + let config = {}; try { config = await FindConfig.parallelAutoDiscovery( this.abortable, domain, - configData.email + this.#email ); } catch (error) { // Error would be thrown if autoDiscovery caused a 401 error. @@ -481,10 +533,18 @@ class AccountHubEmail extends HTMLElement { this.abortable = null; - if (config === null) { - // TODO: Run guessConfig on user input, which prompt return error - // error notification if nothing found. + if (!config) { + try { + config = await this.#guessConfig(domain, initialConfig); + } catch (error) { + // We are returning the initial null config here, as guessConfig does + // not discern errors and always moves to manual config if nothing is + // found. + return config; + } } + + return config; } /** @@ -495,35 +555,64 @@ class AccountHubEmail extends HTMLElement { // Clear error notifications. this.#clearNotifications(); + // TODO: Do guess config on manual config data. + } + + /** + * Guess an account configuration with the provided domain. + * + * @param {String} domain - The domain from the email address. + * @param {AccountConifg} initialConifg - Account Config object. + */ + #guessConfig(domain, initialConfig) { + let configType = "both"; + + if (initialConfig.outgoing?.existingServerKey) { + configType = "incoming"; + } + + const { promise, resolve, reject } = Promise.withResolvers(); this.abortable = GuessConfig.guessConfig( - this.#currentConfig.domain, - (type, hostname, port) => { + domain, + (type, hostname, port, socketType) => { + // The guessConfig search progress is ongoing. gAccountSetupLogger.debug( - `progress callback host: ${hostname}, port: ${port}, type: ${type}` + `${hostname}:${port} socketType=${socketType} ${type}: progress callback` ); }, - // eslint-disable-next-line no-unused-vars config => { - // This will validate and fill all of the form fields, as well as - // enable the continue button. + // The guessConifg was successful. this.abortable = null; - // TODO: Update form fields for both incoming and outgoing here with - // the config object. + resolve(config); }, - error => { + e => { + gAccountSetupLogger.warn(`guessConfig failed: ${e}`); + reject(e); this.abortable = null; - - // guessConfig failed. - if (error instanceof CancelledException) { - return; - } - gAccountSetupLogger.warn(`guessConfig failed: ${error}`); - // Load the manual config view again and show an error notification. - this.showErrorNotification("account-hub-find-settings-failed", ""); }, - this.#currentConfig, - this.#currentConfig.outgoing.existingServerKey ? "incoming" : "both" + initialConfig, + configType ); + + return promise; + } + + /** + * Adds name and email address to AccountConfig object. + * + * @param {AccountConfig} accountConfig - AccountConfig from findConfig(). + * @param {String} password - The password for the account. + * @returns {AccountConfig} - The concrete AccountConfig object. + */ + #fillAccountConfig(configData, password = "") { + AccountConfig.replaceVariables( + configData, + this.#realName, + this.#email, + password + ); + + return configData; } /** @@ -658,7 +747,19 @@ class AccountHubEmail extends HTMLElement { * @returns {boolean} - If the account hub can remove this view. */ reset() { - // Reset saved for when additional accounts can be added in Account Hub. + if (this.abortable) { + return false; + } + + this.#currentState = "autoConfigSubview"; + this.#currentConfig = {}; + this.#hideSubviews(); + this.#clearNotifications(); + this.#states[this.#currentState].subview.hidden = false; + this.#setFooterButtons(); + this.#states[this.#currentState].subview.resetState(); + this.#emailFooter.toggleForwardDisabled(true); + return true; } } diff --git a/mail/locales/en-US/messenger/accountcreation/accountHub.ftl b/mail/locales/en-US/messenger/accountcreation/accountHub.ftl index 7e7dd72cd2..d44c817dac 100644 --- a/mail/locales/en-US/messenger/accountcreation/accountHub.ftl +++ b/mail/locales/en-US/messenger/accountcreation/accountHub.ftl @@ -69,13 +69,19 @@ account-hub-email-continue-button = Continue account-hub-email-confirm-button = Confirm -account-hub-incoming-server-legend = Incoming server +account-hub-result-incoming-server-legend = Incoming server + .title = Incoming server -account-hub-outgoing-server-legend = Outgoing server +account-hub-result-outgoing-server-legend = Outgoing server + .title = Outgoing server account-hub-protocol-label = Protocol -account-hub-hostname-label = Hostname +account-hub-result-hostname-label = Hostname + .title = Hostname + +account-hub-result-authentication-label = Authentication + .title = Authentication account-hub-port-label = Port .title = Set the port number to 0 for autodetection @@ -108,7 +114,8 @@ account-hub-auth-no-authentication-option = account-hub-auth-label = Authentication method -account-hub-username-label = Username +account-hub-result-username-label = Username + .title = Username account-hub-name-label = Full name .accesskey = n @@ -144,3 +151,15 @@ account-hub-email-sync-accounts = Sync your calendars and address books account-hub-test-configuration = Test account-hub-add-new-email = Add another email + +account-hub-result-imap-description = Keep your folders and emails synced on your server + +account-hub-result-pop-description = Keep your folders and emails on your computer + +account-hub-result-ews-shortname = Exchange + +account-hub-result-ews-description = Use Microsoft Exchange Web Services to sync your folders and emails + +account-hub-result-ews-text = Server + +account-hub-result-recommended-label = Recommended diff --git a/mail/test/browser/account/browser_accountHub.js b/mail/test/browser/account/browser_accountHub.js index 5642839524..a3d0ad6c3e 100644 --- a/mail/test/browser/account/browser_accountHub.js +++ b/mail/test/browser/account/browser_accountHub.js @@ -4,10 +4,89 @@ "use strict"; +const { nsMailServer } = ChromeUtils.importESModule( + "resource://testing-common/mailnews/Maild.sys.mjs" +); + +const emailUser = { + name: "John Doe", + email: "john.doe@momo.invalid", + password: "abc12345", + incomingHost: "mail.momo.invalid", + outgoingHost: "mail.momo.invalid", +}; + +const IMAPServer = { + open() { + const { + ImapDaemon, + ImapMessage, + IMAP_RFC2195_extension, + IMAP_RFC3501_handler, + mixinExtension, + } = ChromeUtils.importESModule( + "resource://testing-common/mailnews/Imapd.sys.mjs" + ); + IMAPServer.ImapMessage = ImapMessage; + + this.daemon = new ImapDaemon(); + this.server = new nsMailServer(daemon => { + const handler = new IMAP_RFC3501_handler(daemon); + mixinExtension(handler, IMAP_RFC2195_extension); + + handler.kUsername = "john.doe@momo.invalid"; + handler.kPassword = "abc12345"; + handler.kAuthRequired = true; + handler.kAuthSchemes = ["PLAIN"]; + return handler; + }, this.daemon); + this.server.start(1993); + info(`IMAP server started on port ${this.server.port}`); + + registerCleanupFunction(() => this.close()); + }, + close() { + this.server.stop(); + }, + get port() { + return this.server.port; + }, +}; + +const SMTPServer = { + open() { + const { SmtpDaemon, SMTP_RFC2821_handler } = ChromeUtils.importESModule( + "resource://testing-common/mailnews/Smtpd.sys.mjs" + ); + + this.daemon = new SmtpDaemon(); + this.server = new nsMailServer(daemon => { + const handler = new SMTP_RFC2821_handler(daemon); + handler.kUsername = "john.doe@momo.invalid"; + handler.kPassword = "abc12345"; + handler.kAuthRequired = true; + handler.kAuthSchemes = ["PLAIN"]; + return handler; + }, this.daemon); + this.server.start(1587); + info(`SMTP server started on port ${this.server.port}`); + + registerCleanupFunction(() => this.close()); + }, + close() { + this.server.stop(); + }, + get port() { + return this.server.port; + }, +}; + // TODO: Defer this for when the account hub replaces the account setup tab. // add_task(async function test_account_hub_opening_at_startup() {}); add_task(async function test_account_hub_opening() { + IMAPServer.open(); + SMTPServer.open(); // TODO: Use an actual button once it's implemented in the UI. // Open the dialog. await window.openAccountHub(); @@ -41,3 +120,360 @@ add_task(async function test_account_hub_opening() { "The dialog element was closed" ); }); + +add_task(async function test_account_email_step() { + // Open the dialog. + await window.openAccountHub(); + const hub = document.querySelector("account-hub-container"); + await BrowserTestUtils.waitForMutationCondition( + hub, + { modal: true, subtree: true, childList: true }, + () => hub.shadowRoot.querySelector(".account-hub-dialog"), + "The dialog element was created" + ); + + const dialog = hub.shadowRoot.querySelector(".account-hub-dialog"); + const emailFormPrmise = BrowserTestUtils.waitForMutationCondition( + dialog, + { + subtree: true, + childList: true, + }, + () => dialog.querySelector("email-auto-form") + ); + await emailFormPrmise; + await TestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(dialog.querySelector("email-auto-form")), + "The initial email template is in view." + ); + + const emailTemplate = dialog.querySelector("email-auto-form"); + const nameInput = emailTemplate.querySelector("#realName"); + const emailInput = emailTemplate.querySelector("#email"); + const footerForward = dialog + .querySelector("#emailFooter") + .querySelector("#forward"); + + // Ensure fields are empty. + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + + nameInput.value = ""; + emailInput.value = ""; + + // Check if the input icons are hidden. + const icons = emailTemplate.querySelectorAll("img"); + + for (const icon of icons) { + Assert.ok(BrowserTestUtils.isHidden(icon), `${icon.src} is hidden.`); + } + + Assert.ok( + footerForward.disabled, + "Account Hub footer forward button is disabled." + ); + + // Type a full name into the name input element and check for success. + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + + let inputEvent = BrowserTestUtils.waitForEvent( + nameInput, + "input", + false, + event => event.target.value === "Test User" + ); + EventUtils.sendString("Test User", window); + await inputEvent; + + // Move to email input to trigger animation icon. + EventUtils.synthesizeMouseAtCenter(emailInput, {}, window); + + const nameSuccessIcon = Array.from(icons).find(img => + img.classList.contains("icon-success") + ); + Assert.ok( + BrowserTestUtils.isVisible(nameSuccessIcon), + "Name success icon is visible." + ); + + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + // Delete text and move back to name input to reveal error icon. + const clearInputEvent = BrowserTestUtils.waitForEvent( + nameInput, + "input", + false, + event => event.target.value === "" + ); + nameInput.value = "a"; + EventUtils.synthesizeKey("KEY_Backspace", {}, window); + await clearInputEvent; + + Assert.ok( + BrowserTestUtils.isHidden(nameSuccessIcon), + "Name success icon is hidden." + ); + + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + + const nameDangerIcon = Array.from(icons).find(img => + img.classList.contains("icon-danger") + ); + Assert.ok( + BrowserTestUtils.isVisible(nameDangerIcon), + "Name danger icon is visible." + ); + + // Fill name and incorrect email input, error email icon should be showing. + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + inputEvent = BrowserTestUtils.waitForEvent( + nameInput, + "input", + false, + event => event.target.value === "Test User" + ); + EventUtils.sendString("Test User", window); + await inputEvent; + + EventUtils.synthesizeMouseAtCenter(emailInput, {}, window); + inputEvent = BrowserTestUtils.waitForEvent( + emailInput, + "input", + false, + event => event.target.value === "testUser@" + ); + EventUtils.sendString("testUser@", window); + await inputEvent; + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + + Assert.ok( + BrowserTestUtils.isVisible(emailTemplate.querySelector("#emailWarning")), + "Email danger icon is visible." + ); + + // Fill in correct email input, see email success icon and continue should + // be enabled. + EventUtils.synthesizeMouseAtCenter(emailInput, {}, window); + inputEvent = BrowserTestUtils.waitForEvent( + emailInput, + "input", + false, + event => event.target.value === "testUser@testing.com" + ); + EventUtils.sendString("testing.com", window); + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + + Assert.ok( + BrowserTestUtils.isHidden(emailTemplate.querySelector("#emailWarning")), + "Email danger icon is hidden." + ); + Assert.ok( + BrowserTestUtils.isVisible(emailTemplate.querySelector("#emailSuccess")), + "Email success icon is visible." + ); + + Assert.ok(!footerForward.disabled, "Continue button is enabled."); + + EventUtils.synthesizeMouseAtCenter( + hub.shadowRoot.querySelector("#closeButton"), + {}, + window + ); + + await BrowserTestUtils.waitForEvent(dialog, "close"); + IMAPServer.close(); + SMTPServer.close(); +}); + +const PREF_NAME = "mailnews.auto_config_url"; +const PREF_VALUE = Services.prefs.getCharPref(PREF_NAME); + +add_task(async function test_account_email_config_found() { + IMAPServer.open(); + SMTPServer.open(); + // Set the pref to load a local autoconfig file. + const url = + "http://mochi.test:8888/browser/comm/mail/test/browser/account/xml/"; + Services.prefs.setCharPref(PREF_NAME, url); + + // Fill in email auto form and click continue, waiting for config found + // view to be shown. + await window.openAccountHub(); + const hub = document.querySelector("account-hub-container"); + await BrowserTestUtils.waitForMutationCondition( + hub, + { modal: true, subtree: true, childList: true }, + () => hub.shadowRoot.querySelector(".account-hub-dialog"), + "The dialog element was created" + ); + + const dialog = hub.shadowRoot.querySelector(".account-hub-dialog"); + + const emailFormPrmise = BrowserTestUtils.waitForMutationCondition( + dialog, + { + subtree: true, + childList: true, + }, + () => dialog.querySelector("email-auto-form") + ); + await emailFormPrmise; + await TestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(dialog.querySelector("email-auto-form")), + "The initial email template is in view." + ); + + const emailTemplate = dialog.querySelector("email-auto-form"); + const nameInput = emailTemplate.querySelector("#realName"); + const emailInput = emailTemplate.querySelector("#email"); + const footerForward = dialog + .querySelector("#emailFooter") + .querySelector("#forward"); + + // Ensure fields are empty. + nameInput.value = ""; + emailInput.value = ""; + + EventUtils.synthesizeMouseAtCenter(nameInput, {}, window); + let inputEvent = BrowserTestUtils.waitForEvent( + nameInput, + "input", + false, + event => event.target.value === emailUser.name + ); + EventUtils.sendString(emailUser.name, window); + await inputEvent; + + EventUtils.synthesizeMouseAtCenter(emailInput, {}, window); + inputEvent = BrowserTestUtils.waitForEvent( + emailInput, + "input", + false, + event => event.target.value === emailUser.email + ); + EventUtils.sendString(emailUser.email, window); + await inputEvent; + + Assert.ok(!footerForward.disabled, "Continue button is enabled."); + + // Click continue and wait for config found template to be in view. + EventUtils.synthesizeMouseAtCenter(footerForward, {}, window); + const configFoundTemplate = dialog.querySelector("email-config-found"); + + await TestUtils.waitForCondition( + () => BrowserTestUtils.isVisible(configFoundTemplate), + "The email config found template is in view." + ); + + await TestUtils.waitForCondition( + () => + BrowserTestUtils.isVisible(configFoundTemplate.querySelector("#imap")), + "The IMAP config option is visible" + ); + + Assert.ok( + BrowserTestUtils.isVisible(configFoundTemplate.querySelector("#pop3")), + "POP3 config option is visible" + ); + + // This config should not include exchange. + Assert.ok( + BrowserTestUtils.isHidden(configFoundTemplate.querySelector("#exchange")), + "Exchange config option is hidden" + ); + + // POP3 should be the recommended configuration. + Assert.ok( + BrowserTestUtils.isVisible( + configFoundTemplate.querySelector("#pop3").querySelector(".recommended") + ), + "POP3 is the recommended config option." + ); + + // POP3 should be the selected config. + Assert.ok( + configFoundTemplate.querySelector("#pop3").classList.contains("selected"), + "POP3 is the selected config option." + ); + + // The config details should show the POP3 details. + testConfigResults(configFoundTemplate, "pop"); + + // Select the IMAP config and check the details match. + EventUtils.synthesizeMouseAtCenter( + configFoundTemplate.querySelector("#imap"), + {}, + window + ); + + Assert.ok( + configFoundTemplate.querySelector("#imap").classList.contains("selected"), + "SMTP is the selected config option." + ); + + // The config details should show the IMAP details. + testConfigResults(configFoundTemplate, "imap"); + + EventUtils.synthesizeMouseAtCenter( + hub.shadowRoot.querySelector("#closeButton"), + {}, + window + ); + + await BrowserTestUtils.waitForEvent(dialog, "close"); + + // // Restore the original pref. + Services.prefs.setCharPref(PREF_NAME, PREF_VALUE); + IMAPServer.close(); + SMTPServer.close(); +}); + +function testConfigResults(template, configType) { + const type = configType === "pop" ? "pop3" : configType; + + Assert.equal( + template.querySelector("#incomingType").textContent, + type, + `Incoming type is expected type.` + ); + + Assert.equal( + template.querySelector("#outgoingType").textContent, + "smtp", + `${configType}: Outgoing type is expected type.` + ); + + Assert.equal( + template.querySelector("#incomingHost").textContent, + `${configType}.mail.momo.invalid`, + `${configType}: Incoming host is ${configType}.mail.momo.invalid.` + ); + + Assert.equal( + template.querySelector("#outgoingHost").textContent, + "smtp.mail.momo.invalid", + `${configType}: Outgoing host is expected host.` + ); + + Assert.equal( + template.l10n.getAttributes(template.querySelector("#incomingAuth")).id, + "account-setup-result-ssl", + `${configType}: Incoming auth is expected auth.` + ); + + Assert.equal( + template.l10n.getAttributes(template.querySelector("#outgoingAuth")).id, + "account-setup-result-ssl", + `${configType}: Outgoing auth is expected auth.` + ); + + Assert.equal( + template.querySelector("#incomingUsername").textContent, + "john.doe", + `${configType}: Incoming username is expected username.` + ); + + Assert.equal( + template.querySelector("#outgoingUsername").textContent, + "john.doe", + `${configType}: Outgoing username is expected username.` + ); +} diff --git a/mail/themes/shared/jar.inc.mn b/mail/themes/shared/jar.inc.mn index 59cb6d1319..bb0d3039a6 100644 --- a/mail/themes/shared/jar.inc.mn +++ b/mail/themes/shared/jar.inc.mn @@ -634,12 +634,14 @@ skin/classic/messenger/icons/new/normal/globe-secure.svg (../shared/mail/icons/new/normal/globe-secure.svg) skin/classic/messenger/icons/new/normal/globe.svg (../shared/mail/icons/new/normal/globe.svg) skin/classic/messenger/icons/new/normal/inbox.svg (../shared/mail/icons/new/normal/inbox.svg) + skin/classic/messenger/icons/new/normal/incoming.svg (../shared/mail/icons/new/normal/incoming.svg) skin/classic/messenger/icons/new/normal/link.svg (../shared/mail/icons/new/normal/link.svg) skin/classic/messenger/icons/new/normal/mail-secure.svg (../shared/mail/icons/new/normal/mail-secure.svg) skin/classic/messenger/icons/new/normal/mail.svg (../shared/mail/icons/new/normal/mail.svg) skin/classic/messenger/icons/new/normal/more.svg (../shared/mail/icons/new/normal/more.svg) skin/classic/messenger/icons/new/normal/newsletter.svg (../shared/mail/icons/new/normal/newsletter.svg) skin/classic/messenger/icons/new/normal/outbox.svg (../shared/mail/icons/new/normal/outbox.svg) + skin/classic/messenger/icons/new/normal/outgoing.svg (../shared/mail/icons/new/normal/outgoing.svg) skin/classic/messenger/icons/new/normal/overflow.svg (../shared/mail/icons/new/normal/overflow.svg) skin/classic/messenger/icons/new/normal/rss.svg (../shared/mail/icons/new/normal/rss.svg) skin/classic/messenger/icons/new/normal/sent.svg (../shared/mail/icons/new/normal/sent.svg) diff --git a/mail/themes/shared/mail/accountHub.css b/mail/themes/shared/mail/accountHub.css index cb01fd8efe..0a32c18e6e 100644 --- a/mail/themes/shared/mail/accountHub.css +++ b/mail/themes/shared/mail/accountHub.css @@ -263,13 +263,13 @@ header { margin-inline: 30px; max-height: 40vh; - div:last-child { - border-inline-end: none; - } - &.flex-direction-row { flex-direction: row; } + + &.auto-config-body { + max-width: 650px; + } } .hub-body-column { diff --git a/mail/themes/shared/mail/accountHubForms.css b/mail/themes/shared/mail/accountHubForms.css index fa701abbbd..80202fa3ab 100644 --- a/mail/themes/shared/mail/accountHubForms.css +++ b/mail/themes/shared/mail/accountHubForms.css @@ -337,6 +337,220 @@ div:has(legend) { } } +.config-form-data { + display: grid; + grid-template-columns: 2fr 60%; + column-gap: 20px; + + & .selection-grid { + display: grid; + grid-template-columns: 50% 50%; + grid-template-rows: auto; + gap: 12px; + padding-inline: 12px; + + & .config-column { + display: flex; + flex-direction: column; + gap: 8px; + } + } + + & .form-options { + display: flex; + flex-direction: column; + justify-content: stretch; + margin: 0; + padding: 0; + border: 0; + gap: 10px; + } + + & .config-option { + background-color: light-dark(rgba(254, 255, 255, 0.5), rgba(26, 32, 44, 0.5)); + display: flex; + flex-direction: column; + border-radius: 10px; + cursor: pointer; + overflow: hidden; + font-size: 1.2rem; + line-height: normal; + border: 1px solid transparent; + + & .recommended { + display: none; + text-transform: uppercase; + font-size: 0.9rem; + font-weight: 600; + margin-inline-start: auto; + padding: 2px; + border: 1px solid transparent; + border-radius: 3px; + } + + &.recommended-protocol .recommended { + display: flex; + } + + & .config-title { + color: light-dark( var(--color-primary-default), #feffff); + display: flex; + align-items: center; + padding-block: 8px 2px; + padding-inline: 8px; + font-weight: bold; + + & input[type="radio"] { + appearance: none; + background-color: transparent; + margin-inline-end: 8px; + width: 15px; + height: 15px; + border: 1px solid var(--color-neutral-border-intense); + border-radius: 50%; + display: grid; + place-content: center; + + &::before { + content: ""; + width: 5px; + height: 5px; + border-radius: 50%; + transform: scale(0); + box-shadow: inset 1em 1em var(--color-neutral-base); + } + + &:checked::before { + transform: scale(1); + } + } + } + + & .config-text { + color: var(--color-text-base); + font-weight: 400; + padding-block: 2px 6px; + padding-inline: 9px; + font-size: 1.1rem; + } + + + &.selected { + border-color: var(--color-primary-default); + + & .config-title { + background-color: var(--color-primary-default); + color: var(--color-neutral-base); + padding-block: 6px; + } + + & .config-text { + color: light-dark(#15427c, var(--color-text-base)); + background-color: var(--color-neutral-base); + padding-block: 6px; + } + + & .recommended { + background-color: light-dark(var(--color-primary-soft), transparent); + color: light-dark(var(--color-primary-default), currentColor); + } + + & input[type="radio"]:checked { + border-width: 2px; + border-color: var(--color-neutral-base); + } + } + } + + & .form-selection { + display: flex; + flex-direction: column; + gap: 12px; + background-color: var(--color-neutral-base); + padding: 6px; + border: 1px solid var(--color-primary-default); + border-radius: 10px; + + &.single .selection-grid { + grid-template-columns: auto; + } + + & .config-item-icon { + height: 20px; + width: 20px; + -moz-context-properties: fill, stroke, stroke-opacity; + fill: light-dark(color-mix(in srgb, #bdbdbd 20%, transparent), color-mix(in srgb, #3d4d67 20%, transparent)); + stroke: light-dark(#bdbdbd, #3d4d67); + + &.lighter { + fill: light-dark(color-mix(in srgb, #bdbdbd 5%, transparent), color-mix(in srgb, #3d4d67 5%, transparent)); + } + } + + & .config-directions { + background-color: light-dark(#f0f8ff, #262c40); + padding-block: 10px; + border-radius: 6px; + + & .config-detail { + padding-block: 0; + + & img { + height: 30px; + width: 30px; + } + + & .config-item-data { + p { + &:first-of-type { + color: light-dark(#2176d6, #feffff); + font-weight: 500; + font-size: 1.3rem; + } + + &:last-of-type { + font-size: 1rem; + color: var(--color-text-secondary); + } + } + } + } + } + + & .config-detail { + display: flex; + flex-direction: row; + align-items: center; + gap: 9px; + + & .config-item-data { + overflow: hidden; + display: flex; + flex-direction: column; + gap: 2px; + + & p { + overflow: hidden; + text-overflow: ellipsis; + line-height: normal; + margin: 0; + + &:first-of-type { + font-size: 0.8rem; + text-transform: uppercase; + color: var(--color-text-secondary); + font-weight: 500; + } + + &:last-of-type { + color: var(--color-text-base); + font-weight: 400; + } + } + } + } + } +} .remember-button-container { margin-block-start: -18px; diff --git a/mail/themes/shared/mail/icons/new/normal/incoming.svg b/mail/themes/shared/mail/icons/new/normal/incoming.svg new file mode 100644 index 0000000000..8ea789372d --- /dev/null +++ b/mail/themes/shared/mail/icons/new/normal/incoming.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/mail/themes/shared/mail/icons/new/normal/outgoing.svg b/mail/themes/shared/mail/icons/new/normal/outgoing.svg new file mode 100644 index 0000000000..0a7923cdcc --- /dev/null +++ b/mail/themes/shared/mail/icons/new/normal/outgoing.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + +