Bug 1002280 - ID starting with numeric value crashes the devtools DOM inspector. r=jwalker, r=bholley, a=lmandel
☠☠ backed out by 7c17cea2508b ☠ ☠
authorBrian Grinstead <bgrinstead@mozilla.com>
Fri, 09 May 2014 09:58:26 -0500
changeset 199366 9cf15aa7756f6be485a5af66534c0c4971f76a7f
parent 199365 765e85a7f2916b42b899eef4ec9dadb66a65d084
child 199367 4622a42e4a2c48db42d717cbb5d0bc041d2861dd
push id3624
push userasasaki@mozilla.com
push dateMon, 09 Jun 2014 21:49:01 +0000
treeherdermozilla-beta@b1a5da15899a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjwalker, bholley, lmandel
bugs1002280
milestone31.0a2
Bug 1002280 - ID starting with numeric value crashes the devtools DOM inspector. r=jwalker, r=bholley, a=lmandel
browser/devtools/commandline/test/browser.ini
browser/devtools/commandline/test/browser_gcli_util.js
browser/devtools/inspector/test/browser_inspector_reload.js
js/xpconnect/src/Sandbox.cpp
js/xpconnect/src/xpcprivate.h
js/xpconnect/tests/unit/test_css.js
js/xpconnect/tests/unit/xpcshell.ini
toolkit/devtools/gcli/source/lib/gcli/util/util.js
toolkit/devtools/server/actors/script.js
toolkit/devtools/server/tests/mochitest/chrome.ini
toolkit/devtools/server/tests/mochitest/test_css-logic.html
toolkit/devtools/styleinspector/css-logic.js
--- a/browser/devtools/commandline/test/browser.ini
+++ b/browser/devtools/commandline/test/browser.ini
@@ -75,9 +75,8 @@ skip-if = true || e10s # Disabled until 
 [browser_gcli_node.js]
 [browser_gcli_resource.js]
 [browser_gcli_short.js]
 [browser_gcli_spell.js]
 [browser_gcli_split.js]
 [browser_gcli_tokenize.js]
 [browser_gcli_tooltip.js]
 [browser_gcli_types.js]
-[browser_gcli_util.js]
deleted file mode 100644
--- a/browser/devtools/commandline/test/browser_gcli_util.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*
- * Copyright 2012, Mozilla Foundation and contributors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-'use strict';
-// <INJECTED SOURCE:START>
-
-// THIS FILE IS GENERATED FROM SOURCE IN THE GCLI PROJECT
-// DO NOT EDIT IT DIRECTLY
-
-var exports = {};
-
-var TEST_URI = "data:text/html;charset=utf-8,<p id='gcli-input'>gcli-testUtil.js</p>";
-
-function test() {
-  return Task.spawn(function() {
-    let options = yield helpers.openTab(TEST_URI);
-    yield helpers.openToolbar(options);
-    gcli.addItems(mockCommands.items);
-
-    yield helpers.runTests(options, exports);
-
-    gcli.removeItems(mockCommands.items);
-    yield helpers.closeToolbar(options);
-    yield helpers.closeTab(options);
-  }).then(finish, helpers.handleError);
-}
-
-// <INJECTED SOURCE:END>
-
-// var assert = require('../testharness/assert');
-var util = require('gcli/util/util');
-
-exports.testFindCssSelector = function(options) {
-  if (options.isPhantomjs || options.isNoDom) {
-    assert.log('Skipping tests due to issues with querySelectorAll.');
-    return;
-  }
-
-  var nodes = options.window.document.querySelectorAll('*');
-  for (var i = 0; i < nodes.length; i++) {
-    var selector = util.findCssSelector(nodes[i]);
-    var matches = options.window.document.querySelectorAll(selector);
-
-    assert.is(matches.length, 1, 'multiple matches for ' + selector);
-    assert.is(matches[0], nodes[i], 'non-matching selector: ' + selector);
-  }
-};
--- a/browser/devtools/inspector/test/browser_inspector_reload.js
+++ b/browser/devtools/inspector/test/browser_inspector_reload.js
@@ -16,17 +16,20 @@ function test() {
     waitForFocus(function() {
       let target = TargetFactory.forTab(gBrowser.selectedTab);
       gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
         startInspectorTests(toolbox);
       }).then(null, console.error);
     }, content);
   }, true);
 
-  content.location = "data:text/html,<p>p</p>";
+  // Reload should reselect the currently selected markup view element.
+  // This should work even when an element whose selector needs escaping
+  // is selected (bug 100228).
+  content.location = "data:text/html,<p id='1'>p</p>";
 
   function startInspectorTests(aToolbox)
   {
     toolbox = aToolbox;
     inspector = toolbox.getCurrentPanel();
     info("Inspector started");
     let p = content.document.querySelector("p");
     inspector.selection.setNode(p);
--- a/js/xpconnect/src/Sandbox.cpp
+++ b/js/xpconnect/src/Sandbox.cpp
@@ -25,16 +25,17 @@
 #include "nsPrincipal.h"
 #include "nsXMLHttpRequest.h"
 #include "WrapperFactory.h"
 #include "xpcprivate.h"
 #include "XPCQuickStubs.h"
 #include "XPCWrapper.h"
 #include "XrayWrapper.h"
 #include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/CSSBinding.h"
 #include "mozilla/dom/indexedDB/IndexedDatabaseManager.h"
 #include "mozilla/dom/PromiseBinding.h"
 #include "mozilla/dom/TextDecoderBinding.h"
 #include "mozilla/dom/TextEncoderBinding.h"
 #include "mozilla/dom/URLBinding.h"
 
 using namespace mozilla;
 using namespace JS;
@@ -981,16 +982,18 @@ xpc::GlobalProperties::Parse(JSContext *
         if (!nameValue.isString()) {
             JS_ReportError(cx, "Property names must be strings");
             return false;
         }
         JSAutoByteString name(cx, nameValue.toString());
         NS_ENSURE_TRUE(name, false);
         if (promise && !strcmp(name.ptr(), "-Promise")) {
             Promise = false;
+        } else if (!strcmp(name.ptr(), "CSS")) {
+            CSS = true;
         } else if (!strcmp(name.ptr(), "indexedDB")) {
             indexedDB = true;
         } else if (!strcmp(name.ptr(), "XMLHttpRequest")) {
             XMLHttpRequest = true;
         } else if (!strcmp(name.ptr(), "TextEncoder")) {
             TextEncoder = true;
         } else if (!strcmp(name.ptr(), "TextDecoder")) {
             TextDecoder = true;
@@ -1006,16 +1009,19 @@ xpc::GlobalProperties::Parse(JSContext *
         }
     }
     return true;
 }
 
 bool
 xpc::GlobalProperties::Define(JSContext *cx, JS::HandleObject obj)
 {
+    if (CSS && !dom::CSSBinding::GetConstructorObject(cx, obj))
+        return false;
+
     if (Promise && !dom::PromiseBinding::GetConstructorObject(cx, obj))
         return false;
 
     if (indexedDB && AccessCheck::isChrome(obj) &&
         !IndexedDatabaseManager::DefineIndexedDB(cx, obj))
         return false;
 
     if (XMLHttpRequest &&
--- a/js/xpconnect/src/xpcprivate.h
+++ b/js/xpconnect/src/xpcprivate.h
@@ -3292,16 +3292,17 @@ ThrowAndFail(nsresult errNum, JSContext 
 
 struct GlobalProperties {
     GlobalProperties(bool aPromise) {
       mozilla::PodZero(this);
       Promise = true;
     }
     bool Parse(JSContext *cx, JS::HandleObject obj);
     bool Define(JSContext *cx, JS::HandleObject obj);
+    bool CSS : 1;
     bool Promise : 1;
     bool indexedDB : 1;
     bool XMLHttpRequest : 1;
     bool TextDecoder : 1;
     bool TextEncoder : 1;
     bool URL : 1;
     bool atob : 1;
     bool btoa : 1;
new file mode 100644
--- /dev/null
+++ b/js/xpconnect/tests/unit/test_css.js
@@ -0,0 +1,10 @@
+function run_test() {
+  var Cu = Components.utils;
+  var sb = new Cu.Sandbox('http://www.example.com',
+                          { wantGlobalProperties: ["CSS"] });
+  sb.do_check_eq = do_check_eq;
+  Cu.evalInSandbox('do_check_eq(CSS.escape("$"), "\\\\$");',
+                   sb);
+  Cu.importGlobalProperties(["CSS"]);
+  do_check_eq(CSS.escape("$"), "\\$");
+}
--- a/js/xpconnect/tests/unit/xpcshell.ini
+++ b/js/xpconnect/tests/unit/xpcshell.ini
@@ -69,16 +69,17 @@ fail-if = os == "android"
 [test_allowedDomains.js]
 [test_allowedDomainsXHR.js]
 [test_nuke_sandbox.js]
 [test_sandbox_metadata.js]
 [test_exportFunction.js]
 [test_promise.js]
 [test_textDecoder.js]
 [test_url.js]
+[test_css.js]
 [test_sandbox_atob.js]
 [test_isProxy.js]
 [test_getObjectPrincipal.js]
 [test_watchdog_enable.js]
 head = head_watchdog.js
 [test_watchdog_disable.js]
 head = head_watchdog.js
 [test_watchdog_toggle.js]
--- a/toolkit/devtools/gcli/source/lib/gcli/util/util.js
+++ b/toolkit/devtools/gcli/source/lib/gcli/util/util.js
@@ -543,100 +543,25 @@ exports.isXmlDocument = function(doc) {
   // Best test for Chrome
   if (doc.xmlVersion != null) {
     return true;
   }
   return false;
 };
 
 /**
- * Find the position of [element] in [nodeList].
- * @returns an index of the match, or -1 if there is no match
- */
-function positionInNodeList(element, nodeList) {
-  for (var i = 0; i < nodeList.length; i++) {
-    if (element === nodeList[i]) {
-      return i;
-    }
-  }
-  return -1;
-}
-
-/**
  * We'd really like to be able to do 'new NodeList()'
  */
 exports.createEmptyNodeList = function(doc) {
   if (doc.createDocumentFragment) {
     return doc.createDocumentFragment().childNodes;
   }
   return doc.querySelectorAll('x>:root');
 };
 
-/**
- * Find a unique CSS selector for a given element
- * @returns a string such that ele.ownerDocument.querySelector(reply) === ele
- * and ele.ownerDocument.querySelectorAll(reply).length === 1
- */
-exports.findCssSelector = function(ele) {
-  var document = ele.ownerDocument;
-  if (ele.id && document.getElementById(ele.id) === ele) {
-    return '#' + ele.id;
-  }
-
-  // Inherently unique by tag name
-  var tagName = ele.tagName.toLowerCase();
-  if (tagName === 'html') {
-    return 'html';
-  }
-  if (tagName === 'head') {
-    return 'head';
-  }
-  if (tagName === 'body') {
-    return 'body';
-  }
-
-  if (ele.parentNode == null) {
-    console.log('danger: ' + tagName);
-  }
-
-  // We might be able to find a unique class name
-  var selector, index, matches;
-  if (ele.classList.length > 0) {
-    for (var i = 0; i < ele.classList.length; i++) {
-      // Is this className unique by itself?
-      selector = '.' + ele.classList.item(i);
-      matches = document.querySelectorAll(selector);
-      if (matches.length === 1) {
-        return selector;
-      }
-      // Maybe it's unique with a tag name?
-      selector = tagName + selector;
-      matches = document.querySelectorAll(selector);
-      if (matches.length === 1) {
-        return selector;
-      }
-      // Maybe it's unique using a tag name and nth-child
-      index = positionInNodeList(ele, ele.parentNode.children) + 1;
-      selector = selector + ':nth-child(' + index + ')';
-      matches = document.querySelectorAll(selector);
-      if (matches.length === 1) {
-        return selector;
-      }
-    }
-  }
-
-  // So we can be unique w.r.t. our parent, and use recursion
-  index = positionInNodeList(ele, ele.parentNode.children) + 1;
-  selector = exports.findCssSelector(ele.parentNode) + ' > ' +
-          tagName + ':nth-child(' + index + ')';
-
-  return selector;
-};
-
-
 //------------------------------------------------------------------------------
 
 /**
  * Keyboard handling is a mess. http://unixpapa.com/js/key.html
  * It would be good to use DOM L3 Keyboard events,
  * http://www.w3.org/TR/2010/WD-DOM-Level-3-Events-20100907/#events-keyboardevents
  * however only Webkit supports them, and there isn't a shim on Monernizr:
  * https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-browser-Polyfills
--- a/toolkit/devtools/server/actors/script.js
+++ b/toolkit/devtools/server/actors/script.js
@@ -1,16 +1,18 @@
 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; js-indent-level: 2; -*- */
 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */
 /* 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/. */
 
 "use strict";
 
+const {CssLogic} = require("devtools/styleinspector/css-logic");
+
 let B2G_ID = "{3c2e2abc-06d4-11e1-ac3b-374f68613e61}";
 
 let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
       "Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
       "Float64Array"];
 
 // Number of items to preview in objects, arrays, maps, sets, lists,
 // collections, etc.
@@ -1784,17 +1786,17 @@ ThreadActor.prototype = {
         let listener = handler.listenerObject;
         // Native event listeners don't provide any listenerObject or type and
         // are not that useful to a JS debugger.
         if (!listener || !handler.type) {
           continue;
         }
 
         // There will be no tagName if the event listener is set on the window.
-        let selector = node.tagName ? findCssSelector(node) : "window";
+        let selector = node.tagName ? CssLogic.findCssSelector(node) : "window";
         let nodeDO = this.globalDebugObject.makeDebuggeeValue(node);
         listenerForm.node = {
           selector: selector,
           object: this.createValueGrip(nodeDO)
         };
         listenerForm.type = handler.type;
         listenerForm.capturing = handler.capturing;
         listenerForm.allowsUntrusted = handler.allowsUntrusted;
@@ -5495,93 +5497,16 @@ function convertToUnicode(aString, aChar
  */
 function reportError(aError, aPrefix="") {
   dbg_assert(aError instanceof Error, "Must pass Error objects to reportError");
   let msg = aPrefix + aError.message + ":\n" + aError.stack;
   Cu.reportError(msg);
   dumpn(msg);
 }
 
-// The following are copied here verbatim from css-logic.js, until we create a
-// server-friendly helper module.
-
-/**
- * Find a unique CSS selector for a given element
- * @returns a string such that ele.ownerDocument.querySelector(reply) === ele
- * and ele.ownerDocument.querySelectorAll(reply).length === 1
- */
-function findCssSelector(ele) {
-  var document = ele.ownerDocument;
-  if (ele.id && document.getElementById(ele.id) === ele) {
-    return '#' + ele.id;
-  }
-
-  // Inherently unique by tag name
-  var tagName = ele.tagName.toLowerCase();
-  if (tagName === 'html') {
-    return 'html';
-  }
-  if (tagName === 'head') {
-    return 'head';
-  }
-  if (tagName === 'body') {
-    return 'body';
-  }
-
-  if (ele.parentNode == null) {
-    console.log('danger: ' + tagName);
-  }
-
-  // We might be able to find a unique class name
-  var selector, index, matches;
-  if (ele.classList.length > 0) {
-    for (var i = 0; i < ele.classList.length; i++) {
-      // Is this className unique by itself?
-      selector = '.' + ele.classList.item(i);
-      matches = document.querySelectorAll(selector);
-      if (matches.length === 1) {
-        return selector;
-      }
-      // Maybe it's unique with a tag name?
-      selector = tagName + selector;
-      matches = document.querySelectorAll(selector);
-      if (matches.length === 1) {
-        return selector;
-      }
-      // Maybe it's unique using a tag name and nth-child
-      index = positionInNodeList(ele, ele.parentNode.children) + 1;
-      selector = selector + ':nth-child(' + index + ')';
-      matches = document.querySelectorAll(selector);
-      if (matches.length === 1) {
-        return selector;
-      }
-    }
-  }
-
-  // So we can be unique w.r.t. our parent, and use recursion
-  index = positionInNodeList(ele, ele.parentNode.children) + 1;
-  selector = findCssSelector(ele.parentNode) + ' > ' +
-          tagName + ':nth-child(' + index + ')';
-
-  return selector;
-};
-
-/**
- * Find the position of [element] in [nodeList].
- * @returns an index of the match, or -1 if there is no match
- */
-function positionInNodeList(element, nodeList) {
-  for (var i = 0; i < nodeList.length; i++) {
-    if (element === nodeList[i]) {
-      return i;
-    }
-  }
-  return -1;
-}
-
 /**
  * Make a debuggee value for the given object, if needed. Primitive values
  * are left the same.
  *
  * Use case: you have a raw JS object (after unsafe dereference) and you want to
  * send it to the client. In that case you need to use an ObjectActor which
  * requires a debuggee value. The Debugger.Object.prototype.makeDebuggeeValue()
  * method works only for JS objects and functions.
--- a/toolkit/devtools/server/tests/mochitest/chrome.ini
+++ b/toolkit/devtools/server/tests/mochitest/chrome.ini
@@ -12,16 +12,17 @@ support-files =
   Debugger.Source.prototype.element-2.js
   Debugger.Source.prototype.element.html
 
 [test_Debugger.Source.prototype.introductionScript.html]
 [test_Debugger.Source.prototype.introductionType.html]
 [test_Debugger.Source.prototype.element.html]
 [test_Debugger.Script.prototype.global.html]
 [test_connection-manager.html]
+[test_css-logic.html]
 [test_device.html]
 [test_inspector-changeattrs.html]
 [test_inspector-changevalue.html]
 [test_inspector-hide.html]
 [test_inspector-insert.html]
 [test_inspector-mutations-attr.html]
 [test_inspector-mutations-childlist.html]
 [test_inspector-mutations-frameload.html]
new file mode 100644
--- /dev/null
+++ b/toolkit/devtools/server/tests/mochitest/test_css-logic.html
@@ -0,0 +1,145 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=
+-->
+<head>
+  <meta charset="utf-8">
+  <title>Test for Bug </title>
+
+  <script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
+  <script type="application/javascript;version=1.8" src="inspector-helpers.js"></script>
+  <script type="application/javascript;version=1.8">
+Cu.import("resource://gre/modules/devtools/Loader.jsm");
+const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
+
+const {CssLogic} = devtools.require("devtools/styleinspector/css-logic");
+Cu.importGlobalProperties(['CSS']);
+
+window.onload = function() {
+  SimpleTest.waitForExplicitFinish();
+  runNextTest();
+}
+
+addTest(function findAllCssSelectors() {
+  var nodes = document.querySelectorAll('*');
+  for (var i = 0; i < nodes.length; i++) {
+    var selector = CssLogic.findCssSelector(nodes[i]);
+    var matches = document.querySelectorAll(selector);
+
+    is(matches.length, 1, 'There is a single match: ' + selector);
+    is(matches[0], nodes[i], 'The selector matches the correct node: ' + selector);
+  }
+
+  runNextTest();
+});
+
+addTest(function findCssSelectorNotContainedInDocument() {
+
+  var unattached = document.createElement("div");
+  unattached.id = "unattached";
+  try {
+    CssLogic.findCssSelector(unattached);
+    ok (false, "Unattached node did not throw")
+  } catch(e) {
+    ok(e, "Unattached node throws an exception");
+  }
+
+  var unattachedChild = document.createElement("div");
+  unattached.appendChild(unattachedChild);
+  try {
+    CssLogic.findCssSelector(unattachedChild);
+    ok (false, "Unattached child node did not throw")
+  } catch(e) {
+    ok(e, "Unattached child node throws an exception");
+  }
+
+  var unattachedBody = document.createElement("body");
+  try {
+    CssLogic.findCssSelector(unattachedBody);
+    ok (false, "Unattached body node did not throw")
+  } catch(e) {
+    ok(e, "Unattached body node throws an exception");
+  }
+
+  runNextTest();
+});
+
+addTest(function findCssSelector() {
+
+  let data = [
+    "#one",
+    "#" + CSS.escape("2"),
+    ".three",
+    "." + CSS.escape("4"),
+    "#find-css-selector > div:nth-child(5)",
+    "#find-css-selector > p:nth-child(6)",
+    ".seven",
+    ".eight",
+    ".nine",
+    ".ten",
+    "div.sameclass:nth-child(11)",
+    "div.sameclass:nth-child(12)",
+    "div.sameclass:nth-child(13)",
+    "#" + CSS.escape("!, \", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \\, ], ^, `, {, |, }, ~"),
+  ];
+
+  let container = document.querySelector("#find-css-selector");
+  is (container.children.length, data.length, "Container has correct number of children.");
+
+  for (let i = 0; i < data.length; i++) {
+    let node = container.children[i];
+    is (CssLogic.findCssSelector(node), data[i], "matched id for index " + (i-1));
+  }
+
+  runNextTest();
+});
+
+addTest(function getBackgroundImageUriFromProperty() {
+
+  let data = [
+    ["background: url(foo.png);", "foo.png"],
+    ["background: url(\"foo.png\") ", "foo.png"],
+    ["background: url('foo.png') ; ", "foo.png"],
+    ["background: foo.png", null],
+    ["background: url()", ""],
+  ];
+
+  for (let i = 0; i < data.length; i++) {
+    let prop = data[i][0];
+    let result = data[i][1];
+    is (CssLogic.getBackgroundImageUriFromProperty(prop), result,
+      "Background image matches for index " + i);
+  }
+
+  runNextTest();
+});
+
+  </script>
+</head>
+<body>
+  <div id="find-css-selector">
+    <div id="one"></div> <!-- Basic ID -->
+    <div id="2"></div> <!-- Escaped ID -->
+    <div class="three"></div> <!-- Basic Class -->
+    <div class="4"></div> <!-- Escaped Class -->
+    <div attr="5"></div>  <!-- Only an attribute -->
+    <p></p> <!-- Nothing unique -->
+    <div class="seven seven"></div> <!-- Two classes with same name -->
+    <div class="eight eight2"></div> <!-- Two classes with different names -->
+
+    <!-- Two elements with the same id - should not use ID -->
+    <div class="nine" id="nine-and-ten"></div>
+    <div class="ten" id="nine-and-ten"></div>
+
+    <!-- Three elements with the same id - should use class and nth-child instead -->
+    <div class="sameclass" id="11-12-13"></div>
+    <div class="sameclass" id="11-12-13"></div>
+    <div class="sameclass" id="11-12-13"></div>
+
+    <!-- Special characters -->
+    <div id="!, &quot;, #, $, %, &amp;, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, `, {, |, }, ~"></div>
+  </div>
+</body>
+</html>
--- a/toolkit/devtools/styleinspector/css-logic.js
+++ b/toolkit/devtools/styleinspector/css-logic.js
@@ -45,16 +45,17 @@ const RX_NOT = /:not\((.*?)\)/g;
 const RX_PSEUDO_CLASS_OR_ELT = /(:[\w-]+\().*?\)/g;
 const RX_CONNECTORS = /\s*[\s>+~]\s*/g;
 const RX_ID = /\s*#\w+\s*/g;
 const RX_CLASS_OR_ATTRIBUTE = /\s*(?:\.\w+|\[.+?\])\s*/g;
 const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.importGlobalProperties(['CSS']);
 
 function CssLogic()
 {
   // The cache of examined CSS properties.
   _propertyInfos: {};
 }
 
 exports.CssLogic = CssLogic;
@@ -860,42 +861,43 @@ function positionInNodeList(element, nod
 
 /**
  * Find a unique CSS selector for a given element
  * @returns a string such that ele.ownerDocument.querySelector(reply) === ele
  * and ele.ownerDocument.querySelectorAll(reply).length === 1
  */
 CssLogic.findCssSelector = function CssLogic_findCssSelector(ele) {
   var document = ele.ownerDocument;
-  if (ele.id && document.getElementById(ele.id) === ele) {
-    return '#' + ele.id;
+  if (!document.contains(ele)) {
+    throw new Error('findCssSelector received element not inside document');
+  }
+
+  // document.querySelectorAll("#id") returns multiple if elements share an ID
+  if (ele.id && document.querySelectorAll('#' + CSS.escape(ele.id)).length === 1) {
+    return '#' + CSS.escape(ele.id);
   }
 
   // Inherently unique by tag name
-  var tagName = ele.tagName.toLowerCase();
+  var tagName = ele.localName;
   if (tagName === 'html') {
     return 'html';
   }
   if (tagName === 'head') {
     return 'head';
   }
   if (tagName === 'body') {
     return 'body';
   }
 
-  if (ele.parentNode == null) {
-    console.log('danger: ' + tagName);
-  }
-
   // We might be able to find a unique class name
   var selector, index, matches;
   if (ele.classList.length > 0) {
     for (var i = 0; i < ele.classList.length; i++) {
       // Is this className unique by itself?
-      selector = '.' + ele.classList.item(i);
+      selector = '.' + CSS.escape(ele.classList.item(i));
       matches = document.querySelectorAll(selector);
       if (matches.length === 1) {
         return selector;
       }
       // Maybe it's unique with a tag name?
       selector = tagName + selector;
       matches = document.querySelectorAll(selector);
       if (matches.length === 1) {