summaryrefslogtreecommitdiffstats
path: root/browser/components/tests/marionette
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--browser/components/tests/marionette/manifest.ini5
-rw-r--r--browser/components/tests/marionette/test_no_errors_clean_profile.py186
2 files changed, 191 insertions, 0 deletions
diff --git a/browser/components/tests/marionette/manifest.ini b/browser/components/tests/marionette/manifest.ini
new file mode 100644
index 0000000000..b772c1e6c2
--- /dev/null
+++ b/browser/components/tests/marionette/manifest.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+tags = local
+
+[test_no_errors_clean_profile.py]
+
diff --git a/browser/components/tests/marionette/test_no_errors_clean_profile.py b/browser/components/tests/marionette/test_no_errors_clean_profile.py
new file mode 100644
index 0000000000..efa75fcb47
--- /dev/null
+++ b/browser/components/tests/marionette/test_no_errors_clean_profile.py
@@ -0,0 +1,186 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import time
+from unittest.util import safe_repr
+
+from marionette_driver.by import By
+from marionette_driver.keys import Keys
+from marionette_harness import MarionetteTestCase
+
+# This list shouldn't exist!
+# DO NOT ADD NEW EXCEPTIONS HERE! (unless they are specifically caused by
+# being run under marionette rather than in a "real" profile, or only occur
+# for browser developers)
+# The only reason this exists is that when this test was written we already
+# created a bunch of errors on startup, and it wasn't feasible to fix all
+# of them before landing the test.
+known_errors = [
+ {
+ # Disabling Shield because app.normandy.api_url is not set.
+ # (Marionette-only error, bug 1826314)
+ "message": "app.normandy.api_url is not set",
+ },
+ {
+ # From Remote settings, because it's intercepted by our test
+ # infrastructure which serves text/plain rather than JSON.
+ # Even if we fixed that we'd probably see a different error,
+ # unless we mock a full-blown remote settings server in the
+ # test infra, which doesn't seem worth it.
+ # Either way this wouldn't happen on "real" profiles.
+ "message": 'Error: Unexpected content-type "text/plain',
+ "filename": "RemoteSettingsClient",
+ },
+ {
+ # Triggered as soon as anything tries to use shortcut keys.
+ # The browser toolbox shortcut is not portable.
+ "message": "key_browserToolbox",
+ },
+ {
+ # Triggered as soon as anything tries to use shortcut keys.
+ # The developer-only restart shortcut is not portable.
+ "message": "key_quickRestart",
+ },
+ {
+ # Triggered as soon as anything tries to use shortcut keys.
+ # The reader mode shortcut is not portable on Linux.
+ # Bug 1825431 to fix this.
+ "message": "key_toggleReaderMode",
+ },
+ {
+ # Triggered on Linux because it doesn't implement the
+ # secondsSinceLastOSRestart property at all.
+ "message": "(NS_ERROR_NOT_IMPLEMENTED) [nsIAppStartup.secondsSinceLastOSRestart]",
+ "filename": "BrowserGlue",
+ },
+]
+
+# Same rules apply here - please don't add anything! - but headless runs
+# produce more errors that aren't normal in regular runs, so we've separated
+# them out.
+headless_errors = [{"message": "TelemetryEnvironment::_isDefaultBrowser"}]
+
+
+class TestNoErrorsNewProfile(MarionetteTestCase):
+ def setUp(self):
+ super(MarionetteTestCase, self).setUp()
+
+ self.maxDiff = None
+ self.marionette.set_context("chrome")
+
+ # Create a fresh profile.
+ self.marionette.restart(in_app=False, clean=True)
+
+ def ensure_proper_startup(self):
+ # First wait for the browser to settle:
+ self.marionette.execute_async_script(
+ """
+ let resolve = arguments[0];
+ let { BrowserInitState } = ChromeUtils.importESModule("resource:///modules/BrowserGlue.sys.mjs");
+ let promises = [
+ BrowserInitState.startupIdleTaskPromise,
+ gBrowserInit.idleTasksFinishedPromise,
+ ];
+ Promise.all(promises).then(resolve);
+ """
+ )
+
+ if self.marionette.session_capabilities["platformName"] == "mac":
+ self.mod_key = Keys.META
+ else:
+ self.mod_key = Keys.CONTROL
+ # Focus the URL bar by keyboard
+ url_bar = self.marionette.find_element(By.ID, "urlbar-input")
+ url_bar.send_keys(self.mod_key, "l")
+ # and open a tab by mouse:
+ new_tab_button = self.marionette.find_element(By.ID, "new-tab-button")
+ new_tab_button.click()
+
+ # Wait a bit more for async tasks to complete...
+ time.sleep(5)
+
+ def get_all_errors(self):
+ return self.marionette.execute_async_script(
+ """
+ let resolve = arguments[0];
+ // Get all the messages from the console service,
+ // and then get all of the ones from the console API storage.
+ let msgs = Services.console.getMessageArray();
+
+ const ConsoleAPIStorage = Cc[
+ "@mozilla.org/consoleAPI-storage;1"
+ ].getService(Ci.nsIConsoleAPIStorage);
+ const getCircularReplacer = () => {
+ const seen = new WeakSet();
+ return (key, value) => {
+ if (typeof value === "object" && value !== null) {
+ if (seen.has(value)) {
+ return "<circular ref>";
+ }
+ seen.add(value);
+ }
+ return value;
+ };
+ };
+ // Take cyclical values out, add a simplified 'message' prop
+ // that matches how things work for the console service objects.
+ const consoleApiMessages = ConsoleAPIStorage.getEvents().map(ev => {
+ let rv;
+ try {
+ rv = structuredClone(ev);
+ } catch (ex) {
+ rv = JSON.parse(JSON.stringify(ev, getCircularReplacer()));
+ }
+ delete rv.wrappedJSObject;
+ rv.message = ev.arguments.join(", ");
+ return rv;
+ });
+ resolve(msgs.concat(consoleApiMessages));
+ """
+ )
+
+ def should_ignore_error(self, error):
+ if not "message" in error:
+ print("Unparsable error:")
+ print(safe_repr(error))
+ return False
+
+ error_filename = error.get("filename", "")
+ error_msg = error["message"]
+ headless = self.marionette.session_capabilities["moz:headless"]
+ all_known_errors = known_errors + (headless_errors if headless else [])
+
+ for known_error in all_known_errors:
+ known_filename = known_error.get("filename", "")
+ known_msg = known_error["message"]
+ if known_msg in error_msg and known_filename in error_filename:
+ print(
+ "Known error seen: %s (%s)"
+ % (error["message"], error.get("filename", "no filename"))
+ )
+ return True
+
+ return False
+
+ def short_error_display(self, errors):
+ rv = []
+ for error in errors:
+ rv += [
+ {
+ "message": error.get("message", "No message!?"),
+ "filename": error.get("filename", "No filename!?"),
+ }
+ ]
+ return rv
+
+ def test_no_errors(self):
+ self.ensure_proper_startup()
+ errors = self.get_all_errors()
+ errors[:] = [error for error in errors if not self.should_ignore_error(error)]
+ if len(errors) > 0:
+ print("Unexpected errors encountered:")
+ # Hack to get nice printing:
+ for error in errors:
+ print(safe_repr(error))
+ self.assertEqual(self.short_error_display(errors), [])