Bug 1408508 - Move Context to browser module. r?maja_zf draft
authorAndreas Tolfsen <ato@sny.no>
Mon, 16 Oct 2017 17:47:35 +0100
changeset 681467 272e0ad4673a17e92fd2837da9115289cb10de86
parent 681450 0d9c6250f99dc4b6aa1a94f5260737d046c52b1e
child 736163 cf66a327734808f9dbae27586984f8f676b8b501
push id84847
push userbmo:ato@sny.no
push dateTue, 17 Oct 2017 11:49:13 +0000
reviewersmaja_zf
bugs1408508
milestone58.0a1
Bug 1408508 - Move Context to browser module. r?maja_zf To avoid circular dependencies, where file A depends on B depending on A, we should strive towards fewer inter-dependencies and more autonomy between modules. For example, if testing/marionette/browser.js needs access to the Context enum it can currently only attain it by importing testing/marionette/driver.js. Because driver.js imports browser.js, we create a circular dependency and Cu.import enters an infinite import recursion. MozReview-Commit-ID: LGiA9sy9xrd
testing/marionette/browser.js
testing/marionette/driver.js
testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py
testing/marionette/test_browser.js
testing/marionette/unit.ini
--- a/testing/marionette/browser.js
+++ b/testing/marionette/browser.js
@@ -10,24 +10,62 @@ const {utils: Cu} = Components;
 const {WebElementEventTarget} = Cu.import("chrome://marionette/content/dom.js", {});
 Cu.import("chrome://marionette/content/element.js");
 const {
   NoSuchWindowError,
   UnsupportedOperationError,
 } = Cu.import("chrome://marionette/content/error.js", {});
 Cu.import("chrome://marionette/content/frame.js");
 
-this.EXPORTED_SYMBOLS = ["browser", "WindowState"];
+this.EXPORTED_SYMBOLS = ["browser", "Context", "WindowState"];
 
 /** @namespace */
 this.browser = {};
 
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 /**
+ * Variations of Marionette contexts.
+ *
+ * Choosing a context through the <tt>Marionette:SetContext</tt>
+ * command directs all subsequent browsing context scoped commands
+ * to that context.
+ *
+ * @enum
+ */
+const Context = {
+  Chrome: "chrome",
+  Content: "content",
+};
+this.Context = Context;
+
+/**
+ * Gets the correct context from a string.
+ *
+ * @param {string} s
+ *     Context string serialisation.
+ *
+ * @return {Context}
+ *     Context.
+ *
+ * @throws {TypeError}
+ *     If <var>s</var> is not a context.
+ */
+Context.fromString = function(s) {
+  switch (s) {
+    case "chrome":
+      return Context.Chrome;
+    case "content":
+      return Context.Content;
+    default:
+      throw new TypeError(`Unknown context: ${s}`);
+  }
+};
+
+/**
  * Get the <code>&lt;xul:browser&gt;</code> for the specified tab.
  *
  * @param {Tab} tab
  *     The tab whose browser needs to be returned.
  *
  * @return {Browser}
  *     The linked browser for the tab or null if no browser can be found.
  */
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -13,16 +13,17 @@ Cu.import("resource://gre/modules/Servic
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/addon.js");
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/atom.js");
 const {
   browser,
+  Context,
   WindowState,
 } = Cu.import("chrome://marionette/content/browser.js", {});
 Cu.import("chrome://marionette/content/capture.js");
 Cu.import("chrome://marionette/content/cert.js");
 Cu.import("chrome://marionette/content/cookie.js");
 Cu.import("chrome://marionette/content/element.js");
 const {
   ElementNotInteractableError,
@@ -49,17 +50,17 @@ Cu.import("chrome://marionette/content/r
 Cu.import("chrome://marionette/content/session.js");
 const {
   PollPromise,
   TimedPromise,
 } = Cu.import("chrome://marionette/content/sync.js", {});
 
 Cu.importGlobalProperties(["URL"]);
 
-this.EXPORTED_SYMBOLS = ["GeckoDriver", "Context"];
+this.EXPORTED_SYMBOLS = ["GeckoDriver"];
 
 const APP_ID_FIREFOX = "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}";
 
 const FRAME_SCRIPT = "chrome://marionette/content/listener.js";
 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
 
 const CLICK_TO_START_PREF = "marionette.debugging.clicktostart";
 const CONTENT_LISTENER_PREF = "marionette.contentListener";
@@ -82,34 +83,16 @@ const globalMessageManager = Cc["@mozill
  * The Marionette WebDriver services provides a standard conforming
  * implementation of the W3C WebDriver specification.
  *
  * @see {@link https://w3c.github.io/webdriver/webdriver-spec.html}
  * @namespace driver
  */
 
 /**
- * @enum
- * @memberof driver
- */
-this.Context = {
-  CHROME: "chrome",
-  CONTENT: "content",
-};
-
-/** @memberof driver */
-this.Context.fromString = function(s) {
-  s = s.toUpperCase();
-  if (s in this) {
-    return this[s];
-  }
-  return null;
-};
-
-/**
  * Helper function for converting a {@link nsISimpleEnumerator} to a
  * JavaScript iterator.
  *
  * @memberof driver
  *
  * @param {nsISimpleEnumerator} enumerator
  *     Enumerator to turn into  iterator.
  *
@@ -155,17 +138,17 @@ this.GeckoDriver = function(appId, serve
   this.currentFrameElement = null;
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   this.observing = null;
   this._browserIds = new WeakMap();
 
   // The curent context decides if commands should affect chrome- or
   // content space.
-  this.context = Context.CONTENT;
+  this.context = Context.Content;
 
   this.sandboxes = new Sandboxes(() => this.getCurrentWindow());
   this.legacyactions = new legacyaction.Chain();
 
   this.timer = null;
   this.inactivityTimer = null;
 
   this.testName = null;
@@ -192,37 +175,37 @@ Object.defineProperty(GeckoDriver.protot
  * depending on context.
  *
  * @return {URL}
  *     Read-only property containing the currently loaded URL.
  */
 Object.defineProperty(GeckoDriver.prototype, "currentURL", {
   get() {
     switch (this.context) {
-      case Context.CHROME:
+      case Context.Chrome:
         let chromeWin = this.getCurrentWindow();
         return new URL(chromeWin.location.href);
 
-      case Context.CONTENT:
+      case Context.Content:
         return new URL(this.curBrowser.currentURI.spec);
 
       default:
         throw new TypeError(`Unknown context: ${this.context}`);
     }
   },
 });
 
 Object.defineProperty(GeckoDriver.prototype, "title", {
   get() {
     switch (this.context) {
-      case Context.CHROME:
+      case Context.Chrome:
         let chromeWin = this.getCurrentWindow();
         return chromeWin.document.documentElement.getAttribute("title");
 
-      case Context.CONTENT:
+      case Context.Content:
         return this.curBrowser.currentTitle;
 
       default:
         throw new TypeError(`Unknown context: ${this.context}`);
     }
   },
 });
 
@@ -404,25 +387,25 @@ GeckoDriver.prototype.sendTargettedAsync
  * @return {ChromeWindow}
  *     The current top-level browsing context.
  */
 GeckoDriver.prototype.getCurrentWindow = function(forcedContext = undefined) {
   let context = typeof forcedContext == "undefined" ? this.context : forcedContext;
   let win = null;
 
   switch (context) {
-    case Context.CHROME:
+    case Context.Chrome:
       if (this.curFrame !== null) {
         win = this.curFrame;
       } else if (this.curBrowser !== null) {
         win = this.curBrowser.window;
       }
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       if (this.curFrame !== null) {
         win = this.curFrame;
       } else if (this.curBrowser !== null && this.curBrowser.contentBrowser) {
         win = this.curBrowser.window;
       }
       break;
   }
 
@@ -859,35 +842,49 @@ GeckoDriver.prototype.newSession = async
  * ("capabilities") to values, which may be of types boolean,
  * numerical or string.
  */
 GeckoDriver.prototype.getSessionCapabilities = function(cmd, resp) {
   resp.body.capabilities = this.capabilities;
 };
 
 /**
- * Sets the context of the subsequent commands to be either "chrome" or
- * "content".
+ * Sets the context of the subsequent commands.
+ *
+ * All subsequent requests to commands that in some way involve
+ * interaction with a browsing context will target the chosen browsing
+ * context.
  *
  * @param {string} value
  *     Name of the context to be switched to.  Must be one of "chrome" or
  *     "content".
+ *
+ * @throws {InvalidArgumentError}
+ *     If <var>value</var> is not a string.
+ * @throws {WebDriverError}
+ *     If <var>value</var> is not a valid browsing context.
  */
 GeckoDriver.prototype.setContext = function(cmd) {
-  let val = cmd.parameters.value;
-  let ctx = Context.fromString(val);
-  if (ctx === null) {
-    throw new WebDriverError(`Invalid context: ${val}`);
-  }
-  this.context = ctx;
+  let value = assert.string(cmd.parameters.value);
+  this.context = Context.fromString(value);
 };
 
-/** Gets the context of the server, either "chrome" or "content". */
+/**
+ * Gets the context type that is Marionette's current target for
+ * browsing context scoped commands.
+ *
+ * You may choose a context through the {@link #setContext} command.
+ *
+ * The default browsing context is {@link Context.Content}.
+ *
+ * @return {Context}
+ *     Current context.
+ */
 GeckoDriver.prototype.getContext = function(cmd, resp) {
-  resp.body.value = this.context.toString();
+  return this.context;
 };
 
 /**
  * Executes a JavaScript function in the context of the current browsing
  * context, if in content space, or in chrome space otherwise, and returns
  * the return value of the function.
  *
  * It is important to note that if the <var>sandboxName</var> parameter
@@ -1030,30 +1027,30 @@ GeckoDriver.prototype.executeAsyncScript
   resp.body.value = await this.execute_(script, args, scriptTimeout, opts);
 };
 
 GeckoDriver.prototype.execute_ = async function(
     script, args, timeout, opts = {}) {
   let res, els;
 
   switch (this.context) {
-    case Context.CONTENT:
+    case Context.Content:
       // evaluate in content with lasting side-effects
       if (!opts.sandboxName) {
         res = await this.listener.execute(script, args, timeout, opts);
 
       // evaluate in content with sandbox
       } else {
         res = await this.listener.executeInSandbox(
             script, args, timeout, opts);
       }
 
       break;
 
-    case Context.CHROME:
+    case Context.Chrome:
       let sb = this.sandboxes.get(opts.sandboxName, opts.newSandbox);
       opts.timeout = timeout;
       let wargs = evaluate.fromJSON(args, this.curBrowser.seenEls, sb.window);
       res = await evaluate.sandbox(sb, script, wargs, opts);
       els = this.curBrowser.seenEls;
       break;
 
     default:
@@ -1181,22 +1178,22 @@ GeckoDriver.prototype.getWindowType = fu
  * @throws {UnexpectedAlertOpenError}
  *     A modal dialog is open, blocking this operation.
  */
 GeckoDriver.prototype.getPageSource = async function(cmd, resp) {
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let s = new win.XMLSerializer();
       resp.body.value = s.serializeToString(win.document);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.getPageSource();
       break;
   }
 };
 
 /**
  * Cause the browser to traverse one step backward in the joint history
  * of the current browsing context.
@@ -1395,17 +1392,17 @@ GeckoDriver.prototype.getWindowHandles =
  *
  * @return {string}
  *     Unique window handle.
  *
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
  */
 GeckoDriver.prototype.getChromeWindowHandle = function(cmd, resp) {
-  assert.window(this.getCurrentWindow(Context.CHROME));
+  assert.window(this.getCurrentWindow(Context.Chrome));
 
   for (let i in this.browsers) {
     if (this.curBrowser == this.browsers[i]) {
       resp.body.value = i;
       return;
     }
   }
 };
@@ -1672,28 +1669,28 @@ GeckoDriver.prototype.setWindowHandle = 
     }
   }
 };
 
 GeckoDriver.prototype.getActiveFrame = function(cmd, resp) {
   assert.window(this.getCurrentWindow());
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       // no frame means top-level
       resp.body.value = null;
       if (this.curFrame) {
         let elRef = this.curBrowser.seenEls
             .add(this.curFrame.frameElement);
         let el = element.makeWebElement(elRef);
         resp.body.value = el;
       }
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = null;
       if (this.currentFrameElement !== null) {
         let el = element.makeWebElement(this.currentFrameElement);
         resp.body.value = el;
       }
       break;
   }
 };
@@ -1751,17 +1748,17 @@ GeckoDriver.prototype.switchToFrame = as
         throw new UnknownError("Reached error page: " + documentURI);
       }
     }
 
     checkTimer.initWithCallback(
         checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
   };
 
-  if (this.context == Context.CHROME) {
+  if (this.context == Context.Chrome) {
     let foundFrame = null;
 
     // just focus
     if (typeof id == "undefined" && typeof element == "undefined") {
       this.curFrame = null;
       if (focus) {
         this.mainFrame.focus();
       }
@@ -1868,17 +1865,17 @@ GeckoDriver.prototype.switchToFrame = as
         this.curFrame.focus();
       }
       checkTimer.initWithCallback(
           checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
     } else {
       throw new NoSuchFrameError(`Unable to locate frame: ${id}`);
     }
 
-  } else if (this.context == Context.CONTENT) {
+  } else if (this.context == Context.Content) {
     if (!id && !element &&
         this.curBrowser.frameManager.currentRemoteFrame !== null) {
       // We're currently using a ChromeMessageSender for a remote frame,
       // so this request indicates we need to switch back to the top-level
       // (parent) frame.  We'll first switch to the parent's (global)
       // ChromeMessageBroadcaster, so we send the message to the right
       // listener.
       this.switchToGlobalMessageManager();
@@ -1925,21 +1922,21 @@ GeckoDriver.prototype.setTimeouts = func
 
 /** Single tap. */
 GeckoDriver.prototype.singleTap = async function(cmd) {
   assert.window(this.getCurrentWindow());
 
   let {id, x, y} = cmd.parameters;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       throw new UnsupportedOperationError(
           "Command 'singleTap' is not yet available in chrome context");
 
-    case Context.CONTENT:
+    case Context.Content:
       this.addFrameCloseListener("tap");
       await this.listener.singleTap(id, x, y);
       break;
   }
 };
 
 /**
  * Perform a series of grouped actions at the specified points in time.
@@ -2001,26 +1998,26 @@ GeckoDriver.prototype.releaseActions = a
  */
 GeckoDriver.prototype.actionChain = async function(cmd, resp) {
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let {chain, nextId} = cmd.parameters;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       // be conservative until this has a use case and is established
       // to work as expected in Fennec
       assert.firefox();
 
       resp.body.value = await this.legacyactions.dispatchActions(
           chain, nextId, {frame: win}, this.curBrowser.seenEls);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       this.addFrameCloseListener("action chain");
       resp.body.value = await this.listener.actionChain(chain, nextId);
       break;
   }
 };
 
 /**
  * A multi-action chain.
@@ -2069,33 +2066,33 @@ GeckoDriver.prototype.findElement = asyn
   let expr = cmd.parameters.value;
   let opts = {
     startNode: cmd.parameters.element,
     timeout: this.timeouts.implicit,
     all: false,
   };
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       if (!SUPPORTED_STRATEGIES.has(strategy)) {
         throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
       }
 
       let container = {frame: win};
       if (opts.startNode) {
         opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
       }
       let el = await element.find(container, strategy, expr, opts);
       let elRef = this.curBrowser.seenEls.add(el);
       let webEl = element.makeWebElement(elRef);
 
       resp.body.value = webEl;
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.findElementContent(
           strategy,
           expr,
           opts);
       break;
   }
 };
 
@@ -2114,33 +2111,33 @@ GeckoDriver.prototype.findElements = asy
   let expr = cmd.parameters.value;
   let opts = {
     startNode: cmd.parameters.element,
     timeout: this.timeouts.implicit,
     all: true,
   };
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       if (!SUPPORTED_STRATEGIES.has(strategy)) {
         throw new InvalidSelectorError(`Strategy not supported: ${strategy}`);
       }
 
       let container = {frame: win};
       if (opts.startNode) {
         opts.startNode = this.curBrowser.seenEls.get(opts.startNode);
       }
       let els = await element.find(container, strategy, expr, opts);
 
       let elRefs = this.curBrowser.seenEls.addAll(els);
       let webEls = elRefs.map(element.makeWebElement);
       resp.body = webEls;
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body = await this.listener.findElementsContent(
           cmd.parameters.using,
           cmd.parameters.value,
           opts);
       break;
   }
 };
 
@@ -2178,22 +2175,22 @@ GeckoDriver.prototype.getActiveElement =
  */
 GeckoDriver.prototype.clickElement = async function(cmd) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       await interaction.clickElement(el, this.a11yChecks);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       // We need to protect against the click causing an OOP frame
       // to close.  This fires the mozbrowserclose event when it closes
       // so we need to listen for it and then just send an error back.
       // The person making the call should be aware something is not right
       // and handle accordingly.
       this.addFrameCloseListener("click");
 
       let click = this.listener.clickElement(
@@ -2237,22 +2234,22 @@ GeckoDriver.prototype.clickElement = asy
  */
 GeckoDriver.prototype.getElementAttribute = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let {id, name} = cmd.parameters;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       resp.body.value = el.getAttribute(name);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.getElementAttribute(id, name);
       break;
   }
 };
 
 /**
  * Returns the value of a property associated with given element.
  *
@@ -2271,22 +2268,22 @@ GeckoDriver.prototype.getElementAttribut
  */
 GeckoDriver.prototype.getElementProperty = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let {id, name} = cmd.parameters;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       resp.body.value = el[name];
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.getElementProperty(id, name);
       break;
   }
 };
 
 /**
  * Get the text of an element, if any.  Includes the text of all child
  * elements.
@@ -2304,25 +2301,25 @@ GeckoDriver.prototype.getElementProperty
  */
 GeckoDriver.prototype.getElementText = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       // for chrome, we look at text nodes, and any node with a "label" field
       let el = this.curBrowser.seenEls.get(id);
       let lines = [];
       this.getVisibleText(el, lines);
       resp.body.value = lines.join("\n");
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.getElementText(id);
       break;
   }
 };
 
 /**
  * Get the tag name of the element.
  *
@@ -2339,22 +2336,22 @@ GeckoDriver.prototype.getElementText = a
  */
 GeckoDriver.prototype.getElementTagName = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       resp.body.value = el.tagName.toLowerCase();
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.getElementTagName(id);
       break;
   }
 };
 
 /**
  * Check if element is displayed.
  *
@@ -2371,23 +2368,23 @@ GeckoDriver.prototype.getElementTagName 
  */
 GeckoDriver.prototype.isElementDisplayed = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       resp.body.value = await interaction.isElementDisplayed(
           el, this.a11yChecks);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.isElementDisplayed(id);
       break;
   }
 };
 
 /**
  * Return the property of the computed style of an element.
  *
@@ -2407,23 +2404,23 @@ GeckoDriver.prototype.isElementDisplayed
 GeckoDriver.prototype.getElementValueOfCssProperty = async function(
     cmd, resp) {
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let {id, propertyName: prop} = cmd.parameters;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       let sty = win.document.defaultView.getComputedStyle(el);
       resp.body.value = sty.getPropertyValue(prop);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener
           .getElementValueOfCssProperty(id, prop);
       break;
   }
 };
 
 /**
  * Check if element is enabled.
@@ -2441,24 +2438,24 @@ GeckoDriver.prototype.getElementValueOfC
  */
 GeckoDriver.prototype.isElementEnabled = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       // Selenium atom doesn't quite work here
       let el = this.curBrowser.seenEls.get(id);
       resp.body.value = await interaction.isElementEnabled(
           el, this.a11yChecks);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.isElementEnabled(id);
       break;
   }
 };
 
 /**
  * Check if element is selected.
  *
@@ -2475,24 +2472,24 @@ GeckoDriver.prototype.isElementEnabled =
  */
 GeckoDriver.prototype.isElementSelected = async function(cmd, resp) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       // Selenium atom doesn't quite work here
       let el = this.curBrowser.seenEls.get(id);
       resp.body.value = await interaction.isElementSelected(
           el, this.a11yChecks);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body.value = await this.listener.isElementSelected(id);
       break;
   }
 };
 
 /**
  * @throws {NoSuchWindowError}
  *     Top-level browsing context has been discarded.
@@ -2501,28 +2498,28 @@ GeckoDriver.prototype.isElementSelected 
  */
 GeckoDriver.prototype.getElementRect = async function(cmd, resp) {
   const win = assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       let rect = el.getBoundingClientRect();
       resp.body = {
         x: rect.x + win.pageXOffset,
         y: rect.y + win.pageYOffset,
         width: rect.width,
         height: rect.height,
       };
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       resp.body = await this.listener.getElementRect(id);
       break;
   }
 };
 
 /**
  * Send key presses to element after focusing on it.
  *
@@ -2539,23 +2536,23 @@ GeckoDriver.prototype.getElementRect = a
 GeckoDriver.prototype.sendKeysToElement = async function(cmd) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let {id, text} = cmd.parameters;
   assert.string(text);
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let el = this.curBrowser.seenEls.get(id);
       await interaction.sendKeysToElement(
           el, text, true, this.a11yChecks);
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       await this.listener.sendKeysToElement(id, text);
       break;
   }
 };
 
 /**
  * Clear the text of an element.
  *
@@ -2569,27 +2566,27 @@ GeckoDriver.prototype.sendKeysToElement 
  */
 GeckoDriver.prototype.clearElement = async function(cmd) {
   assert.window(this.getCurrentWindow());
   assert.noUserPrompt(this.dialog);
 
   let id = cmd.parameters.id;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       // the selenium atom doesn't work here
       let el = this.curBrowser.seenEls.get(id);
       if (el.nodeName == "textbox") {
         el.value = "";
       } else if (el.nodeName == "checkbox") {
         el.checked = false;
       }
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       await this.listener.clearElement(id);
       break;
   }
 };
 
 /**
  * Switch to shadow root of the given host element.
  *
@@ -2759,17 +2756,17 @@ GeckoDriver.prototype.close = async func
  * closed to prevent a shutdown of the application. Instead the returned
  * list of chrome window handles is empty.
  *
  * @return {Array.<string>}
  *     Unique chrome window handles of remaining chrome windows.
  */
 GeckoDriver.prototype.closeChromeWindow = async function() {
   assert.firefox();
-  assert.window(this.getCurrentWindow(Context.CHROME));
+  assert.window(this.getCurrentWindow(Context.Chrome));
 
   let nwins = 0;
 
   // eslint-disable-next-line
   for (let _ of this.windows) {
     nwins++;
   }
 
@@ -2883,17 +2880,17 @@ GeckoDriver.prototype.deleteSession = fu
 GeckoDriver.prototype.takeScreenshot = function(cmd) {
   let win = assert.window(this.getCurrentWindow());
 
   let {id, highlights, full, hash} = cmd.parameters;
   highlights = highlights || [];
   let format = hash ? capture.Format.Hash : capture.Format.Base64;
 
   switch (this.context) {
-    case Context.CHROME:
+    case Context.Chrome:
       let highlightEls = highlights.map(
           ref => this.curBrowser.seenEls.get(ref));
 
       // viewport
       let canvas;
       if (!id && !full) {
         canvas = capture.viewport(win, highlightEls);
 
@@ -2913,17 +2910,17 @@ GeckoDriver.prototype.takeScreenshot = f
         case capture.Format.Hash:
           return capture.toHash(canvas);
 
         case capture.Format.Base64:
           return capture.toBase64(canvas);
       }
       break;
 
-    case Context.CONTENT:
+    case Context.Content:
       return this.listener.takeScreenshot(format, cmd.parameters);
   }
 
   throw new TypeError(`Unknown context: ${this.context}`);
 };
 
 /**
  * Get the current browser orientation.
@@ -3463,17 +3460,17 @@ GeckoDriver.prototype.localizeProperty =
  * Initialize the reftest mode
  */
 GeckoDriver.prototype.setupReftest = async function(cmd) {
   if (this._reftest) {
     throw new UnsupportedOperationError(
         "Called reftest:setup with a reftest session already active");
   }
 
-  if (this.context !== Context.CHROME) {
+  if (this.context !== Context.Chrome) {
     throw new UnsupportedOperationError(
         "Must set chrome context before running reftests");
   }
 
   let {urlCount = {}, screenshot = "unexpected"} = cmd.parameters;
   if (!["always", "fail", "unexpected"].includes(screenshot)) {
     throw new InvalidArgumentError(
         "Value of `screenshot` should be 'always', 'fail' or 'unexpected'");
@@ -3512,17 +3509,16 @@ GeckoDriver.prototype.teardownReftest = 
     throw new UnsupportedOperationError(
         "Called reftest:teardown before reftest:start");
   }
 
   this._reftest.abort();
   this._reftest = null;
 };
 
-
 GeckoDriver.prototype.commands = {
   // Marionette service
   "Marionette:SetContext": GeckoDriver.prototype.setContext,
   "setContext": GeckoDriver.prototype.setContext,  // deprecated, remove in Firefox 60
   "Marionette:GetContext": GeckoDriver.prototype.getContext,
   "getContext": GeckoDriver.prototype.getContext,
   "Marionette:AcceptConnections": GeckoDriver.prototype.acceptConnections,
   "acceptConnections": GeckoDriver.prototype.acceptConnections,  // deprecated, remove in Firefox 60
--- a/testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py
+++ b/testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py
@@ -1,17 +1,15 @@
 # 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 itertools
 import time
 
 from marionette_driver import errors
-
 from marionette_harness import MarionetteTestCase, run_if_manage_instance, skip_if_mobile
 
 
 class TestMarionette(MarionetteTestCase):
 
     def test_correct_test_name(self):
         """Test that the correct test name gets set."""
         expected_test_name = '{module}.py {cls}.{func}'.format(
@@ -26,8 +24,39 @@ class TestMarionette(MarionetteTestCase)
     @skip_if_mobile("Bug 1322993 - Missing temporary folder")
     def test_wait_for_port_non_existing_process(self):
         """Test that wait_for_port doesn't run into a timeout if instance is not running."""
         self.marionette.quit()
         self.assertIsNotNone(self.marionette.instance.runner.returncode)
         start_time = time.time()
         self.assertFalse(self.marionette.wait_for_port(timeout=5))
         self.assertLess(time.time() - start_time, 5)
+
+
+class TestContext(MarionetteTestCase):
+
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        self.marionette.set_context(self.marionette.CONTEXT_CONTENT)
+
+    def get_context(self):
+        return self.marionette._send_message("getContext", key="value")
+
+    def set_context(self, value):
+        return self.marionette._send_message("setContext", {"value": value})
+
+    def test_set_context(self):
+        self.assertEqual(self.set_context("content"), {})
+        self.assertEqual(self.set_context("chrome"), {})
+
+        for typ in [True, 42, [], {}, None]:
+            with self.assertRaises(errors.InvalidArgumentException):
+                self.set_context(typ)
+
+        with self.assertRaises(errors.MarionetteException):
+            self.set_context("foo")
+
+    def test_get_context(self):
+        self.assertEqual(self.get_context(), "content")
+        self.set_context("chrome")
+        self.assertEqual(self.get_context(), "chrome")
+        self.set_context("content")
+        self.assertEqual(self.get_context(), "content")
new file mode 100644
--- /dev/null
+++ b/testing/marionette/test_browser.js
@@ -0,0 +1,25 @@
+const {utils: Cu} = Components;
+
+const {Context} = Cu.import("chrome://marionette/content/browser.js", {});
+
+add_test(function test_Context() {
+  ok(Context.hasOwnProperty("Chrome"));
+  ok(Context.hasOwnProperty("Content"));
+  equal(typeof Context.Chrome, "string");
+  equal(typeof Context.Content, "string");
+  equal(Context.Chrome, "chrome");
+  equal(Context.Content, "content");
+
+  run_next_test();
+});
+
+add_test(function test_Context_fromString() {
+  equal(Context.fromString("chrome"), Context.Chrome);
+  equal(Context.fromString("content"), Context.Content);
+
+  for (let typ of ["", "foo", true, 42, [], {}, null, undefined]) {
+    Assert.throws(() => Context.fromString(typ), /TypeError/);
+  }
+
+  run_next_test();
+});
--- a/testing/marionette/unit.ini
+++ b/testing/marionette/unit.ini
@@ -4,16 +4,17 @@
 
 # xpcshell unit tests for Marionette
 
 [DEFAULT]
 skip-if = appname == "thunderbird"
 
 [test_action.js]
 [test_assert.js]
+[test_browser.js]
 [test_cookie.js]
 [test_dom.js]
 [test_element.js]
 [test_error.js]
 [test_format.js]
 [test_message.js]
 [test_navigate.js]
 [test_session.js]