summaryrefslogtreecommitdiffstats
path: root/browser/components/tests/marionette/test_no_errors_clean_profile.py
blob: efa75fcb476e832ebb5d086c6304dd2f98b4b437 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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), [])