tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

qa_tests.py (35139B)


      1 #!/usr/bin/env python3
      2 # This Source Code Form is subject to the terms of the Mozilla Public
      3 # License, v. 2.0. If a copy of the MPL was not distributed with this
      4 # file, You can obtain one at https://mozilla.org/MPL/2.0/.
      5 
      6 
      7 import os
      8 import random
      9 import subprocess
     10 import tempfile
     11 import time
     12 
     13 from basic_tests import SnapTestsBase
     14 from selenium.common.exceptions import TimeoutException
     15 from selenium.webdriver.common.action_chains import ActionChains
     16 from selenium.webdriver.common.by import By
     17 from selenium.webdriver.common.keys import Keys
     18 from selenium.webdriver.support import expected_conditions as EC
     19 from selenium.webdriver.support.select import Select
     20 
     21 
     22 class QATests(SnapTestsBase):
     23    def __init__(self):
     24        self._dir = "qa_tests"
     25        self._http_server = None
     26        self._http_tmpdir = None
     27 
     28        super().__init__(
     29            exp=os.path.join(
     30                self._dir, f"qa_expectations_{self._distro_release()}.json"
     31            )
     32        )
     33 
     34    def _distro_release(self):
     35        with open("/etc/lsb-release") as lsb_release:
     36            f = list(
     37                filter(
     38                    lambda x: x.startswith("DISTRIB_RELEASE"),
     39                    lsb_release.read().split(),
     40                )
     41            )
     42            return f[0].split("=")[1].replace(".", "")
     43 
     44    def _start_local_http_server(self, port=45678):
     45        """Start a local HTTP server with a 1MB random file for download tests."""
     46        # Create temporary directory
     47        tmpdir = tempfile.mkdtemp(prefix="snap-test-http-")
     48 
     49        # Generate 1MB random file
     50        random_file = os.path.join(tmpdir, "testfile.iso")
     51        with open(random_file, "wb") as f:
     52            f.write(os.urandom(1024 * 1024))  # 1MB of random bytes
     53 
     54        # Create simple HTML page with download link
     55        html_file = os.path.join(tmpdir, "index.html")
     56        with open(html_file, "w") as f:
     57            f.write(
     58                """<!DOCTYPE html>
     59 <html>
     60 <head><title>Test Download Page</title></head>
     61 <body>
     62 <h1>Download Test</h1>
     63 <a href="testfile.iso" id="download-link" download="testfile.iso">Download 1MB File</a>
     64 </body>
     65 </html>"""
     66            )
     67 
     68        # Start http.server as background process
     69        server_process = subprocess.Popen(
     70            ["python3", "-m", "http.server", str(port)],
     71            cwd=tmpdir,
     72            stdout=subprocess.DEVNULL,
     73            stderr=subprocess.DEVNULL,
     74        )
     75 
     76        # Give server time to start
     77        time.sleep(0.5)
     78 
     79        self._logger.info(f"Started local HTTP server on port {port} in {tmpdir}")
     80        return server_process, tmpdir
     81 
     82    def _stop_local_http_server(self, server_process, tmpdir):
     83        """Stop the local HTTP server and clean up."""
     84        if server_process:
     85            server_process.terminate()
     86            server_process.wait()
     87            self._logger.info("Stopped local HTTP server")
     88 
     89        # Clean up temporary directory
     90        if tmpdir and os.path.exists(tmpdir):
     91            import shutil
     92 
     93            shutil.rmtree(tmpdir)
     94 
     95    def _test_audio_playback(
     96        self, url, iframe_selector=None, click_to_play=False, video_selector=None
     97    ):
     98        self._logger.info(f"open url {url}")
     99        if url:
    100            self.open_tab(url)
    101 
    102        if iframe_selector:
    103            self._logger.info("find iframe")
    104            iframe = self._wait.until(
    105                EC.visibility_of_element_located((By.CSS_SELECTOR, iframe_selector))
    106            )
    107            self._driver.switch_to.frame(iframe)
    108 
    109        self._logger.info("find video")
    110        video = self._wait.until(
    111            EC.visibility_of_element_located((
    112                By.CSS_SELECTOR,
    113                video_selector or "video",
    114            ))
    115        )
    116        self._wait.until(lambda d: type(video.get_property("duration")) is float)
    117        assert video.get_property("duration") > 0.0, "<video> duration null"
    118 
    119        # For HE-AAC page, Google Drive does not like SPACE
    120        if not click_to_play and video.get_property("autoplay") is False:
    121            self._logger.info("force play")
    122            video.send_keys(Keys.SPACE)
    123 
    124        # Mostly for Google Drive video, click()/play() seems not to really
    125        # work to trigger, but 'k' is required
    126        if not click_to_play:
    127            self._driver.execute_script("arguments[0].click();", video)
    128        else:
    129            video.send_keys("k")
    130 
    131        ref_volume = video.get_property("volume")
    132 
    133        self._logger.info("find video: wait readyState")
    134        self._wait.until(lambda d: video.get_property("readyState") >= 4)
    135 
    136        # Some videos sometimes self-pause?
    137        self._logger.info(
    138            "find video: check paused: {}".format(video.get_property("paused"))
    139        )
    140        self._logger.info(
    141            "find video: check autoplay: {}".format(video.get_property("autoplay"))
    142        )
    143        if click_to_play or (
    144            not click_to_play and video.get_property("paused") is True
    145        ):
    146            self._driver.execute_script("arguments[0].play()", video)
    147 
    148        self._logger.info("find video: sleep")
    149        # let it play at least 500ms
    150        time.sleep(0.5)
    151 
    152        self._logger.info("find video: wait currentTime")
    153        self._wait.until(lambda d: video.get_property("currentTime") >= 0.01)
    154        assert video.get_property("currentTime") >= 0.01, (
    155            "<video> currentTime not moved"
    156        )
    157 
    158        # this should pause
    159        self._logger.info("find video: pause")
    160        self._driver.execute_script("arguments[0].pause()", video)
    161        datum = video.get_property("currentTime")
    162        paused = video.get_property("paused")
    163        time.sleep(1)
    164        datum_after_sleep = video.get_property("currentTime")
    165        self._logger.info(f"datum={datum} datum_after_sleep={datum_after_sleep}")
    166        assert datum == datum_after_sleep, "<video> is sleeping"
    167        assert paused is True, "<video> is paused"
    168 
    169        self._logger.info("find video: unpause")
    170        # unpause and verify playback
    171        self._driver.execute_script("arguments[0].play()", video)
    172        paused = video.get_property("paused")
    173        assert paused is False, "<video> is not paused"
    174        time.sleep(2)
    175        datum_after_resume = video.get_property("currentTime")
    176        self._logger.info(
    177            f"datum_after_resume={datum_after_resume} datum_after_sleep={datum_after_sleep}"
    178        )
    179        # we wait for 2s but it's not super accurate on CI (vbox VMs?),
    180        # observed values +/- 15% so check for more that should avoid
    181        # intermittent failures
    182        assert datum_after_resume >= datum_after_sleep + 0.5, (
    183            "<video> progressed after pause"
    184        )
    185 
    186        self._logger.info("find video: volume")
    187        self._driver.execute_script(
    188            "arguments[0].volume = arguments[1]", video, ref_volume * 0.25
    189        )
    190        new_volume = video.get_property("volume")
    191        assert new_volume == ref_volume * 0.25, (
    192            f"<video> sound volume increased from {ref_volume} to {ref_volume * 0.25} but got {new_volume}"
    193        )
    194 
    195        self._logger.info("find video: done")
    196 
    197    def _test_audio_video_playback(self, url):
    198        iframe_css_selector = "iframe[id*=ucc-]"
    199        self._logger.info(f"open url {url}")
    200        self.open_tab(url)
    201        self._logger.info("find thumbnail")
    202        thumbnail = self._longwait.until(
    203            EC.visibility_of_element_located((By.CSS_SELECTOR, "img"))
    204        )
    205        self._logger.info("click")
    206        self._driver.execute_script("arguments[0].click()", thumbnail)
    207        self._logger.info("audio test")
    208        self._test_audio_playback(
    209            url=None,
    210            iframe_selector=iframe_css_selector,
    211            click_to_play=True,
    212        )
    213 
    214        # switch back
    215        self._driver.switch_to.parent_frame()
    216        self._logger.info("try fullscreen")
    217 
    218        fullscreen_button = self._wait.until(
    219            EC.presence_of_element_located((
    220                By.CSS_SELECTOR,
    221                "button[aria-label*='(f)']",
    222            ))
    223        )
    224        self._driver.execute_script("return arguments[0].click();", fullscreen_button)
    225        time.sleep(1)
    226        fullscreen = self._driver.execute_script("return document.fullscreen")
    227        assert fullscreen, "<video> is fullscreen"
    228 
    229        iframe = self._wait.until(
    230            EC.visibility_of_element_located((By.CSS_SELECTOR, iframe_css_selector))
    231        )
    232        self._driver.switch_to.frame(iframe)
    233        time.sleep(0.5)
    234        # we are again in the iframe
    235        self._logger.info("find video again")
    236        video = self._wait.until(
    237            EC.visibility_of_element_located((By.CSS_SELECTOR, "video"))
    238        )
    239 
    240        self._driver.execute_script("arguments[0].pause();", video)
    241        self._driver.execute_script("document.exitFullscreen()")
    242 
    243    def test_h264_mov(self, exp):
    244        """
    245        C95233
    246        """
    247 
    248        self._test_audio_video_playback(
    249            "https://drive.google.com/file/d/1lY6xYRR_KC0MGosopJA6Kn6uCvJ1RgQm/view?hl=en-US"
    250        )
    251 
    252        return True
    253 
    254    def test_he_aac(self, exp):
    255        """
    256        C95239
    257        """
    258        self._test_audio_playback(
    259            url="https://www2.iis.fraunhofer.de/AAC/multichannel.html",
    260            video_selector="p.inlineVideo > video",
    261        )
    262 
    263        return True
    264 
    265    def test_flac(self, exp):
    266        """
    267        C95240
    268        """
    269 
    270        self._test_audio_playback(
    271            "https://dl.espressif.com/dl/audio/ff-16b-2c-44100hz.flac"
    272        )
    273 
    274        return True
    275 
    276    def test_mp3(self, exp):
    277        """
    278        C95241
    279        """
    280        self._test_audio_playback(
    281            "https://freetestdata.com/wp-content/uploads/2021/09/Free_Test_Data_5MB_MP3.mp3"
    282        )
    283 
    284        return True
    285 
    286    def test_ogg(self, exp):
    287        """
    288        C95244
    289        """
    290        self._test_audio_playback(
    291            "http://www.metadecks.org/software/sweep/audio/demos/beats1.ogg"
    292        )
    293 
    294        return True
    295 
    296    def test_custom_fonts(self, exp):
    297        """
    298        C128146
    299        """
    300 
    301        self.open_tab(
    302            "http://codinginparadise.org/projects/svgweb/samples/demo.html?name=droid%20font1"
    303        )
    304 
    305        renderer = self._wait.until(
    306            EC.visibility_of_element_located((By.ID, "selectRenderer"))
    307        )
    308        self._wait.until(lambda d: len(renderer.text) > 0)
    309 
    310        renderer_drop = Select(renderer)
    311        renderer_drop.select_by_visible_text("browser native svg")
    312 
    313        font = self._wait.until(EC.visibility_of_element_located((By.ID, "selectSVG")))
    314        self._wait.until(lambda d: len(font.text) > 0)
    315 
    316        font_drop = Select(font)
    317        font_drop.select_by_value("droid font1")
    318 
    319        svg_div = self._wait.until(
    320            EC.visibility_of_element_located((By.ID, "__svg__random___1__object"))
    321        )
    322        self._wait.until(lambda d: svg_div.is_displayed() is True)
    323 
    324        self.assert_rendering(exp, svg_div)
    325 
    326        return True
    327 
    328    def pdf_select_zoom(self, value):
    329        pdf_zoom = self._wait.until(
    330            EC.visibility_of_element_located((By.ID, "scaleSelect"))
    331        )
    332        self._wait.until(lambda d: len(pdf_zoom.text) > 0)
    333 
    334        pdf_zoom_drop = Select(pdf_zoom)
    335        pdf_zoom_drop.select_by_value(value)
    336 
    337    def pdf_wait_div(self):
    338        pdf_div = self._wait.until(EC.visibility_of_element_located((By.ID, "viewer")))
    339        self._wait.until(lambda d: pdf_div.is_displayed() is True)
    340        return pdf_div
    341 
    342    def pdf_get_page(self, page, long=False):
    343        waiter = self._longwait if long is True else self._wait
    344        page = waiter.until(
    345            EC.visibility_of_element_located((
    346                By.CSS_SELECTOR,
    347                f"div.page[data-page-number='{page}'] canvas",
    348            ))
    349        )
    350 
    351        self._wait.until(
    352            lambda d: d.execute_script(
    353                'return window.getComputedStyle(document.querySelector(".loadingInput.start"), "::after").getPropertyValue("visibility");'
    354            )
    355            != "visible"
    356        )
    357        # PDF.js can take time to settle and we don't have a nice way to wait
    358        # for an event on it
    359        time.sleep(1)
    360 
    361        # Rendering can be slower on debug build so give more time to settle
    362        if self.is_debug_build():
    363            time.sleep(3)
    364 
    365        return page
    366 
    367    def pdf_go_to_page(self, page):
    368        pagenum = self._wait.until(
    369            EC.visibility_of_element_located((By.ID, "pageNumber"))
    370        )
    371        pagenum.send_keys(Keys.BACKSPACE)
    372        pagenum.send_keys(f"{page}")
    373 
    374    def test_pdf_navigation(self, exp):
    375        """
    376        C3927
    377        """
    378 
    379        self.open_tab("http://www.pdf995.com/samples/pdf.pdf")
    380 
    381        # Test basic rendering
    382        self.pdf_wait_div()
    383        self.pdf_select_zoom("1")
    384        self.pdf_get_page(1)
    385        self.assert_rendering(exp["base"], self._driver)
    386 
    387        # Navigating to page X, we know the PDF has 5 pages.
    388        rand_page = random.randint(1, 5)
    389        self.pdf_go_to_page(rand_page)
    390        # the click step ensures we change page
    391        self.pdf_wait_div().click()
    392        # getting page X will wait on is_displayed() so if page X is not visible
    393        # this will timeout
    394        self.pdf_get_page(rand_page)
    395 
    396        # press down/up/right/left/PageDown/PageUp/End/Home
    397        key_presses = [
    398            (Keys.DOWN, "down"),
    399            (Keys.UP, "up"),
    400            (Keys.RIGHT, "right"),
    401            (Keys.LEFT, "left"),
    402            (Keys.PAGE_DOWN, "pagedown"),
    403            (Keys.PAGE_UP, "pageup"),
    404            (Keys.END, "end"),
    405            (Keys.HOME, "home"),
    406        ]
    407 
    408        for key, ref in key_presses:
    409            # reset to page 2
    410            self.pdf_go_to_page(2)
    411            pdfjs = self._wait.until(
    412                EC.visibility_of_element_located((By.CSS_SELECTOR, "html"))
    413            )
    414            pdfjs.send_keys(key)
    415            self.pdf_get_page(2)
    416            # give some time for rendering to update
    417            time.sleep(0.2)
    418            self._logger.info(f"assert {ref}")
    419            self.assert_rendering(exp[ref], self._driver)
    420 
    421        # click Next/Previous page
    422        self.pdf_go_to_page(1)
    423        button_next = self._wait.until(
    424            EC.visibility_of_element_located((By.ID, "next"))
    425        )
    426        button_next.click()
    427        button_next.click()
    428        self._logger.info("assert next twice 1 => 3")
    429        self.assert_rendering(exp["next"], self._driver)
    430 
    431        button_previous = self._wait.until(
    432            EC.visibility_of_element_located((By.ID, "previous"))
    433        )
    434        button_previous.click()
    435        self._logger.info("assert previous 3 => 2")
    436        self.assert_rendering(exp["previous"], self._driver)
    437 
    438        secondary_menu = self._wait.until(
    439            EC.visibility_of_element_located((By.ID, "secondaryToolbarToggle"))
    440        )
    441 
    442        # Use tools button
    443        #  - first/lage page
    444        #  - rotate left/right
    445        #  - doc properties
    446        menu_buttons = [
    447            "firstPage",
    448            "lastPage",
    449            "pageRotateCw",
    450            "pageRotateCcw",
    451            "documentProperties",
    452        ]
    453 
    454        for menu_id in menu_buttons:
    455            self._logger.info(f"reset to page for {menu_id}")
    456            if menu_id != "firstPage":
    457                self.pdf_go_to_page(1)
    458            else:
    459                self.pdf_go_to_page(2)
    460            time.sleep(0.2)
    461 
    462            self._logger.info(f"click menu for {menu_id}")
    463            # open menu
    464            secondary_menu.click()
    465 
    466            self._logger.info(f"find button for {menu_id}")
    467            button_to_test = self._wait.until(
    468                EC.visibility_of_element_located((By.ID, menu_id))
    469            )
    470 
    471            self._logger.info(f"click button for {menu_id}")
    472            button_to_test.click()
    473 
    474            try:
    475                self._wait.until(
    476                    EC.invisibility_of_element_located((By.ID, "secondaryToolbar"))
    477                )
    478            except TimeoutException:
    479                # Menu does not close itself on those??
    480                if menu_id in ("pageRotateCw", "pageRotateCcw"):
    481                    self._logger.info(f"force close menu for {menu_id}")
    482                    secondary_menu.click()
    483                    self._logger.info(f"wait menu disappear for {menu_id}")
    484                    self._wait.until(
    485                        EC.invisibility_of_element_located((By.ID, "secondaryToolbar"))
    486                    )
    487 
    488            self._logger.info(f"assert {menu_id}")
    489            self.assert_rendering(exp[menu_id], self._driver)
    490 
    491            if menu_id == "documentProperties":
    492                close = self._wait.until(
    493                    EC.visibility_of_element_located((By.ID, "documentPropertiesClose"))
    494                )
    495                close.click()
    496 
    497        self.pdf_go_to_page(1)
    498 
    499        #  - select text
    500        secondary_menu.click()
    501        text_selection = self._wait.until(
    502            EC.visibility_of_element_located((By.ID, "cursorSelectTool"))
    503        )
    504        text_selection.click()
    505 
    506        action = ActionChains(self._driver)
    507        paragraph = self._wait.until(
    508            EC.visibility_of_element_located((
    509                By.CSS_SELECTOR,
    510                "span[role=presentation]",
    511            ))
    512        )
    513        action.drag_and_drop_by_offset(paragraph, 50, 10).perform()
    514        time.sleep(0.75)
    515        try:
    516            ref_screen_source = "select_text_with_highlight"
    517            self._wait.until(
    518                EC.visibility_of_element_located((
    519                    By.CSS_SELECTOR,
    520                    "button.highlightButton",
    521                ))
    522            )
    523        except TimeoutException:
    524            ref_screen_source = "select_text_without_highlight"
    525            self._logger.info(
    526                "Wait for pdf highlight button: timed out, maybe it is not there"
    527            )
    528        finally:
    529            time.sleep(0.75)
    530            self.assert_rendering(exp[ref_screen_source], self._driver)
    531 
    532        # release select selection
    533        action.move_by_offset(0, 150).perform()
    534        action.click()
    535        # make sure we go back to page 1
    536        self.pdf_go_to_page(1)
    537 
    538        #  - hand tool
    539        secondary_menu.click()
    540        hand_tool = self._wait.until(
    541            EC.visibility_of_element_located((By.ID, "cursorHandTool"))
    542        )
    543        hand_tool.click()
    544        action.drag_and_drop_by_offset(paragraph, 0, -200).perform()
    545        self.assert_rendering(exp["hand_tool"], self._driver)
    546 
    547        return True
    548 
    549    def test_pdf_zoom(self, exp):
    550        """
    551        C3929
    552        """
    553 
    554        self.open_tab("http://www.pdf995.com/samples/pdf.pdf")
    555 
    556        self.pdf_wait_div()
    557 
    558        zoom_levels = [
    559            ("1", 1, "p1_100p"),
    560            ("0.5", 1, "p1_50p"),
    561            ("0.75", 1, "p1_75p"),
    562            ("1.5", 1, "p1_150p"),
    563            ("4", 1, "p1_400p"),
    564            ("page-actual", 1, "p1_actual"),
    565            ("page-fit", 1, "p1_fit"),
    566            ("page-width", 1, "p1_width"),
    567        ]
    568 
    569        for zoom, page, ref in zoom_levels:
    570            self.pdf_select_zoom(zoom)
    571            self.pdf_get_page(page, long=True)
    572            self._logger.info(f"assert {ref}")
    573            self.assert_rendering(exp[ref], self._driver)
    574 
    575        return True
    576 
    577    def test_pdf_download(self, exp):
    578        """
    579        C936503
    580        """
    581 
    582        self.open_tab(
    583            "https://file-examples.com/index.php/sample-documents-download/sample-pdf-download/"
    584        )
    585 
    586        try:
    587            consent = self._wait.until(
    588                EC.visibility_of_element_located((By.CSS_SELECTOR, ".fc-cta-consent"))
    589            )
    590            consent.click()
    591        except TimeoutException:
    592            self._logger.info("Wait for consent form: timed out, maybe it is not here")
    593 
    594        for iframe in self._driver.find_elements(By.CSS_SELECTOR, "iframe"):
    595            self._driver.execute_script("arguments[0].remove();", iframe)
    596 
    597        download_button = self._wait.until(
    598            EC.presence_of_element_located((By.CSS_SELECTOR, ".download-button"))
    599        )
    600        self._driver.execute_script("arguments[0].scrollIntoView();", download_button)
    601        self._driver.execute_script("this.window.scrollBy(0, -100);")
    602        self._wait.until(
    603            EC.visibility_of_element_located((By.CSS_SELECTOR, ".download-button"))
    604        )
    605        # clicking seems to break on CI because we nuke ads
    606        self._driver.get(download_button.get_property("href"))
    607 
    608        self.pdf_get_page(1)
    609 
    610        self.assert_rendering(exp, self._driver)
    611 
    612        return True
    613 
    614    def context_menu_copy(self, element, mime_type):
    615        action = ActionChains(self._driver)
    616 
    617        # Open context menu and copy
    618        action.context_click(element).perform()
    619        self._driver.set_context("chrome")
    620        context_menu = self._wait.until(
    621            EC.visibility_of_element_located((By.ID, "contentAreaContextMenu"))
    622        )
    623        copy = self._wait.until(
    624            EC.visibility_of_element_located((
    625                By.ID,
    626                (
    627                    "context-copyimage-contents"
    628                    if mime_type.startswith("image/")
    629                    else "context-copy"
    630                ),
    631            ))
    632        )
    633        copy.click()
    634        self.wait_for_element_in_clipboard(mime_type, False)
    635        context_menu.send_keys(Keys.ESCAPE)
    636 
    637        # go back to content context
    638        self._driver.set_context("content")
    639 
    640    def verify_clipboard(self, mime_type, should_be_present):
    641        self._driver.set_context("chrome")
    642        in_clipboard = self._driver.execute_script(
    643            "return Services.clipboard.hasDataMatchingFlavors([arguments[0]], Ci.nsIClipboard.kGlobalClipboard);",
    644            mime_type,
    645        )
    646        self._driver.set_context("content")
    647        assert in_clipboard == should_be_present, (
    648            f"type {mime_type} should/should ({should_be_present}) not be in clipboard"
    649        )
    650 
    651    def wait_for_element_in_clipboard(self, mime_type, context_change=False):
    652        if context_change:
    653            self._driver.set_context("chrome")
    654        self._wait.until(
    655            lambda d: self._driver.execute_script(
    656                "return Services.clipboard.hasDataMatchingFlavors([arguments[0]], Ci.nsIClipboard.kGlobalClipboard);",
    657                mime_type,
    658            )
    659            is True
    660        )
    661        if context_change:
    662            self._driver.set_context("content")
    663 
    664    def test_copy_paste_image_text(self, exp):
    665        """
    666        C464474
    667        """
    668 
    669        mystor = self.open_tab("https://mystor.github.io/dragndrop/#")
    670        images = self.open_tab("https://1stwebdesigner.com/image-file-types/")
    671 
    672        image = self._wait.until(
    673            EC.presence_of_element_located((By.CSS_SELECTOR, ".wp-image-42224"))
    674        )
    675        self._driver.execute_script("arguments[0].scrollIntoView();", image)
    676        self._wait.until(
    677            EC.visibility_of_element_located((By.CSS_SELECTOR, ".wp-image-42224"))
    678        )
    679        self.verify_clipboard("image/png", False)
    680        self.context_menu_copy(image, "image/png")
    681        self.verify_clipboard("image/png", True)
    682 
    683        self._driver.switch_to.window(mystor)
    684        link = self._wait.until(
    685            EC.visibility_of_element_located((
    686                By.CSS_SELECTOR,
    687                "#testlist > li:nth-child(11) > a:nth-child(1)",
    688            ))
    689        )
    690        link.click()
    691        drop_area = self._wait.until(
    692            EC.visibility_of_element_located((By.CSS_SELECTOR, "html"))
    693        )
    694        drop_area.click()
    695        drop_area.send_keys(Keys.CONTROL + "v")
    696        self.verify_clipboard("image/png", True)
    697 
    698        matching_text = self._wait.until(
    699            EC.visibility_of_element_located((By.ID, "matching"))
    700        )
    701        assert matching_text.text == "MATCHING", "copy/paste image should match"
    702 
    703        self._driver.switch_to.window(images)
    704        text = self._wait.until(
    705            EC.presence_of_element_located((
    706                By.CSS_SELECTOR,
    707                ".entry-content > p:nth-child(1)",
    708            ))
    709        )
    710        self._driver.execute_script("arguments[0].scrollIntoView();", text)
    711 
    712        action = ActionChains(self._driver)
    713        action.drag_and_drop_by_offset(text, 50, 10).perform()
    714 
    715        self.context_menu_copy(text, "text/plain")
    716 
    717        self._driver.switch_to.window(mystor)
    718        link = self._wait.until(
    719            EC.visibility_of_element_located((
    720                By.CSS_SELECTOR,
    721                "#testlist > li:nth-child(12) > a:nth-child(1)",
    722            ))
    723        )
    724        link.click()
    725        drop_area = self._wait.until(
    726            EC.visibility_of_element_located((By.CSS_SELECTOR, "html"))
    727        )
    728        drop_area.click()
    729        drop_area.send_keys(Keys.CONTROL + "v")
    730 
    731        matching_text = self._wait.until(
    732            EC.visibility_of_element_located((By.ID, "matching"))
    733        )
    734        assert matching_text.text == "MATCHING", "copy/paste html should match"
    735 
    736        return True
    737 
    738    def accept_download(self):
    739        # check the Firefox UI
    740        self._driver.set_context("chrome")
    741        download_button = self._wait.until(
    742            EC.visibility_of_element_located((By.ID, "downloads-button"))
    743        )
    744        download_button.click()
    745        time.sleep(1)
    746 
    747        download_item = self._wait.until(
    748            EC.visibility_of_element_located((
    749                By.CSS_SELECTOR,
    750                ".download-state .downloadTarget",
    751            ))
    752        )
    753        download_item.click()
    754        download_name = download_item.get_property("value")
    755 
    756        download_allow = self._wait.until(
    757            EC.presence_of_element_located((
    758                By.ID,
    759                "downloadsPanel-blockedSubview-unblockButton",
    760            ))
    761        )
    762        download_allow.click()
    763 
    764        # back to page
    765        self._driver.set_context("content")
    766 
    767        return download_name
    768 
    769    def wait_for_download(self):
    770        # check the Firefox UI
    771        self._driver.set_context("chrome")
    772        download_button = self._wait.until(
    773            EC.visibility_of_element_located((By.ID, "downloads-button"))
    774        )
    775        download_button.click()
    776        time.sleep(1)
    777 
    778        download_item = self._wait.until(
    779            EC.visibility_of_element_located((
    780                By.CSS_SELECTOR,
    781                ".download-state .downloadTarget",
    782            ))
    783        )
    784        download_name = download_item.get_property("value")
    785        self._logger.info(f"Waiting for download: {download_name}")
    786 
    787        download_progress = self._wait.until(
    788            EC.presence_of_element_located((
    789                By.CSS_SELECTOR,
    790                ".download-state .downloadProgress",
    791            ))
    792        )
    793        self._logger.info(
    794            "Download progress {}%".format(download_progress.get_property("value"))
    795        )
    796 
    797        try:
    798            self._wait.until(lambda d: download_progress.get_property("value") == 100)
    799        except TimeoutException as ex:
    800            details_normal = self._wait.until(
    801                EC.presence_of_element_located((
    802                    By.CSS_SELECTOR,
    803                    ".download-state .downloadDetailsNormal",
    804                ))
    805            )
    806            self._logger.info(
    807                "Download details normal {}".format(
    808                    details_normal.get_property("value")
    809                )
    810            )
    811            details_hover = self._wait.until(
    812                EC.presence_of_element_located((
    813                    By.CSS_SELECTOR,
    814                    ".download-state .downloadDetailsHover",
    815                ))
    816            )
    817            self._logger.info(
    818                "Download details hover {}".format(details_hover.get_property("value"))
    819            )
    820            raise ex
    821 
    822        # back to page
    823        self._driver.set_context("content")
    824        return download_name
    825 
    826    def change_download_folder(self, previous=None, new=None):
    827        self._logger.info(f"Download change folder: {previous} => {new}")
    828        self._driver.set_context("chrome")
    829        self._driver.execute_script(
    830            "Services.prefs.setIntPref('browser.download.folderList', 2);"
    831        )
    832        self._driver.execute_script(
    833            "Services.prefs.setCharPref('browser.download.dir', arguments[0]);", new
    834        )
    835        download_dir_pref = self._driver.execute_script(
    836            "return Services.prefs.getCharPref('browser.download.dir', null);"
    837        )
    838        self._driver.set_context("content")
    839        self._logger.info(f"Download folder pref: {download_dir_pref}")
    840        assert download_dir_pref == new, (
    841            "download directory from pref should match new directory"
    842        )
    843 
    844    def enable_downloads_debug(self):
    845        self._driver.set_context("chrome")
    846        self._logger.info("Setting downloads loglevel to Debug")
    847        self._driver.execute_script(
    848            "return Services.prefs.setStringPref('toolkit.download.loglevel', 'Debug');"
    849        )
    850        self._driver.set_context("content")
    851 
    852    def open_local(self):
    853        self.enable_downloads_debug()
    854        # Start local HTTP server with test file
    855        server_process, tmpdir = self._start_local_http_server(port=45678)
    856        # Store server info for cleanup
    857        self._http_server = server_process
    858        self._http_tmpdir = tmpdir
    859        # Open local test page
    860        download_site = self.open_tab("http://localhost:45678/")
    861        return download_site
    862 
    863    def get_local_1M(self):
    864        return self._wait.until(
    865            EC.presence_of_element_located((
    866                By.ID,
    867                "download-link",
    868            ))
    869        )
    870 
    871    def test_download_folder_change(self, exp):
    872        """
    873        C1756713
    874        """
    875 
    876        download_site = self.open_local()
    877        extra_small = self.get_local_1M()
    878        self._driver.execute_script("arguments[0].click();", extra_small)
    879 
    880        download_name = self.accept_download()
    881        self.wait_for_download()
    882 
    883        self.open_tab("about:preferences")
    884        download_folder = self._wait.until(
    885            EC.presence_of_element_located((By.ID, "chooseFolder"))
    886        )
    887        if not download_folder.get_property("value"):
    888            # Fallback to "downloadFoler" for older Firefox versions
    889            download_folder = self._wait.until(
    890                EC.presence_of_element_located((By.ID, "downloadFolder"))
    891            )
    892        previous_folder = (
    893            download_folder.get_property("value")
    894            .replace("\u2066", "")
    895            .replace("\u2069", "")
    896        )
    897        self._logger.info(f"Download folder from about:preferences: {previous_folder}")
    898        if not os.path.isabs(previous_folder):
    899            previous_folder = os.path.join(os.environ.get("HOME", ""), previous_folder)
    900        with tempfile.TemporaryDirectory(
    901            dir=os.environ.get("HOME"), prefix="snap-test-download"
    902        ) as tmpdir:
    903            assert os.path.isdir(tmpdir), "tmpdir download should exists"
    904 
    905            download_1 = os.path.abspath(os.path.join(previous_folder, download_name))
    906            self._logger.info(f"Download 1 assert: {download_1}")
    907            assert os.path.isfile(download_1), "downloaded file #1 should exists"
    908 
    909            self.change_download_folder(previous_folder, tmpdir)
    910 
    911            self._driver.switch_to.window(download_site)
    912            self._driver.execute_script("arguments[0].click();", extra_small)
    913            self.accept_download()
    914            download_name2 = self.wait_for_download()
    915            download_2 = os.path.join(tmpdir, download_name2)
    916 
    917            self._logger.info(f"Download 2 assert: {download_2}")
    918            assert os.path.isfile(download_2), "downloaded file #2 should exists"
    919 
    920        # Cleanup local HTTP server
    921        self._stop_local_http_server(self._http_server, self._http_tmpdir)
    922        self._http_server = None
    923        self._http_tmpdir = None
    924 
    925        return True
    926 
    927    def test_download_folder_removal(self, exp):
    928        """
    929        C1756715
    930        """
    931 
    932        download_site = self.open_local()
    933        extra_small = self.get_local_1M()
    934 
    935        with tempfile.TemporaryDirectory(
    936            dir=os.environ.get("HOME"), prefix="snap-test-download-rm"
    937        ) as tmpdir:
    938            self.change_download_folder(None, tmpdir)
    939 
    940            self._driver.switch_to.window(download_site)
    941            self._driver.execute_script("arguments[0].click();", extra_small)
    942 
    943            self.accept_download()
    944            download_name = self.wait_for_download()
    945            download_file = os.path.join(tmpdir, download_name)
    946            self._logger.info(f"Download assert: {download_file}")
    947            assert os.path.isdir(tmpdir), "tmpdir download should exists"
    948            assert os.path.isfile(download_file), "downloaded file should exists"
    949 
    950            self._driver.set_context("chrome")
    951            download_button = self._wait.until(
    952                EC.visibility_of_element_located((By.ID, "downloads-button"))
    953            )
    954            download_button.click()
    955            time.sleep(1)
    956 
    957            download_details = self._wait.until(
    958                EC.visibility_of_element_located((
    959                    By.CSS_SELECTOR,
    960                    ".download-state .downloadDetailsNormal",
    961                ))
    962            )
    963            assert download_details.get_property("value").startswith("Completed"), (
    964                "download should be marked as completed"
    965            )
    966 
    967        # TemporaryDirectory out of focus so folder removed
    968 
    969        # Close panel we will re-open it
    970        self._driver.execute_script("this.window.DownloadsButton.hide();")
    971        self._wait.until(
    972            EC.invisibility_of_element_located((
    973                By.CSS_SELECTOR,
    974                ".download-state .downloadDetailsNormal",
    975            ))
    976        )
    977 
    978        assert os.path.isdir(tmpdir) is False, "tmpdir should have been removed"
    979        assert os.path.isfile(download_file) is False, (
    980            "downloaded file should have been removed"
    981        )
    982 
    983        download_button.click()
    984        time.sleep(1)
    985 
    986        download_item = self._wait.until(
    987            EC.visibility_of_element_located((
    988                By.CSS_SELECTOR,
    989                ".download-state .downloadTarget",
    990            ))
    991        )
    992        download_name_2 = download_item.get_property("value")
    993        assert download_name == download_name_2, "downloaded names should match"
    994 
    995        download_details = self._wait.until(
    996            EC.visibility_of_element_located((
    997                By.CSS_SELECTOR,
    998                ".download-state .downloadDetailsNormal",
    999            ))
   1000        )
   1001        assert download_details.get_property("value").startswith(
   1002            "File moved or missing"
   1003        ), "download panel should report file moved/missing"
   1004 
   1005        self._driver.execute_script("this.window.DownloadsButton.hide();")
   1006 
   1007        self._driver.set_context("content")
   1008 
   1009        # Cleanup local HTTP server
   1010        self._stop_local_http_server(self._http_server, self._http_tmpdir)
   1011        self._http_server = None
   1012        self._http_tmpdir = None
   1013 
   1014        return True
   1015 
   1016 
   1017 if __name__ == "__main__":
   1018    QATests()