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 680952 4dc9211f4ff0c6d68e2e706a3f3005d729770deb
parent 680782 c6a2643362a67cdf7a87ac165454fce4b383debb
child 736026 e775bc23cfc842c3da7563a2072cb2b628092c55
push id84690
push userbmo:ato@sny.no
push dateMon, 16 Oct 2017 16:50:57 +0000
reviewersmaja_zf
bugs1408508
milestone58.0a1
Bug 1408508 - Move Context to browser module. r?maja_zf To avoid circular dependencies in Marionette, specific types or symbols required by many parts of Marionette should live in namespaced modules so that they can be imported without fear or recursion. For example, if testing/marionette/browser.js which in the future needs to have access to the Context enum were to import testing/marionette/driver.js where it is specified, Cu.import would enter an infinite recursion because driver.js also imports browser.js. 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
@@ -60,8 +60,39 @@ class TestProtocol2Errors(MarionetteTest
                  "stacktrace": None})
 
     def test_unknown_error_status(self):
         with self.assertRaises(errors.MarionetteException):
             self.marionette._handle_error(
                 {"error": "barbera",
                  "message": None,
                  "stacktrace": None})
+
+
+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]