--- a/testing/marionette/client/marionette_driver/marionette.py
+++ b/testing/marionette/client/marionette_driver/marionette.py
@@ -1120,41 +1120,67 @@ class Marionette(object):
self.delete_session()
self.instance.restart(prefs)
self.raise_for_port()
self.start_session()
# Restore the context as used before the restart
self.set_context(context)
- def _request_in_app_shutdown(self, shutdown_flags=None):
- """Terminate the currently running instance from inside the application.
+ def _request_in_app_shutdown(self, *shutdown_flags):
+ """Attempt to quit the currently running instance from inside the
+ application.
+
+ Duplicate entries in `shutdown_flags` are removed, and
+ `"eAttemptQuit"` is added if no other `*Quit` flags are given.
+ This provides backwards compatible behaviour with earlier
+ Firefoxen.
- :param shutdown_flags: If specified use additional flags for the shutdown
- of the application. Possible values here correspond
- to constants in nsIAppStartup: http://mzl.la/1X0JZsC.
+ This method effectively calls `Services.startup.quit` in Gecko.
+ Possible flag values are listed at http://mzl.la/1X0JZsC.
+
+ :param shutdown_flags: Optional additional quit masks to include.
+ Duplicates are removed, and `"eAttemptQuit"` is added if no
+ flags ending with `"Quit"` are present.
+
+ :throws InvalidArgumentException: If there are multiple
+ `shutdown_flags` ending with `"Quit"`.
+
"""
- flags = set([])
- if shutdown_flags:
- flags.add(shutdown_flags)
+
+ # The vast majority of this function was implemented inside
+ # the quitApplication command as part of bug 1337743, and can be
+ # removed from here in Firefox 55 at the earliest.
+
+ # remove duplicates
+ flags = set(shutdown_flags)
- # Trigger a 'quit-application-requested' observer notification so that
- # components can safely shutdown before quitting the application.
+ # add eAttemptQuit if there are no *Quits
+ if not any(flag.endswith("Quit") for flag in flags):
+ flags = flags | set(("eAttemptQuit",))
+
+ # Trigger a quit-application-requested observer notification
+ # so that components can safely shutdown before quitting the
+ # application.
with self.using_context("chrome"):
canceled = self.execute_script("""
Components.utils.import("resource://gre/modules/Services.jsm");
- let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"].
- createInstance(Components.interfaces.nsISupportsPRBool);
+ let cancelQuit = Components.classes["@mozilla.org/supports-PRBool;1"]
+ .createInstance(Components.interfaces.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, "quit-application-requested", null);
return cancelQuit.data;
""")
if canceled:
- raise errors.MarionetteException("Something canceled the quit application request")
+ raise errors.MarionetteException(
+ "Something cancelled the quit application request")
- self._send_message("quitApplication", {"flags": list(flags)})
+ body = None
+ if len(flags) > 0:
+ body = {"flags": list(flags)}
+ self._send_message("quitApplication", body)
@do_process_check
def quit(self, in_app=False, callback=None):
"""Terminate the currently running instance.
This command will delete the active marionette session. It also allows
manipulation of eg. the profile data while the application is not running.
To start the application again, start_session() has to be called.
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -2705,32 +2705,78 @@ GeckoDriver.prototype._checkIfAlertIsPre
* True if the server should accept new socket connections.
*/
GeckoDriver.prototype.acceptConnections = function (cmd, resp) {
assert.boolean(cmd.parameters.value);
this._server.acceptConnections = cmd.parameters.value;
}
/**
- * Quits Firefox with the provided flags and tears down the current
- * session.
+ * Quits the application with the provided flags.
+ *
+ * Marionette will stop accepting new connections before ending the
+ * current session, and finally attempting to quit the application.
+ *
+ * Optional {@code nsIAppStartup} flags may be provided as
+ * an array of masks, and these will be combined by ORing
+ * them with a bitmask. The available masks are defined in
+ * https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIAppStartup.
+ *
+ * Crucially, only one of the *Quit flags can be specified. The |eRestart|
+ * flag may be bit-wise combined with one of the *Quit flags to cause
+ * the application to restart after it quits.
+ *
+ * @param {Array.<string>=} flags
+ * Constant name of masks to pass to |Services.startup.quit|.
+ * If empty or undefined, |nsIAppStartup.eAttemptQuit| is used.
+ *
+ * @throws {InvalidArgumentError}
+ * If |flags| contains unknown or incompatible flags, for example
+ * multiple Quit flags.
*/
-GeckoDriver.prototype.quitApplication = function (cmd, resp) {
- assert.firefox("Bug 1298921 - In app initiated quit not yet available beside Firefox")
-
- let flags = Ci.nsIAppStartup.eAttemptQuit;
- for (let k of cmd.parameters.flags || []) {
- flags |= Ci.nsIAppStartup[k];
+GeckoDriver.prototype.quitApplication = function* (cmd, resp) {
+ const quits = ["eConsiderQuit", "eAttemptQuit", "eForceQuit"];
+
+ let flags = [];
+ if (typeof cmd.parameters.flags != "undefined") {
+ flags = assert.array(cmd.parameters.flags);
+ }
+
+ // bug 1298921
+ assert.firefox()
+
+ let quitSeen;
+ let mode = 0;
+ if (flags.length > 0) {
+ for (let k of flags) {
+ assert.in(k, Ci.nsIAppStartup);
+
+ if (quits.includes(k)) {
+ if (quitSeen) {
+ throw new InvalidArgumentError(
+ `${k} cannot be combined with ${quitSeen}`);
+ }
+ quitSeen = k;
+ }
+
+ mode |= Ci.nsIAppStartup[k];
+ }
+ } else {
+ mode = Ci.nsIAppStartup.eAttemptQuit;
}
this._server.acceptConnections = false;
- resp.send();
-
this.deleteSession();
- Services.startup.quit(flags);
+
+ // delay response until the application is about to quit
+ let quitApplication = new Promise(resolve =>
+ Services.obs.addObserver(resolve, "quit-application", false));
+
+ Services.startup.quit(mode);
+ yield quitApplication.then(() => resp.send());
};
GeckoDriver.prototype.installAddon = function (cmd, resp) {
assert.firefox()
let path = cmd.parameters.path;
let temp = cmd.parameters.temporary || false;
if (typeof path == "undefined" || typeof path != "string" ||
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_quit_restart.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_quit_restart.py
@@ -1,157 +1,217 @@
# 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/.
-from marionette_driver.errors import MarionetteException
+from marionette_driver import errors
from marionette_harness import MarionetteTestCase
+class TestServerQuitApplication(MarionetteTestCase):
+
+ def tearDown(self):
+ if self.marionette.session is None:
+ self.marionette.start_session()
+
+ def quit(self, flags=None):
+ body = None
+ if flags is not None:
+ body = {"flags": list(flags)}
+
+ try:
+ self.marionette._send_message("quitApplication", body)
+ finally:
+ self.marionette.session_id = None
+ self.marionette.session = None
+ self.marionette.process_id = None
+ self.marionette.profile = None
+ self.marionette.window = None
+
+ self.marionette.client.close()
+ self.marionette.instance.runner.wait()
+
+ def test_types(self):
+ for typ in [42, True, "foo", []]:
+ print("testing type {}".format(type(typ)))
+ with self.assertRaises(errors.InvalidArgumentException):
+ self.marionette._send_message("quitApplication", typ)
+
+ with self.assertRaises(errors.InvalidArgumentException):
+ self.quit("foo")
+
+ def test_undefined_default(self):
+ self.quit()
+
+ def test_empty_default(self):
+ self.quit(())
+
+ def test_incompatible_flags(self):
+ with self.assertRaises(errors.InvalidArgumentException):
+ self.quit(("eAttemptQuit", "eForceQuit"))
+
+ def test_attempt_quit(self):
+ self.quit(("eAttemptQuit",))
+
+ def test_force_quit(self):
+ self.quit(("eForceQuit",))
+
+
class TestQuitRestart(MarionetteTestCase):
def setUp(self):
MarionetteTestCase.setUp(self)
self.pid = self.marionette.process_id
self.session_id = self.marionette.session_id
- self.assertNotEqual(self.marionette.get_pref("browser.startup.page"), 3)
+ self.assertNotEqual(
+ self.marionette.get_pref("browser.startup.page"), 3)
self.marionette.set_pref("browser.startup.page", 3)
def tearDown(self):
# Ensure to restart a session if none exist for clean-up
- if not self.marionette.session:
+ if self.marionette.session is None:
self.marionette.start_session()
self.marionette.clear_pref("browser.startup.page")
MarionetteTestCase.tearDown(self)
def test_force_restart(self):
self.marionette.restart()
self.assertEqual(self.marionette.session_id, self.session_id)
# A forced restart will cause a new process id
self.assertNotEqual(self.marionette.process_id, self.pid)
# If a preference value is not forced, a restart will cause a reset
- self.assertNotEqual(self.marionette.get_pref("browser.startup.page"), 3)
+ self.assertNotEqual(
+ self.marionette.get_pref("browser.startup.page"), 3)
def test_force_quit(self):
self.marionette.quit()
self.assertEqual(self.marionette.session, None)
- with self.assertRaisesRegexp(MarionetteException, "Please start a session"):
+ with self.assertRaisesRegexp(errors.MarionetteException, "Please start a session"):
self.marionette.get_url()
self.marionette.start_session()
self.assertNotEqual(self.marionette.session_id, self.session_id)
- self.assertNotEqual(self.marionette.get_pref("browser.startup.page"), 3)
+ self.assertNotEqual(
+ self.marionette.get_pref("browser.startup.page"), 3)
def test_in_app_clean_restart(self):
with self.assertRaises(ValueError):
self.marionette.restart(in_app=True, clean=True)
def test_in_app_restart(self):
self.marionette.restart(in_app=True)
self.assertEqual(self.marionette.session_id, self.session_id)
# An in-app restart will keep the same process id only on Linux
if self.marionette.session_capabilities["platformName"] == "linux":
self.assertEqual(self.marionette.process_id, self.pid)
else:
self.assertNotEqual(self.marionette.process_id, self.pid)
# If a preference value is not forced, a restart will cause a reset
- self.assertNotEqual(self.marionette.get_pref("browser.startup.page"), 3)
+ self.assertNotEqual(
+ self.marionette.get_pref("browser.startup.page"), 3)
def test_in_app_restart_with_callback(self):
self.marionette.restart(in_app=True,
callback=lambda: self.shutdown(restart=True))
self.assertEqual(self.marionette.session_id, self.session_id)
# An in-app restart will keep the same process id only on Linux
if self.marionette.session_capabilities["platformName"] == "linux":
self.assertEqual(self.marionette.process_id, self.pid)
else:
self.assertNotEqual(self.marionette.process_id, self.pid)
# If a preference value is not forced, a restart will cause a reset
- self.assertNotEqual(self.marionette.get_pref("browser.startup.page"), 3)
+ self.assertNotEqual(
+ self.marionette.get_pref("browser.startup.page"), 3)
def test_in_app_quit(self):
self.marionette.quit(in_app=True)
self.assertEqual(self.marionette.session, None)
- with self.assertRaisesRegexp(MarionetteException, "Please start a session"):
+ with self.assertRaisesRegexp(errors.MarionetteException, "Please start a session"):
self.marionette.get_url()
self.marionette.start_session()
self.assertNotEqual(self.marionette.session_id, self.session_id)
- self.assertNotEqual(self.marionette.get_pref("browser.startup.page"), 3)
+ self.assertNotEqual(
+ self.marionette.get_pref("browser.startup.page"), 3)
def test_in_app_quit_with_callback(self):
self.marionette.quit(in_app=True, callback=self.shutdown)
self.assertEqual(self.marionette.session, None)
- with self.assertRaisesRegexp(MarionetteException, "Please start a session"):
+ with self.assertRaisesRegexp(errors.MarionetteException, "Please start a session"):
self.marionette.get_url()
self.marionette.start_session()
self.assertNotEqual(self.marionette.session_id, self.session_id)
- self.assertNotEqual(self.marionette.get_pref("browser.startup.page"), 3)
+ self.assertNotEqual(
+ self.marionette.get_pref("browser.startup.page"), 3)
def test_reset_context_after_quit_by_set_context(self):
- # Check that we are in content context which is used by default in Marionette
- self.assertNotIn('chrome://', self.marionette.get_url(),
- "Context doesn't default to content")
+ # Check that we are in content context which is used by default in
+ # Marionette
+ self.assertNotIn("chrome://", self.marionette.get_url(),
+ "Context does not default to content")
- self.marionette.set_context('chrome')
+ self.marionette.set_context("chrome")
self.marionette.quit(in_app=True)
self.assertEqual(self.marionette.session, None)
self.marionette.start_session()
- self.assertNotIn('chrome://', self.marionette.get_url(),
+ self.assertNotIn("chrome://", self.marionette.get_url(),
"Not in content context after quit with using_context")
def test_reset_context_after_quit_by_using_context(self):
- # Check that we are in content context which is used by default in Marionette
- self.assertNotIn('chrome://', self.marionette.get_url(),
- "Context doesn't default to content")
+ # Check that we are in content context which is used by default in
+ # Marionette
+ self.assertNotIn("chrome://", self.marionette.get_url(),
+ "Context does not default to content")
- with self.marionette.using_context('chrome'):
+ with self.marionette.using_context("chrome"):
self.marionette.quit(in_app=True)
self.assertEqual(self.marionette.session, None)
self.marionette.start_session()
- self.assertNotIn('chrome://', self.marionette.get_url(),
+ self.assertNotIn("chrome://", self.marionette.get_url(),
"Not in content context after quit with using_context")
def test_keep_context_after_restart_by_set_context(self):
- # Check that we are in content context which is used by default in Marionette
- self.assertNotIn('chrome://', self.marionette.get_url(),
+ # Check that we are in content context which is used by default in
+ # Marionette
+ self.assertNotIn("chrome://", self.marionette.get_url(),
"Context doesn't default to content")
# restart while we are in chrome context
- self.marionette.set_context('chrome')
+ self.marionette.set_context("chrome")
self.marionette.restart(in_app=True)
# An in-app restart will keep the same process id only on Linux
if self.marionette.session_capabilities["platformName"] == "linux":
self.assertEqual(self.marionette.process_id, self.pid)
else:
self.assertNotEqual(self.marionette.process_id, self.pid)
- self.assertIn('chrome://', self.marionette.get_url(),
+ self.assertIn("chrome://", self.marionette.get_url(),
"Not in chrome context after a restart with set_context")
def test_keep_context_after_restart_by_using_context(self):
- # Check that we are in content context which is used by default in Marionette
- self.assertNotIn('chrome://', self.marionette.get_url(),
- "Context doesn't default to content")
+ # Check that we are in content context which is used by default in
+ # Marionette
+ self.assertNotIn("chrome://", self.marionette.get_url(),
+ "Context does not default to content")
# restart while we are in chrome context
with self.marionette.using_context('chrome'):
self.marionette.restart(in_app=True)
# An in-app restart will keep the same process id only on Linux
if self.marionette.session_capabilities["platformName"] == "linux":
self.assertEqual(self.marionette.process_id, self.pid)
@@ -160,14 +220,14 @@ class TestQuitRestart(MarionetteTestCase
self.assertIn("chrome://", self.marionette.get_url(),
"Not in chrome context after a restart with using_context")
def shutdown(self, restart=False):
self.marionette.set_context("chrome")
self.marionette.execute_script("""
Components.utils.import("resource://gre/modules/Services.jsm");
- let flags = Ci.nsIAppStartup.eAttemptQuit
- if(arguments[0]) {
+ let flags = Ci.nsIAppStartup.eAttemptQuit;
+ if (arguments[0]) {
flags |= Ci.nsIAppStartup.eRestart;
}
Services.startup.quit(flags);
- """, script_args=[restart])
+ """, script_args=(restart,))