diff --git a/SELECTOR_INFO.md b/SELECTOR_INFO.md index 9f9e744d2..2d36aeebe 100644 --- a/SELECTOR_INFO.md +++ b/SELECTOR_INFO.md @@ -1151,6 +1151,13 @@ Description: The dropdown menu for default zoom selection Location: about:preferences - Zoom settings Path to .json: modules/data/about_prefs.components.json ``` +``` +Selector Name: unknown-content-type-dialog +Selector Data: unknownContentTypeWindo +Description: The unknown content type dialog +Location: about:preferences#general Applications subsection +Path to .json: modules/data/about_prefs.components.json +``` #### about_profiles ``` Selector Name: profile-container diff --git a/modules/browser_object_navigation.py b/modules/browser_object_navigation.py index b64aad288..60db5ebc2 100644 --- a/modules/browser_object_navigation.py +++ b/modules/browser_object_navigation.py @@ -1,7 +1,8 @@ import logging +import re from typing import Literal -from selenium.common.exceptions import TimeoutException +from selenium.common.exceptions import StaleElementReferenceException, TimeoutException from selenium.webdriver import ActionChains, Firefox from selenium.webdriver.common.by import By from selenium.webdriver.common.keys import Keys @@ -245,6 +246,21 @@ def click_download_button(self) -> BasePage: self.get_download_button().click() return self + @BasePage.context_chrome + def set_always_open_similar_files(self) -> BasePage: + """ + From the downloads panel, right-click the most recent download and set 'Always Open Similar Files'. + """ + downloads_button = self.get_download_button() + downloads_button.click() + + # Locate the latest downloaded file in the panel, open context menu and choose 'Always Open Similar Files' + download_item = self.get_element("download-panel-item") + self.context_click(download_item) + self.context_menu.get_element("context-menu-always-open-similar-files").click() + + return self + @BasePage.context_chrome def wait_for_download_animation_finish( self, downloads_button: WebElement @@ -275,6 +291,14 @@ def click_file_download_warning_panel(self) -> BasePage: self.click_on("file-download-warning-button") return self + # @BasePage.context_chrome + # def wait_for_download_elements(self) -> BasePage: + # """ + # Wait for download elements to be present. + # """ + # self.element_visible("download-target-element") + # return self + def wait_for_item_to_download(self, filename: str) -> BasePage: """ Check the downloads tool in the toolbar to wait for a given file to download @@ -298,6 +322,34 @@ def wait_for_item_to_download(self, filename: str) -> BasePage: self.driver.implicitly_wait(original_timeout) return self + @BasePage.context_chrome + def verify_download_name(self, expected_pattern: str) -> BasePage: + """ + Verify download name matches expected pattern. + Argument: + expected_pattern: Regex pattern to match against download name + """ + download_name = self.get_element("download-target-element") + download_value = download_name.get_attribute("value") + assert re.match(expected_pattern, download_value), ( + f"The download name is incorrect: {download_value}" + ) + return self + + @BasePage.context_chrome + def wait_for_download_completion(self) -> BasePage: + """Wait until the most recent download reaches 100% progress.""" + + def _download_complete(_): + try: + element = self.get_element("download-progress-element") + return element.get_attribute("value") == "100" + except StaleElementReferenceException: + return False + + self.wait.until(_download_complete) + return self + @BasePage.context_chrome def refresh_page(self) -> BasePage: """ diff --git a/modules/data/about_prefs.components.json b/modules/data/about_prefs.components.json index 63628bc54..2b2d50092 100644 --- a/modules/data/about_prefs.components.json +++ b/modules/data/about_prefs.components.json @@ -541,5 +541,13 @@ "selectorData": "menuitem[data-l10n-id='preferences-default-zoom-value'][value='{.*}']", "strategy": "css", "groups": [] - } + }, + + "unknown-content-type-dialog": { + "selectorData": "unknownContentTypeWindow", + "strategy": "id", + "groups": [ + "doNotCache" + ] + } } diff --git a/modules/data/navigation.components.json b/modules/data/navigation.components.json index 33108ce28..61c18f3c9 100644 --- a/modules/data/navigation.components.json +++ b/modules/data/navigation.components.json @@ -405,6 +405,24 @@ "groups": [] }, + "download-target-element": { + "selectorData": "downloadTarget", + "strategy": "class", + "groups": [] + }, + + "download-progress-element": { + "selectorData": "downloadProgress", + "strategy": "class", + "groups": [] + }, + + "download-details-element": { + "selectorData": "downloadDetailsNormal", + "strategy": "class", + "groups": [] + }, + "bookmark-in-bar": { "selectorData": "toolbarbutton.bookmark-item", "strategy": "css", diff --git a/modules/page_object_prefs.py b/modules/page_object_prefs.py index db11765ca..8f870b236 100644 --- a/modules/page_object_prefs.py +++ b/modules/page_object_prefs.py @@ -1,4 +1,5 @@ import datetime +import json import re from time import sleep from typing import List, Literal @@ -638,6 +639,49 @@ def click_popup_panel_button(self, field: str) -> BasePage: self.get_element("panel-popup-button", labels=[field]).click() return self + def get_app_name_for_mime_type(self, mime_type: str) -> str: + """ + Return the application name associated with a given MIME type in about:preferences. + Argument: + mime_type: the MIME type to look up (e.g., "application/msword"). + """ + # Locate the row for the given MIME type + mime_type_item = self.get_element("mime-type-item", labels=[mime_type]) + + # Find the description element that contains application info + action_description = self.get_element( + "mime-type-item-description", parent_element=mime_type_item + ) + + # Parse the JSON data-l10n-args attribute and extract app name + mime_type_data = json.loads(action_description.get_attribute("data-l10n-args")) + return mime_type_data["app-name"] + + def set_pdf_handling_to_always_ask(self) -> BasePage: + """ + Set PDF content type handling to "Always ask" in Applications settings. + """ + self.click_on("pdf-content-type") + self.click_on("pdf-actions-menu") + menu = self.get_element("pdf-actions-menu") + menu.send_keys(Keys.DOWN) + menu.send_keys(Keys.ENTER) + return self + + @BasePage.context_chrome + def handle_unknown_content_dialog(self) -> BasePage: + """ + Wait for the unknown content type dialog to appear and close it with Escape. + """ + self.wait.until(lambda _: len(self.driver.window_handles) > 1) + self.driver.switch_to.window(self.driver.window_handles[-1]) + self.wait.until(lambda _: self.get_element("unknown-content-type-dialog")) + + # Close the dialog with Escape + dialog = self.get_element("unknown-content-type-dialog") + dialog.send_keys(Keys.ESCAPE) + return self + class AboutAddons(BasePage): """ diff --git a/tests/downloads/test_add_mime_type_doc.py b/tests/downloads/test_add_mime_type_doc.py index 4b30e5a49..8d72c7e3e 100644 --- a/tests/downloads/test_add_mime_type_doc.py +++ b/tests/downloads/test_add_mime_type_doc.py @@ -1,11 +1,7 @@ -import json -import sys -from os import environ - import pytest from selenium.webdriver import Firefox -from modules.browser_object import ContextMenu, Navigation +from modules.browser_object import Navigation from modules.page_object import AboutPrefs, GenericPage @@ -14,51 +10,44 @@ def test_case(): return "1756748" +# Constants DOC_LINK = "https://sapphire-hendrika-5.tiiny.site/" -WIN_GHA = environ.get("GITHUB_ACTIONS") == "true" and sys.platform.startswith("win") - @pytest.fixture() def delete_files_regex_string(): return r"sample.*\.doc" -@pytest.mark.skipif(WIN_GHA, reason="Test unstable in Windows Github Actions") +def expected_app_name(sys_platform: str, opt_ci: bool) -> str: + """ + Decide which default application should be used to open .doc files, based on OS + """ + if sys_platform == "Darwin": + return "TextEdit" if opt_ci else "Pages" + # Linux/Windows use LibreOffice + return "LibreOffice Writer" + + @pytest.mark.noxvfb def test_mime_type_doc(driver: Firefox, sys_platform: str, opt_ci: bool, delete_files): """ - C1756748: Verify the user can add the .doc type + C1756748 - Verify that downloading a .doc file adds a new MIME type entry + and the correct default application is assigned. """ - doc_page = GenericPage(driver, url=DOC_LINK).open() + # Instantiate objects + page = GenericPage(driver, url=DOC_LINK) nav = Navigation(driver) - context_menu = ContextMenu(driver) about_prefs = AboutPrefs(driver, category="general") - doc_page.get_element("sample-doc-download").click() - downloads_button = nav.get_download_button() + # Open the test page with the .doc download link + page.open() + page.click_on("sample-doc-download") - with driver.context(driver.CONTEXT_CHROME): - downloads_button.click() - download_item = nav.get_element("download-panel-item") - nav.context_click(download_item) - context_menu.get_element("context-menu-always-open-similar-files").click() + # Download the file and set 'Always Open Similar Files' + nav.set_always_open_similar_files() + # Verify the MIME type entry exists and default app matches expectation about_prefs.open() - about_prefs.element_exists("mime-type-item", labels=["application/msword"]) - - mime_type_item = about_prefs.get_element( - "mime-type-item", labels=["application/msword"] - ) - action_description_item = about_prefs.get_element( - "mime-type-item-description", parent_element=mime_type_item - ) - - mime_type_data = json.loads(action_description_item.get_attribute("data-l10n-args")) - if sys_platform == "Darwin": - if opt_ci: - assert mime_type_data["app-name"] == "TextEdit" - else: - assert mime_type_data["app-name"] == "Pages" - else: - assert mime_type_data["app-name"] == "LibreOffice Writer" + app_name = about_prefs.get_app_name_for_mime_type("application/msword") + assert app_name == expected_app_name(sys_platform, opt_ci) diff --git a/tests/downloads/test_add_zip_type.py b/tests/downloads/test_add_zip_type.py index 850de5d29..426e30769 100644 --- a/tests/downloads/test_add_zip_type.py +++ b/tests/downloads/test_add_zip_type.py @@ -4,7 +4,6 @@ import pytest from selenium.webdriver import Firefox -from modules.browser_object_context_menu import ContextMenu from modules.browser_object_navigation import Navigation from modules.page_object_generics import GenericPage from modules.page_object_prefs import AboutPrefs @@ -50,28 +49,26 @@ def test_add_zip_type( """ C1756743: Verify that the user can add the .zip mime type to Firefox """ - # instantiate object - web_page = GenericPage(driver, url=ZIP_URL).open() + # Instantiate objects + web_page = GenericPage(driver, url=ZIP_URL) nav = Navigation(driver) - context_menu = ContextMenu(driver) about_prefs = AboutPrefs(driver, category="general") web_page.elements |= temp_selectors # Click on the available zip + web_page.open() web_page.click_on("github-code-button") web_page.click_on("github-download-button") # In the download panel right-click on the download and click "Always Open Similar Files" - with driver.context(driver.CONTEXT_CHROME): - nav.context_click(nav.get_element("download-panel-item")) - context_menu.get_element("context-menu-always-open-similar-files").click() + nav.set_always_open_similar_files() # Open about:preferences and check that zip mime type is present in the application list about_prefs.open() - about_prefs.element_exists("mime-type-item", labels=["application/zip"]) + about_prefs.get_app_name_for_mime_type("application/zip") - # Remove the directory created as MacOS automatically unzips + # Remove the directory created as macOS automatically unzips if sys_platform == "Darwin": dir_created = os.path.join(home_folder, "Downloads", "api-guidelines-vNext") shutil.rmtree(dir_created) diff --git a/tests/downloads/test_download_pdf.py b/tests/downloads/test_download_pdf.py index 6bb1896fe..382cdfc35 100644 --- a/tests/downloads/test_download_pdf.py +++ b/tests/downloads/test_download_pdf.py @@ -27,14 +27,23 @@ def test_download_pdf( delete_files, ): """ - C1756769: Verify that the user can Download a PDF + C1756769 - Verify that the user can Download a PDF + + Notes: + - Firefox is launched with a new profile (also a test case precondition) that has default download settings. + - This means the OS-level "Save File" dialog will appear for every download. + - Selenium cannot interact with this native dialog directly, so the test + must rely on fixed waits to give the OS time to render the dialog and to + finish writing the file. """ - pdf = GenericPdf(driver, pdf_url=fillable_pdf_url).open() + + # Initialize objects + pdf = GenericPdf(driver, pdf_url=fillable_pdf_url) keyboard = Controller() # Click the download button - download_button = pdf.get_element("download-button") - download_button.click() + pdf.open() + pdf.click_download_button() # Allow time for the download dialog to appear and pressing handle the prompt time.sleep(2) diff --git a/tests/downloads/test_download_pdf_from_context_menu.py b/tests/downloads/test_download_pdf_from_context_menu.py index c2ebabab9..8b3280c1e 100644 --- a/tests/downloads/test_download_pdf_from_context_menu.py +++ b/tests/downloads/test_download_pdf_from_context_menu.py @@ -28,10 +28,19 @@ def test_download_pdf_from_context_menu( ): """ C1756790: Verify that Telemetry is Recorded when Saving a PDF from the Context menu + + Notes: + - Firefox is launched with a new profile (also a test case precondition) that has default download settings. + - This means the OS-level "Save File" dialog will appear for every download. + - Selenium cannot interact with this native dialog directly, so the test + must rely on fixed waits to give the OS time to render the dialog and to + finish writing the file. """ + # Any attempt to refactor this test make the test fail in CI, if time, it can be revised later. from pynput.keyboard import Controller + # Initialize objects pdf = GenericPdf(driver, pdf_url=fillable_pdf_url) pdf.open() keyboard = Controller() diff --git a/tests/downloads/test_mixed_content_download_via_https.py b/tests/downloads/test_mixed_content_download_via_https.py index b75bcb2d4..a43219129 100644 --- a/tests/downloads/test_mixed_content_download_via_https.py +++ b/tests/downloads/test_mixed_content_download_via_https.py @@ -1,13 +1,7 @@ -import re -from time import sleep - import pytest -from selenium.common.exceptions import StaleElementReferenceException from selenium.webdriver import Firefox -from selenium.webdriver.common.by import By -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.support.wait import WebDriverWait +from modules.browser_object import Navigation from modules.page_object import GenericPage @@ -31,42 +25,16 @@ def test_mixed_content_download_via_https(driver: Firefox, delete_files): """ C1756722: Verify that the user can download mixed content via HTTPS """ - + # Initialize objects web_page = GenericPage(driver, url=MIXED_CONTENT_DOWNLOAD_URL) + nav = Navigation(driver) - # Wait up to 30 seconds for test website to wake up and download the content + # Wait for the test website to wake up and download the content web_page.open() - with driver.context(driver.CONTEXT_CHROME): - WebDriverWait(driver, 30).until(EC.title_contains("File Examples")) - - with driver.context(driver.CONTEXT_CHROME): - download_name = WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "downloadTarget")) - ) - - download_status = WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "downloadProgress")) - ) - - # Verify that the desired download target element is present directly, no extra steps needed. - download_value = download_name.get_attribute("value") - assert re.match(r"file-sample_100kB(\(\d+\)).odt$", download_value), ( - f"The download name is incorrect: {download_value}" - ) + web_page.wait.until(lambda _: web_page.title_contains("File Examples")) - # Verify that the download progress has reached 100%, which indicates that the download is complete. - i = 1 - while True: - try: - download_value = download_status.get_attribute("value") - if download_value == "100": - break - except StaleElementReferenceException: - pass + # Verify download name matches expected pattern + nav.verify_download_name(r"file-sample_100kB(\(\d+\))?.odt$") - if i > MAX_CHECKS: - raise TimeoutError( - "Download progress did not reach 100% within reasonable time." - ) - sleep(1) - i = +1 + # Wait for download completion + nav.wait_for_download_completion() diff --git a/tests/downloads/test_set_always_ask_file_type.py b/tests/downloads/test_set_always_ask_file_type.py index de3eb88ea..b9abd7244 100644 --- a/tests/downloads/test_set_always_ask_file_type.py +++ b/tests/downloads/test_set_always_ask_file_type.py @@ -1,9 +1,5 @@ -from time import sleep - import pytest from selenium.webdriver import Firefox -from selenium.webdriver.common.by import By -from selenium.webdriver.common.keys import Keys from modules.browser_object import Navigation from modules.page_object import AboutPrefs @@ -27,21 +23,21 @@ def delete_files_regex_string(): @pytest.mark.ci def test_set_always_ask_file_type(driver: Firefox, delete_files): """ - C1756752: Ensure that the Always ask option in Firefox Applications settings + C1756752 - Ensure that the Always ask option in Firefox Applications settings leads to a dialog asking "What should Firefox do with this file?" when the file type is downloaded. """ + + # Initialize page objects nav = Navigation(driver) - about_prefs = AboutPrefs(driver, category="general").open() + about_prefs = AboutPrefs(driver, category="general") - about_prefs.click_on("pdf-content-type") - about_prefs.click_on("pdf-actions-menu") - menu = about_prefs.get_element("pdf-actions-menu") - menu.send_keys(Keys.DOWN) - menu.send_keys(Keys.ENTER) + # Set PDF handling to "Always ask" + about_prefs.open() + about_prefs.set_pdf_handling_to_always_ask() + # Navigate to download URL and verify dialog appears nav.search(CONTENT_DISPOSITION_ATTACHMENT_URL) - sleep(2) - with driver.context(driver.CONTEXT_CHROME): - driver.switch_to.window(driver.window_handles[-1]) - driver.find_element(By.ID, "unknownContentTypeWindow").send_keys(Keys.ESCAPE) + + # Wait for and handle the unknown content type dialog + about_prefs.handle_unknown_content_dialog() diff --git a/tests/theme_and_toolbar/test_customize_themes_and_redirect.py b/tests/theme_and_toolbar/test_customize_themes_and_redirect.py index 4512faba6..a732c01ad 100644 --- a/tests/theme_and_toolbar/test_customize_themes_and_redirect.py +++ b/tests/theme_and_toolbar/test_customize_themes_and_redirect.py @@ -1,5 +1,6 @@ import pytest from selenium.webdriver import Firefox + from modules.browser_object import Navigation, PanelUi from modules.page_object import AboutAddons @@ -11,7 +12,7 @@ def test_case(): THEMES: dict[str, list[str]] = { "firefox-compact-dark_mozilla_org-heading": [ - "rgb(43, 42, 51)", # classic darker tone + "rgb(43, 42, 51)", # classic darker tone "rgb(143, 143, 148)", # focused dark "rgb(120, 119, 126)", # dark without focus ], @@ -82,7 +83,9 @@ def test_redirect_to_addons(driver: Firefox) -> None: @pytest.mark.parametrize("theme_name", list(THEMES.keys())) -def test_activate_theme_background_matches_expected(driver: Firefox, theme_name: str) -> None: +def test_activate_theme_background_matches_expected( + driver: Firefox, theme_name: str +) -> None: """ C118173: Ensure that activating each theme in about:addons applies the expected background color. Handles Developer Edition vs standard Firefox defaults. @@ -100,9 +103,7 @@ def test_activate_theme_background_matches_expected(driver: Firefox, theme_name: if theme_name == "firefox-compact-light_mozilla_org-heading": pytest.skip("Compact Light is default on Firefox, skipping.") - current_bg = abt_addons.activate_theme( - nav, theme_name, "", perform_assert=False - ) + current_bg = abt_addons.activate_theme(nav, theme_name, "", perform_assert=False) expected_list = THEMES[theme_name] assert any(colors_match(current_bg, exp) for exp in expected_list), ( diff --git a/tests/theme_and_toolbar/test_installed_theme_enabled.py b/tests/theme_and_toolbar/test_installed_theme_enabled.py index 3116b59df..ec39cc802 100644 --- a/tests/theme_and_toolbar/test_installed_theme_enabled.py +++ b/tests/theme_and_toolbar/test_installed_theme_enabled.py @@ -12,7 +12,9 @@ def test_case(): return "118174" -MAC_GHA: bool = environ.get("GITHUB_ACTIONS") == "true" and sys.platform.startswith("darwin") +MAC_GHA: bool = environ.get("GITHUB_ACTIONS") == "true" and sys.platform.startswith( + "darwin" +) AMO_HOST: str = "addons.mozilla.org" AMO_THEMES_PATH: str = "firefox/themes" @@ -44,7 +46,9 @@ def test_installed_theme_enabled(driver: Firefox) -> None: about_addons.choose_sidebar_option("theme") # Capture currently enabled theme title - starting_theme = about_addons.get_element("enabled-theme-title").get_attribute("innerText") + starting_theme = about_addons.get_element("enabled-theme-title").get_attribute( + "innerText" + ) # Go to AMO and install a recommended theme (POM encapsulates waits and flows) AmoThemes(driver).open().install_recommended_theme()