diff --git a/Pipfile b/Pipfile index 38163e737..94087ec5b 100644 --- a/Pipfile +++ b/Pipfile @@ -16,7 +16,7 @@ pypom = "2.2.4" taskcluster-taskgraph = "==9.0.0" jsonpath-ng = "1.6.1" pillow = "<10.5" -pyfxa = "0.7.0" +pyfxa = "0.8.1" ruff = "0.9.6" pytest-rerunfailures = "14.0" slack-sdk = "3.31.0" diff --git a/SELECTOR_INFO.md b/SELECTOR_INFO.md index 928ada223..f8c72f7b0 100644 --- a/SELECTOR_INFO.md +++ b/SELECTOR_INFO.md @@ -2042,6 +2042,34 @@ Location: In the FxA signin page flow Path to .json: modules/data/fxa_home.components.json ``` ``` +Selector Name: do-it-later-button +Selector Data: "inline_recovery_key_setup_create_do_it_later" +Description: Coninute fxa account creation flow +Location: In the FxA signin page flow +Path to .json: modules/data/fxa_home.components.json +``` +``` +Selector Name: login-password-input +Selector Data: "choice-pair-not-now" +Description: Dont pair with phone now +Location: In the FxA signin page flow +Path to .json: modules/data/fxa_home.components.json +``` +``` +Selector Name: signed-in-status +Selector Data: "div[role='status']" +Description: Sign in status +Location: In the FxA signin page flow +Path to .json: modules/data/fxa_home.components.json +``` +``` +Selector Name: manage-sync-button +Selector Data: "signup_confirmed_sync_manage_sync_button" +Description: sign up and sync confirmation +Location: In the FxA signin page flow +Path to .json: modules/data/fxa_home.components.json +``` +``` Selector Name: signin-otp-input Selector Data: "[data-testid='signin-token-code-input-field']" Description: The FxAccount One Time Password entry field diff --git a/modules/browser_object_panel_ui.py b/modules/browser_object_panel_ui.py index f3900b8ab..4b9d30c8b 100644 --- a/modules/browser_object_panel_ui.py +++ b/modules/browser_object_panel_ui.py @@ -71,6 +71,21 @@ def click_sync_sign_in_button(self) -> BasePage: self.click_on("fxa-sign-in") return self + def open_account_toolbar(self): + """ + Open the FxA account toolbar. + """ + self.click_on("sync-user-button") + return self + + def click_finish_sign_in_button(self): + """ + Click FxA finish sign in button. + """ + self.open_account_toolbar() + self.click_on("fxa-finish-sign-in") + return self + def log_out_fxa(self) -> BasePage: """ Click FxA signout button. @@ -89,6 +104,15 @@ def manage_fxa_account(self) -> BasePage: self.get_element("fxa-manage-account-button").click() return self + @BasePage.context_chrome + def manage_fxa_finish_sign_in(self): + """ + Open the FxA management flow to finish sign in. + """ + self.open_account_toolbar() + self.click_on("fxa-manage-account-button") + return self + def confirm_sync_in_progress(self) -> BasePage: """ Check that FxA Sync Label is set to "Syncing…" diff --git a/modules/data/fxa_home.components.json b/modules/data/fxa_home.components.json index 0c38a096f..edadc5ae5 100644 --- a/modules/data/fxa_home.components.json +++ b/modules/data/fxa_home.components.json @@ -1,84 +1,91 @@ { - "login-email-input": { - "selectorData": "input[type='email']", - "strategy": "css", - "groups": [ - "requiredForPage" - ] - - }, - - "submit-button": { - "selectorData": "button[type='submit']", - "strategy": "css", - "groups": [ - "doNotCache" - ] - }, - - "signup-password-input": { - "selectorData": "[data-testid='new-password-input-field']", - "strategy": "css", - "groups": [] - }, - - "signup-password-repeat-input": { - "selectorData": "[data-testid='verify-password-input-field']", - "strategy": "css", - "groups": [] - }, - - "age-input": { - "selectorData": "[data-testid='age-input-field']", - "strategy": "css", - "groups": [] - }, - - "card-header": { - "selectorData": "card-header", - "strategy": "class", - "groups": [] - }, - - "signup-otp-input": { - "selectorData": "[data-testid='confirm-signup-code-input-field']", - "strategy": "css", - "groups": [] - }, - - "signin-otp-input": { - "selectorData": "[data-testid='signin-token-code-input-field']", - "strategy": "css", - "groups": [] - }, - - "otp-input": { - "selectorData": "input[inputmode='numeric']", - "strategy": "css", - "groups": [] - }, - - "connected-heading": { - "selectorData": "fxa-connected-heading", - "strategy": "id", - "groups": [] - }, - - "continue-browsing-link": { - "selectorData": "cad-not-now", - "strategy": "id", - "groups": [] - }, - - "login-password-input": { - "selectorData": "input[type='password']", - "strategy": "css", - "groups": [] - }, - - "sign-in-button": { - "selectorData": "use-logged-in", - "strategy": "id", - "groups": [] - } + "login-email-input": { + "selectorData": "input[name='email']", + "strategy": "css", + "groups": [ + "requiredForPage" + ] + }, + "submit-button": { + "selectorData": "button[type='submit']", + "strategy": "css", + "groups": [ + "doNotCache" + ] + }, + "signup-password-input": { + "selectorData": "[data-testid='new-password-input-field']", + "strategy": "css", + "groups": [] + }, + "signup-password-repeat-input": { + "selectorData": "[data-testid='verify-password-input-field']", + "strategy": "css", + "groups": [] + }, + "age-input": { + "selectorData": "[data-testid='age-input-field']", + "strategy": "css", + "groups": [] + }, + "card-header": { + "selectorData": "card-header", + "strategy": "class", + "groups": [] + }, + "signup-otp-input": { + "selectorData": "[data-testid='confirm-signup-code-input-field']", + "strategy": "css", + "groups": [] + }, + "signin-otp-input": { + "selectorData": "[data-testid='signin-token-code-input-field']", + "strategy": "css", + "groups": [] + }, + "otp-input": { + "selectorData": "input[inputmode='numeric']", + "strategy": "css", + "groups": [] + }, + "connected-heading": { + "selectorData": "fxa-connected-heading", + "strategy": "id", + "groups": [] + }, + "continue-browsing-link": { + "selectorData": "cad-not-now", + "strategy": "id", + "groups": [] + }, + "login-password-input": { + "selectorData": "input[type='password']", + "strategy": "css", + "groups": [] + }, + "signed-in-status": { + "selectorData": "div[role='status']", + "strategy": "css", + "groups": [] + }, + "manage-sync-button": { + "selectorData": "signup_confirmed_sync_manage_sync_button", + "strategy": "id", + "groups": [] + }, + "do-it-later-button": { + "selectorData": "inline_recovery_key_setup_create_do_it_later", + "strategy": "id", + "groups": [] + }, + "not-now-button": { + "selectorData": "choice-pair-not-now", + "strategy": "id", + "groups": [] + }, + "sign-in-button": { + "selectorData": "use-logged-in", + "strategy": "id", + "groups": [] + } } diff --git a/modules/data/panel_ui.components.json b/modules/data/panel_ui.components.json index ecdc16ac9..4be365c5e 100644 --- a/modules/data/panel_ui.components.json +++ b/modules/data/panel_ui.components.json @@ -38,6 +38,11 @@ "strategy": "css", "groups": [] }, + "fxa-finish-sign-in": { + "selectorData": "appMenu-header-description", + "strategy": "id", + "groups": [] + }, "fxa-sync-label": { "selectorData": "syncnow-label", @@ -57,6 +62,8 @@ "groups": [] }, + + "customize-toolbar": { "selectorData": "overflowMenu-customize-button", "strategy": "id", diff --git a/modules/page_base.py b/modules/page_base.py index 4636c45b8..a09a74991 100644 --- a/modules/page_base.py +++ b/modules/page_base.py @@ -453,6 +453,11 @@ def title_contains(self, url_part: str) -> Page: self.expect(EC.title_contains(url_part)) return self + def title_is(self, url_part: str) -> Page: + """Expect helper: wait until driver URL is given text or timeout""" + self.expect(EC.title_is(url_part)) + return self + def verify_opened_image_url(self, url_substr: str, pattern: str) -> Page: """ Given a part of a URL and a regex, wait for that substring to exist in diff --git a/modules/page_object_fxa_home.py b/modules/page_object_fxa_home.py index a729a03c5..1c701d5ed 100644 --- a/modules/page_object_fxa_home.py +++ b/modules/page_object_fxa_home.py @@ -1,6 +1,3 @@ -from selenium.common.exceptions import NoSuchElementException, TimeoutException -from selenium.webdriver.support import expected_conditions as EC - from modules.page_base import BasePage @@ -19,20 +16,14 @@ def fill_password(self, password: str) -> BasePage: self.set_content_context() self.fill("login-password-input", password, press_enter=False) self.get_element("submit-button").click() - # If OTP is needed, wait for the field to be ready, else move on. - try: - self.custom_wait(timeout=3).until( - EC.presence_of_element_located(self.get_selector("connected-heading")) - ) - except (TimeoutException, NoSuchElementException): - self.element_exists("otp-input") + self.element_visible("signed-in-status") return self def create_new_account(self, password: str, age=30) -> BasePage: """Fill out the password and age fields, then submit and wait for code""" self.fill("signup-password-input", password, press_enter=False) self.fill("signup-password-repeat-input", password, press_enter=False) - self.fill("age-input", str(age), press_enter=False) + # self.fill("age-input", str(age), press_enter=False) self.element_clickable("submit-button") self.get_element("submit-button").click() self.element_has_text("card-header", "Enter confirmation code") @@ -42,7 +33,7 @@ def fill_otp_code(self, otp: str) -> BasePage: """Given an OTP, confirm the account, submit, and wait for account activation""" self.fill("otp-input", otp, press_enter=False) self.get_element("submit-button").click() - self.element_exists("connected-heading") + self.title_is("Mozilla accounts") return self def finish_account_setup(self, password: str) -> BasePage: @@ -51,4 +42,5 @@ def finish_account_setup(self, password: str) -> BasePage: self.driver.switch_to.window(self.driver.window_handles[-1]) self.fill("login-password-input", password, press_enter=False) self.get_element("submit-button").click() + self.element_visible("signed-in-status") return self diff --git a/tests/sync_and_fxa/conftest.py b/tests/sync_and_fxa/conftest.py index 92fa87356..5b2600476 100644 --- a/tests/sync_and_fxa/conftest.py +++ b/tests/sync_and_fxa/conftest.py @@ -1,17 +1,16 @@ import logging -from datetime import datetime +from datetime import datetime, timezone from time import sleep import pytest from fxa.core import Client -from fxa.errors import OutOfProtocolError from fxa.tests.utils import TestEmailAccount -class FxaPrep: - def __init__(self, url: str, password: str): +class FxaSession: + def __init__(self, url: str, password: str, restmail_session=None): self.client = Client(url) - self.restmail = TestEmailAccount() + self.restmail = restmail_session if restmail_session else TestEmailAccount() self.password = password self.otp_code = None logging.info(self.restmail.email) @@ -39,7 +38,7 @@ def fxa_url(fxa_env): @pytest.fixture() def start_time(): - return datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.%fZ") + return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%fZ") @pytest.fixture() @@ -56,16 +55,28 @@ def add_to_prefs_list(): @pytest.fixture() -def new_fxa_prep(fxa_url: str, acct_password: str) -> FxaPrep: +def fxa_session( + fxa_url: str, fxa_env: str, acct_password: str, fxa_test_account, request +) -> FxaSession: """Create a PyFxA object and return a dict with artifacts""" # Create a testing account using an @restmail.net address. - prep = FxaPrep(fxa_url, acct_password) + if fxa_env == "stage": + fxa_url = "https://api-accounts.stage.mozaws.net" + if fxa_test_account: + prep = FxaSession( + fxa_url, acct_password, request.getfixturevalue("restmail_session") + ) + else: + prep = FxaSession(fxa_url, acct_password) prep.restmail.clear() yield prep - try: - prep.destroy_account() - except OutOfProtocolError as e: - logging.info(repr(e)) + prep.destroy_account() + + +@pytest.fixture() +def fxa_test_account(): + """return none by default""" + return None @pytest.fixture() @@ -75,11 +86,11 @@ def restmail_session(fxa_test_account) -> TestEmailAccount: @pytest.fixture() -def create_fxa(new_fxa_prep: FxaPrep, get_otp_code) -> FxaPrep: +def create_fxa(fxa_session: FxaSession, get_otp_code) -> FxaSession: """Create FxA from a PyFxA object""" - new_fxa_prep.create_account() - new_fxa_prep.session.verify_email_code(get_otp_code()) - return new_fxa_prep + fxa_session.create_account() + fxa_session.session.verify_email_code(get_otp_code()) + return fxa_session @pytest.fixture() diff --git a/tests/sync_and_fxa/test_existing_fxa.py b/tests/sync_and_fxa/test_existing_fxa.py index a51ab2fe1..e390f5c40 100644 --- a/tests/sync_and_fxa/test_existing_fxa.py +++ b/tests/sync_and_fxa/test_existing_fxa.py @@ -1,10 +1,8 @@ -import logging from typing import Tuple import pytest -from selenium.common.exceptions import NoSuchElementException, TimeoutException from selenium.webdriver import Firefox -from selenium.webdriver.support import expected_conditions as EC +from selenium.webdriver.common.by import By from modules.browser_object import PanelUi from modules.page_object import FxaHome @@ -25,32 +23,18 @@ def fxa_test_account(): return ("dte_stage_permanent@restmail.net", "Test123???") -# Attempts to deflake this have not been entirely successful -@pytest.mark.skip("Revisit when PyFxA is updated") def test_sync_existing_fxa( driver: Firefox, fxa_test_account: Tuple[str, str], restmail_session, - get_otp_code, screenshot, ): """C131098: User is able to log in with existing FxAccount""" - (username, password) = fxa_test_account + (email, password) = fxa_test_account panel_ui = PanelUi(driver) panel_ui.click_sync_sign_in_button() fxa = FxaHome(driver) - fxa.sign_up_sign_in(username) + fxa.sign_up_sign_in(email) fxa.fill_password(password) - - try: - fxa.custom_wait(timeout=5).until( - EC.presence_of_element_located(fxa.get_selector("otp-input")) - ) - otp = get_otp_code(restmail_session) - logging.info(f"otp code: {otp}") - fxa.fill_otp_code(otp) - except (NoSuchElementException, TimeoutException): - pass - with driver.context(driver.CONTEXT_CHROME): - screenshot("screenshot_test_sync_existing_fxa_chrome") - panel_ui.confirm_sync_in_progress() + status_element = fxa.get_element("signed-in-status").find_element(By.TAG_NAME, "p") + assert "You’re signed in" in status_element.text diff --git a/tests/sync_and_fxa/test_new_fxa.py b/tests/sync_and_fxa/test_new_fxa.py index 146319dbd..061a35438 100644 --- a/tests/sync_and_fxa/test_new_fxa.py +++ b/tests/sync_and_fxa/test_new_fxa.py @@ -1,5 +1,6 @@ import pytest from selenium.webdriver import Firefox +from selenium.webdriver.common.by import By from modules.browser_object import PanelUi from modules.page_object import FxaHome, GenericPage @@ -20,10 +21,7 @@ def acct_password(): return "Test123???" -@pytest.mark.skip( - "Stop spamming stage with fake accounts; remove when we implement acct delete" -) -def test_sync_new_fxa(driver: Firefox, fxa_url: str, new_fxa_prep: dict, get_otp_code): +def test_sync_new_fxa(driver: Firefox, fxa_url: str, fxa_session: dict, get_otp_code): """C131094: The user is able to create a new Firefox Account""" # Navigate to FxA signup flow @@ -34,16 +32,14 @@ def test_sync_new_fxa(driver: Firefox, fxa_url: str, new_fxa_prep: dict, get_otp # Walk through the FxA setup flow fxa = FxaHome(driver) - email = new_fxa_prep.restmail.email + email = fxa_session.restmail.email fxa.sign_up_sign_in(email) - fxa.create_new_account(new_fxa_prep.password) - otp = get_otp_code(new_fxa_prep.restmail) + fxa.create_new_account(fxa_session.password) + otp = get_otp_code(fxa_session.restmail) fxa.fill_otp_code(otp) - fxa.get_element("continue-browsing-link").click() # Walk through the Finish Account Setup flow and confirm sync - fxa.driver.get(fxa_url) - fxa.get_element("submit-button").click() - panel_ui.manage_fxa_account() - fxa.finish_account_setup(new_fxa_prep.password) - panel_ui.confirm_sync_in_progress() + panel_ui.manage_fxa_finish_sign_in() + fxa.finish_account_setup(fxa_session.password) + status_element = fxa.get_element("signed-in-status").find_element(By.TAG_NAME, "p") + assert "You’re signed in" in status_element.text