Bug 1006231 - Get original source content for a stylesheet from source map's 'sourcesContent'. r=fitzgen, a=sledru
authorHeather Arthur <fayearthur@gmail.com>
Thu, 08 May 2014 15:25:00 +0200
changeset 199215 32b62ac2705dd851ebd8404485bce7fd072d9ac5
parent 199214 7edb60d00b59c9f2074c1c3481b9ce2e6bcc863f
child 199216 d7fde0258fb25a1a02003152bbf4137e19a314e5
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)
reviewersfitzgen, sledru
bugs1006231
milestone31.0a2
Bug 1006231 - Get original source content for a stylesheet from source map's 'sourcesContent'. r=fitzgen, a=sledru
browser/devtools/styleeditor/test/browser.ini
browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js
browser/devtools/styleeditor/test/browser_styleeditor_sourcemaps.js
browser/devtools/styleeditor/test/head.js
browser/devtools/styleeditor/test/sourcemap-css/contained.css
browser/devtools/styleeditor/test/sourcemaps.html
toolkit/devtools/server/actors/stylesheets.js
--- a/browser/devtools/styleeditor/test/browser.ini
+++ b/browser/devtools/styleeditor/test/browser.ini
@@ -20,16 +20,17 @@ support-files =
   resources_inpage.jsi
   resources_inpage1.css
   resources_inpage2.css
   simple.css
   simple.css.gz
   simple.css.gz^headers^
   simple.gz.html
   simple.html
+  sourcemap-css/contained.css
   sourcemap-css/sourcemaps.css
   sourcemap-css/sourcemaps.css.map
   sourcemap-sass/sourcemaps.scss
   sourcemaps.html
   test_private.css
   test_private.html
 
 [browser_styleeditor_autocomplete.js]
--- a/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sourcemap_watching.js
@@ -3,19 +3,21 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Components.utils.import("resource://gre/modules/Task.jsm");
 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 const TESTCASE_URI_HTML = TEST_BASE + "sourcemaps.html";
 const TESTCASE_URI_CSS = TEST_BASE + "sourcemap-css/sourcemaps.css";
+const TESTCASE_URI_CSS2 = TEST_BASE + "sourcemap-css/contained.css";
 const TESTCASE_URI_REG_CSS = TEST_BASE + "simple.css";
 const TESTCASE_URI_SCSS = TEST_BASE + "sourcemap-sass/sourcemaps.scss";
 const TESTCASE_URI_MAP = TEST_BASE + "sourcemap-css/sourcemaps.css.map";
+const TESTCASE_SCSS_NAME = "sourcemaps.scss";
 
 const PREF = "devtools.styleeditor.source-maps-enabled";
 
 const CSS_TEXT = "* { color: blue }";
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 
@@ -30,16 +32,17 @@ function test()
   waitForExplicitFinish();
 
   Services.prefs.setBoolPref(PREF, true);
 
   Task.spawn(function() {
     // copy all our files over so we don't screw them up for other tests
     let HTMLFile = yield copy(TESTCASE_URI_HTML, ["sourcemaps.html"]);
     let CSSFile = yield copy(TESTCASE_URI_CSS, ["sourcemap-css", "sourcemaps.css"]);
+    let CSSFile2 = yield copy(TESTCASE_URI_CSS2, ["sourcemap-css", "contained.css"]);
     yield copy(TESTCASE_URI_SCSS, ["sourcemap-sass", "sourcemaps.scss"]);
     yield copy(TESTCASE_URI_MAP, ["sourcemap-css", "sourcemaps.css.map"]);
     yield copy(TESTCASE_URI_REG_CSS, ["simple.css"]);
 
     let uri = Services.io.newFileURI(HTMLFile);
     let testcaseURI = uri.resolve("");
 
     let editor = yield openEditor(testcaseURI);
@@ -66,25 +69,29 @@ function test()
 
     info("wrote to CSS file");
   })
 }
 
 function openEditor(testcaseURI) {
   let deferred = promise.defer();
 
-  addTabAndOpenStyleEditors(3, panel => {
+  addTabAndOpenStyleEditors(5, panel => {
     let UI = panel.UI;
 
-    // wait for 3 editors - 1 for first style sheet, 1 for the
-    // generated style sheet, and 1 for original source after it
-    // loads and replaces the generated style sheet.
+    // wait for 5 editors - 1 for first style sheet, 2 for the
+    // generated style sheets, and 2 for original source after it
+    // loads and replaces the generated style sheets.
     let editor = UI.editors[1];
+    if (getStylesheetNameFor(editor) != TESTCASE_SCSS_NAME) {
+      editor = UI.editors[2];
+    }
+    is(getStylesheetNameFor(editor), TESTCASE_SCSS_NAME, "found scss editor");
 
-    let link = getStylesheetNameLinkFor(editor);
+    let link = getLinkFor(editor);
     link.click();
 
     editor.getSourceEditor().then(deferred.resolve);
   });
   content.location = testcaseURI;
 
   return deferred.promise;
 }
@@ -120,20 +127,25 @@ function pauseForTimeChange() {
 
 function finishUp() {
   Services.prefs.clearUserPref(PREF);
   finish();
 }
 
 /* Helpers */
 
-function getStylesheetNameLinkFor(editor) {
+function getLinkFor(editor) {
   return editor.summary.querySelector(".stylesheet-name");
 }
 
+function getStylesheetNameFor(editor) {
+  return editor.summary.querySelector(".stylesheet-name > label")
+         .getAttribute("value")
+}
+
 function copy(aSrcChromeURL, aDestFilePath)
 {
   let destFile = FileUtils.getFile("ProfD", aDestFilePath);
   return write(read(aSrcChromeURL), destFile);
 }
 
 function read(aSrcChromeURL)
 {
--- a/browser/devtools/styleeditor/test/browser_styleeditor_sourcemaps.js
+++ b/browser/devtools/styleeditor/test/browser_styleeditor_sourcemaps.js
@@ -1,120 +1,125 @@
 /* vim: set ts=2 et sw=2 tw=80: */
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // https rather than chrome to improve coverage
 const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html";
 const PREF = "devtools.styleeditor.source-maps-enabled";
 
-function test()
-{
-  waitForExplicitFinish();
 
-  Services.prefs.setBoolPref(PREF, true);
-
-  // wait for 3 editors - 1 for first style sheet, 1 for the
-  // generated style sheet, and 1 for original source after it
-  // loads and replaces the generated style sheet.
-  addTabAndOpenStyleEditors(3, panel => runTests(panel.UI));
-
-  content.location = TESTCASE_URI;
+const contents = {
+  "sourcemaps.scss": [
+    "",
+    "$paulrougetpink: #f06;",
+    "",
+    "div {",
+    "  color: $paulrougetpink;",
+    "}",
+    "",
+    "span {",
+    "  background-color: #EEE;",
+    "}"
+  ].join("\n"),
+  "contained.scss": [
+    "$pink: #f06;",
+    "",
+    "#header {",
+    "  color: $pink;",
+    "}"
+  ].join("\n"),
+  "sourcemaps.css": [
+    "div {",
+    "  color: #ff0066; }",
+    "",
+    "span {",
+    "  background-color: #EEE; }",
+    "",
+    "/*# sourceMappingURL=sourcemaps.css.map */"
+  ].join("\n"),
+  "contained.css": [
+    "#header {",
+    "  color: #f06; }",
+    "",
+    "/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJzYXNzL2NvbnRhaW5lZC5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBO0VBQ0UsT0FISyIsInNvdXJjZXNDb250ZW50IjpbIiRwaW5rOiAjZjA2O1xuXG4jaGVhZGVyIHtcbiAgY29sb3I6ICRwaW5rO1xufSJdfQ==*/"
+  ].join("\n")
 }
 
-function runTests(UI)
-{
-  is(UI.editors.length, 2);
+const cssNames = ["sourcemaps.css", "contained.css"];
+const scssNames = ["sourcemaps.scss", "contained.scss"];
 
-  let firstEditor = UI.editors[0];
-  testFirstEditor(firstEditor);
+waitForExplicitFinish();
 
-  let ScssEditor = UI.editors[1];
+let test = asyncTest(function*() {
+  Services.prefs.setBoolPref(PREF, true);
 
-  let link = getStylesheetNameLinkFor(ScssEditor);
-  link.click();
-
-  ScssEditor.getSourceEditor().then(() => {
-    testScssEditor(ScssEditor);
+  let {UI} = yield addTabAndOpenStyleEditors(5, null, TESTCASE_URI);
 
-    togglePref(UI);
-  });
-}
+  is(UI.editors.length, 3,
+    "correct number of editors with source maps enabled");
 
-function togglePref(UI) {
-  let count = 0;
+  // Test first plain css editor
+  testFirstEditor(UI.editors[0]);
 
-  UI.on("editor-added", (event, editor) => {
-    if (++count == 2) {
-      testTogglingPref(UI);
-    }
-  })
+  // Test Scss editors
+  yield testEditor(UI.editors[1], scssNames);
+  yield testEditor(UI.editors[2], scssNames);
 
-  Services.prefs.setBoolPref(PREF, false);
-}
-
-function testTogglingPref(UI) {
-  is(UI.editors.length, 2, "correct number of editors after pref toggled");
+  // Test disabling original sources
+  yield togglePref(UI);
 
-  let CSSEditor = UI.editors[1];
-
-  let link = getStylesheetNameLinkFor(CSSEditor);
-  link.click();
+  is(UI.editors.length, 3, "correct number of editors after pref toggled");
 
-  CSSEditor.getSourceEditor().then(() => {
-    testCSSEditor(CSSEditor);
+  // Test CSS editors
+  yield testEditor(UI.editors[1], cssNames);
+  yield testEditor(UI.editors[2], cssNames);
 
-    finishUp();
-  })
-}
+  Services.prefs.clearUserPref(PREF);
+});
 
 function testFirstEditor(editor) {
   let name = getStylesheetNameFor(editor);
   is(name, "simple.css", "First style sheet display name is correct");
 }
 
-function testScssEditor(editor) {
+function testEditor(editor, possibleNames) {
   let name = getStylesheetNameFor(editor);
-  is(name, "sourcemaps.scss", "Original source display name is correct");
-
-  let text = editor.sourceEditor.getText();
+  ok(possibleNames.indexOf(name) >= 0, name + " editor name is correct");
 
-  is(text, "\n\
-$paulrougetpink: #f06;\n\
-\n\
-div {\n\
-  color: $paulrougetpink;\n\
-}\n\
-\n\
-span {\n\
-  background-color: #EEE;\n\
-}", "Original source text is correct");
-}
+  return openEditor(editor).then(() => {
+    let expectedText = contents[name];
 
-function testCSSEditor(editor) {
-  let name = getStylesheetNameFor(editor);
-  is(name, "sourcemaps.css", "CSS source display name is correct");
-
-  let text = editor.sourceEditor.getText();
-
-  is(text, "div {\n\
-  color: #ff0066; }\n\
-\n\
-span {\n\
-  background-color: #EEE; }\n\
-\n\
-/*# sourceMappingURL=sourcemaps.css.map */", "CSS text is correct");
+    let text = editor.sourceEditor.getText();
+    is(text, expectedText, name + " editor contains expected text");
+  });
 }
 
 /* Helpers */
-function getStylesheetNameLinkFor(editor) {
+
+function togglePref(UI) {
+  let deferred = promise.defer();
+  let count = 0;
+
+  UI.on("editor-added", (event, editor) => {
+    if (++count == 3) {
+      deferred.resolve();
+    }
+  })
+
+  Services.prefs.setBoolPref(PREF, false);
+  return deferred.promise;
+}
+
+function openEditor(editor) {
+  getLinkFor(editor).click();
+
+  return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
   return editor.summary.querySelector(".stylesheet-name");
 }
 
 function getStylesheetNameFor(editor) {
   return editor.summary.querySelector(".stylesheet-name > label")
          .getAttribute("value")
 }
-
-function finishUp() {
-  Services.prefs.clearUserPref(PREF);
-  finish();
-}
\ No newline at end of file
--- a/browser/devtools/styleeditor/test/head.js
+++ b/browser/devtools/styleeditor/test/head.js
@@ -1,56 +1,70 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 const TEST_BASE = "chrome://mochitests/content/browser/browser/devtools/styleeditor/test/";
 const TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/styleeditor/test/";
 const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/styleeditor/test/";
 const TEST_HOST = 'mochi.test:8888';
 
-let tempScope = {};
-Cu.import("resource://gre/modules/devtools/Loader.jsm", tempScope);
-let TargetFactory = tempScope.devtools.TargetFactory;
-Cu.import("resource://gre/modules/LoadContextInfo.jsm", tempScope);
-let LoadContextInfo = tempScope.LoadContextInfo;
-Cu.import("resource://gre/modules/devtools/Console.jsm", tempScope);
-let console = tempScope.console;
+let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
+let TargetFactory = devtools.TargetFactory;
+let {LoadContextInfo} = Cu.import("resource://gre/modules/LoadContextInfo.jsm", {});
+let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
+let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
 
 let gPanelWindow;
 let cache = Cc["@mozilla.org/netwerk/cache-storage-service;1"]
               .getService(Ci.nsICacheStorageService);
 
 
 // Import the GCLI test helper
 let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
 Services.scriptloader.loadSubScript(testDir + "../../../commandline/test/helpers.js", this);
 
 gDevTools.testing = true;
 SimpleTest.registerCleanupFunction(() => {
   gDevTools.testing = false;
 });
 
+/**
+ * Define an async test based on a generator function
+ */
+function asyncTest(generator) {
+  return () => Task.spawn(generator).then(null, ok.bind(null, false)).then(finish);
+}
+
 function cleanup()
 {
   gPanelWindow = null;
   while (gBrowser.tabs.length > 1) {
     gBrowser.removeCurrentTab();
   }
 }
 
-function addTabAndOpenStyleEditors(count, callback) {
+function addTabAndOpenStyleEditors(count, callback, uri) {
+  let deferred = promise.defer();
   let currentCount = 0;
   let panel;
   addTabAndCheckOnStyleEditorAdded(p => panel = p, function () {
     currentCount++;
     info(currentCount + " of " + count + " editors opened");
     if (currentCount == count) {
-      callback(panel);
+      if (callback) {
+        callback(panel);
+      }
+      deferred.resolve(panel);
     }
   });
+
+  if (uri) {
+    content.location = uri;
+  }
+  return deferred.promise;
 }
 
 function addTabAndCheckOnStyleEditorAdded(callbackOnce, callbackOnAdded) {
   gBrowser.selectedTab = gBrowser.addTab();
   gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
     gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
     openStyleEditorInWindow(window, function (panel) {
       // Execute the individual callback with the panel argument.
new file mode 100644
--- /dev/null
+++ b/browser/devtools/styleeditor/test/sourcemap-css/contained.css
@@ -0,0 +1,4 @@
+#header {
+  color: #f06; }
+
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJzYXNzL2NvbnRhaW5lZC5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBO0VBQ0UsT0FISyIsInNvdXJjZXNDb250ZW50IjpbIiRwaW5rOiAjZjA2O1xuXG4jaGVhZGVyIHtcbiAgY29sb3I6ICRwaW5rO1xufSJdfQ==*/
\ No newline at end of file
--- a/browser/devtools/styleeditor/test/sourcemaps.html
+++ b/browser/devtools/styleeditor/test/sourcemaps.html
@@ -1,11 +1,12 @@
 <!doctype html>
 <html>
 <head>
   <title>testcase for testing CSS source maps</title>
   <link rel="stylesheet" type="text/css" href="simple.css"/>
   <link rel="stylesheet" type="text/css" href="sourcemap-css/sourcemaps.css?test=1"/>
+  <link rel="stylesheet" type="text/css" href="sourcemap-css/contained.css"
 </head>
 <body>
 	<div>source maps <span>testcase</span></div>
 </body>
 </html>
--- a/toolkit/devtools/server/actors/stylesheets.js
+++ b/toolkit/devtools/server/actors/stylesheets.js
@@ -400,22 +400,23 @@ let StyleSheetActor = protocol.ActorClas
    *        'styleSheetIndex' and 'parentActor' if it's @imported
    */
   form: function(detail) {
     if (detail === "actorid") {
       return this.actorID;
     }
 
     let docHref;
-    if (this.rawSheet.ownerNode) {
-      if (this.rawSheet.ownerNode instanceof Ci.nsIDOMHTMLDocument) {
-        docHref = this.rawSheet.ownerNode.location.href;
+    let ownerNode = this.rawSheet.ownerNode;
+    if (ownerNode) {
+      if (ownerNode instanceof Ci.nsIDOMHTMLDocument) {
+        docHref = ownerNode.location.href;
       }
-      if (this.rawSheet.ownerNode.ownerDocument) {
-        docHref = this.rawSheet.ownerNode.ownerDocument.location.href;
+      else if (ownerNode.ownerDocument && ownerNode.ownerDocument.location) {
+        docHref = ownerNode.ownerDocument.location.href;
       }
     }
 
     let form = {
       actor: this.actorID,  // actorID is set when this actor is added to a pool
       href: this.href,
       nodeHref: docHref,
       disabled: this.rawSheet.disabled,
@@ -841,16 +842,21 @@ let OriginalSourceActor = protocol.Actor
       relatedStyleSheet: this.parentActor.form()
     };
   },
 
   _getText: function() {
     if (this.text) {
       return promise.resolve(this.text);
     }
+    let content = this.sourceMap.sourceContentFor(this.url);
+    if (content) {
+      this.text = content;
+      return promise.resolve(content);
+    }
     return fetch(this.url, { window: this.window }).then(({content}) => {
       this.text = content;
       return content;
     });
   },
 
   /**
    * Protocol method to get the text of this source.