tor-browser

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

test_no_errors_clean_profile.py (7594B)


      1 # This Source Code Form is subject to the terms of the Mozilla Public
      2 # License, v. 2.0. If a copy of the MPL was not distributed with this
      3 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
      4 
      5 import time
      6 from unittest.util import safe_repr
      7 
      8 from marionette_driver.by import By
      9 from marionette_driver.keys import Keys
     10 from marionette_harness import MarionetteTestCase
     11 
     12 # This list shouldn't exist!
     13 # DO NOT ADD NEW EXCEPTIONS HERE! (unless they are specifically caused by
     14 # being run under marionette rather than in a "real" profile, or only occur
     15 # for browser developers)
     16 # The only reason this exists is that when this test was written we already
     17 # created a bunch of errors on startup, and it wasn't feasible to fix all
     18 # of them before landing the test.
     19 known_errors = [
     20    {
     21        # Disabling Shield because app.normandy.api_url is not set.
     22        # (Marionette-only error, bug 1826314)
     23        "message": "app.normandy.api_url is not set",
     24    },
     25    {
     26        # From Remote settings, because it's intercepted by our test
     27        # infrastructure which serves text/plain rather than JSON.
     28        # Even if we fixed that we'd probably see a different error,
     29        # unless we mock a full-blown remote settings server in the
     30        # test infra, which doesn't seem worth it.
     31        # Either way this wouldn't happen on "real" profiles.
     32        "message": 'Error: Unexpected content-type "text/plain',
     33        "filename": "RemoteSettingsClient",
     34    },
     35    {
     36        # Triggered as soon as anything tries to use shortcut keys.
     37        # The browser toolbox shortcut is not portable.
     38        "message": "key_browserToolbox",
     39    },
     40    {
     41        # Triggered as soon as anything tries to use shortcut keys.
     42        # The developer-only restart shortcut is not portable.
     43        "message": "key_quickRestart",
     44    },
     45    {
     46        # Triggered as soon as anything tries to use shortcut keys.
     47        # The reader mode shortcut is not portable on Linux.
     48        # Bug 1825431 to fix this.
     49        "message": "key_toggleReaderMode",
     50    },
     51    {
     52        # Triggered as soon as anything tries to use shortcut keys.
     53        # Bug 1936426 to reconsider warning as we want ctrl-alt-x for chatbot.
     54        "message": "viewGenaiChatSidebarKb",
     55    },
     56    {
     57        # Triggered as soon as anything tries to use shortcut keys.
     58        # Bug 1936426 to reconsider warning as we want ctrl-z / ctrl-alt-z
     59        # for sidebar.
     60        "message": "toggleSidebarKb",
     61    },
     62    {
     63        # Triggered as soon as anything tries to access window.fullScreen.
     64        # Bug 1709294 to stop exposing window.fullScreen to the web content and
     65        # the warning can be removed.
     66        "message": 'JavaScript Warning: "Window.fullScreen attribute is deprecated and will be removed in the future."',
     67    },
     68 ]
     69 
     70 # Same rules apply here - please don't add anything! - but headless runs
     71 # produce more errors that aren't normal in regular runs, so we've separated
     72 # them out.
     73 headless_errors = [{"message": "TelemetryEnvironment::_isDefaultBrowser"}]
     74 
     75 
     76 class TestNoErrorsNewProfile(MarionetteTestCase):
     77    def setUp(self):
     78        super(MarionetteTestCase, self).setUp()
     79 
     80        self.maxDiff = None
     81        self.marionette.set_context("chrome")
     82 
     83        # Create a fresh profile.
     84        self.marionette.restart(in_app=False, clean=True)
     85 
     86    def ensure_proper_startup(self):
     87        # First wait for the browser to settle:
     88        self.marionette.execute_async_script(
     89            """
     90            let resolve = arguments[0];
     91            let { BrowserInitState } = ChromeUtils.importESModule("resource:///modules/BrowserGlue.sys.mjs");
     92            let promises = [
     93              BrowserInitState.startupIdleTaskPromise,
     94              gBrowserInit.idleTasksFinished.promise,
     95            ];
     96            Promise.all(promises).then(resolve);
     97            """
     98        )
     99 
    100        if self.marionette.session_capabilities["platformName"] == "mac":
    101            self.mod_key = Keys.META
    102        else:
    103            self.mod_key = Keys.CONTROL
    104        # Focus the URL bar by keyboard
    105        url_bar = self.marionette.execute_script("return gURLBar.inputField")
    106        url_bar.send_keys(self.mod_key, "l")
    107        # and open a tab by mouse:
    108        new_tab_button = self.marionette.find_element(By.ID, "new-tab-button")
    109        new_tab_button.click()
    110 
    111        # Wait a bit more for async tasks to complete...
    112        time.sleep(5)
    113 
    114    def get_all_errors(self):
    115        return self.marionette.execute_async_script(
    116            """
    117            let resolve = arguments[0];
    118            // Get all the messages from the console service,
    119            // and then get all of the ones from the console API storage.
    120            let msgs = Services.console.getMessageArray();
    121 
    122            const ConsoleAPIStorage = Cc[
    123              "@mozilla.org/consoleAPI-storage;1"
    124            ].getService(Ci.nsIConsoleAPIStorage);
    125            const getCircularReplacer = () => {
    126              const seen = new WeakSet();
    127              return (key, value) => {
    128                if (typeof value === "object" && value !== null) {
    129                  if (seen.has(value)) {
    130                    return "<circular ref>";
    131                  }
    132                  seen.add(value);
    133                }
    134                return value;
    135              };
    136            };
    137            // Take cyclical values out, add a simplified 'message' prop
    138            // that matches how things work for the console service objects.
    139            const consoleApiMessages = ConsoleAPIStorage.getEvents().map(ev => {
    140              let rv;
    141              try {
    142                rv = structuredClone(ev);
    143              } catch (ex) {
    144                rv = JSON.parse(JSON.stringify(ev, getCircularReplacer()));
    145              }
    146              delete rv.wrappedJSObject;
    147              rv.message = ev.arguments.join(", ");
    148              return rv;
    149            });
    150            resolve(msgs.concat(consoleApiMessages));
    151            """
    152        )
    153 
    154    def should_ignore_error(self, error):
    155        if not "message" in error:
    156            print("Unparsable error:")
    157            print(safe_repr(error))
    158            return False
    159 
    160        error_filename = error.get("filename", "")
    161        error_msg = error["message"]
    162        headless = self.marionette.session_capabilities["moz:headless"]
    163        all_known_errors = known_errors + (headless_errors if headless else [])
    164 
    165        for known_error in all_known_errors:
    166            known_filename = known_error.get("filename", "")
    167            known_msg = known_error["message"]
    168            if known_msg in error_msg and known_filename in error_filename:
    169                print(
    170                    "Known error seen: %s (%s)"
    171                    % (error["message"], error.get("filename", "no filename"))
    172                )
    173                return True
    174 
    175        return False
    176 
    177    def short_error_display(self, errors):
    178        rv = []
    179        for error in errors:
    180            rv += [
    181                {
    182                    "message": error.get("message", "No message!?"),
    183                    "filename": error.get("filename", "No filename!?"),
    184                }
    185            ]
    186        return rv
    187 
    188    def test_no_errors(self):
    189        self.ensure_proper_startup()
    190        errors = self.get_all_errors()
    191        errors[:] = [error for error in errors if not self.should_ignore_error(error)]
    192        if len(errors) > 0:
    193            print("Unexpected errors encountered:")
    194            # Hack to get nice printing:
    195            for error in errors:
    196                print(safe_repr(error))
    197        self.assertEqual(self.short_error_display(errors), [])