Bug 755388 - "hud is null" when using the edit gcli command; r=dcamp
authorJoe Walker <jwalker@mozilla.com>
Fri, 01 Jun 2012 08:24:19 +0100
changeset 97656 3e0e827bd2364b9d45172f7de13a6bb665e0409e
parent 97655 66f127caa6fae9e3983f29680f6d64d85ca7cf28
child 97657 68600a8af079a8a3ebbddd09880c3766b7f1d911
push idunknown
push userunknown
push dateunknown
reviewersdcamp
bugs755388
milestone15.0a1
Bug 755388 - "hud is null" when using the edit gcli command; r=dcamp
browser/devtools/commandline/GcliCommands.jsm
browser/devtools/commandline/gcli.jsm
browser/devtools/commandline/test/Makefile.in
browser/devtools/commandline/test/browser_gcli_edit.js
browser/devtools/commandline/test/browser_gcli_pref.js
browser/devtools/commandline/test/head.js
browser/devtools/commandline/test/resources.html
browser/devtools/commandline/test/resources_inpage.js
browser/devtools/commandline/test/resources_inpage1.css
browser/devtools/commandline/test/resources_inpage2.css
--- a/browser/devtools/commandline/GcliCommands.jsm
+++ b/browser/devtools/commandline/GcliCommands.jsm
@@ -130,19 +130,18 @@ gcli.addCommand({
          name: "number",
          min: 1,
          step: 10
        },
        description: gcli.lookup("editLineToJumpToDesc")
      }
    ],
    exec: function(args, context) {
-     let hud = HUDService.getHudReferenceById(context.environment.hudId);
-     let StyleEditor = hud.gcliterm.document.defaultView.StyleEditor;
-     StyleEditor.openChrome(args.resource.element, args.line);
+     let win = HUDService.currentContext();
+     win.StyleEditor.openChrome(args.resource.element, args.line);
    }
 });
 
 /**
  * 'break' command
  */
 gcli.addCommand({
   name: "break",
--- a/browser/devtools/commandline/gcli.jsm
+++ b/browser/devtools/commandline/gcli.jsm
@@ -4152,17 +4152,17 @@ var ResourceCache = {
   add: function(node, resource) {
     ResourceCache._cached.push({ node: node, resource: resource });
   },
 
   /**
    * Drop all cache entries. Helpful to prevent memory leaks
    */
   clear: function() {
-    ResourceCache._cached = {};
+    ResourceCache._cached = [];
   }
 };
 
 
 });
 /*
  * Copyright 2009-2011 Mozilla Foundation and contributors
  * Licensed under the New BSD license. See LICENSE.txt or:
--- a/browser/devtools/commandline/test/Makefile.in
+++ b/browser/devtools/commandline/test/Makefile.in
@@ -10,26 +10,31 @@ VPATH     = @srcdir@
 relativesrcdir  = browser/devtools/commandline/test
 
 include $(DEPTH)/config/autoconf.mk
 include $(topsrcdir)/config/rules.mk
 
 _BROWSER_TEST_FILES = \
   browser_gcli_break.js \
   browser_gcli_commands.js \
+  browser_gcli_edit.js \
   browser_gcli_inspect.js \
   browser_gcli_integrate.js \
   browser_gcli_pref.js \
   browser_gcli_settings.js \
   browser_gcli_web.js \
   head.js \
   $(NULL)
 
 _BROWSER_TEST_PAGES = \
   browser_gcli_break.html \
   browser_gcli_inspect.html \
+  resources_inpage.js \
+  resources_inpage1.css \
+  resources_inpage2.css \
+  resources.html \
   $(NULL)
 
 libs:: $(_BROWSER_TEST_FILES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
 
 libs:: $(_BROWSER_TEST_PAGES)
 	$(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/browser/$(relativesrcdir)
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/browser_gcli_edit.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that the edit command works
+
+const TEST_URI = TEST_BASE_HTTP + "resources.html";
+
+function test() {
+  DeveloperToolbarTest.test(TEST_URI, function(browser, tab) {
+    testEditStatus(browser, tab);
+    // Bug 759853
+    // testEditExec(browser, tab); // calls finish()
+    finish();
+  });
+}
+
+function testEditStatus(browser, tab) {
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit",
+    markup: "VVVV",
+    status: "ERROR",
+    emptyParameters: [ " <resource>", " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit i",
+    markup: "VVVVVI",
+    status: "ERROR",
+    directTabText: "nline-css",
+    emptyParameters: [ " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit c",
+    markup: "VVVVVI",
+    status: "ERROR",
+    directTabText: "ss#style2",
+    emptyParameters: [ " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit http",
+    markup: "VVVVVIIII",
+    status: "ERROR",
+    directTabText: "://example.com/browser/browser/devtools/commandline/test/resources_inpage1.css",
+    arrowTabText: "",
+    emptyParameters: [ " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit page1",
+    markup: "VVVVVIIIII",
+    status: "ERROR",
+    directTabText: "",
+    arrowTabText: "http://example.com/browser/browser/devtools/commandline/test/resources_inpage1.css",
+    emptyParameters: [ " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit page2",
+    markup: "VVVVVIIIII",
+    status: "ERROR",
+    directTabText: "",
+    arrowTabText: "http://example.com/browser/browser/devtools/commandline/test/resources_inpage2.css",
+    emptyParameters: [ " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit stylez",
+    markup: "VVVVVEEEEEE",
+    status: "ERROR",
+    directTabText: "",
+    arrowTabText: "",
+    emptyParameters: [ " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit css#style2",
+    markup: "VVVVVVVVVVVVVVV",
+    status: "VALID",
+    directTabText: "",
+    emptyParameters: [ " [line]" ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit css#style2 5",
+    markup: "VVVVVVVVVVVVVVVVV",
+    status: "VALID",
+    directTabText: "",
+    emptyParameters: [ ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit css#style2 0",
+    markup: "VVVVVVVVVVVVVVVVE",
+    status: "ERROR",
+    directTabText: "",
+    emptyParameters: [ ],
+  });
+
+  DeveloperToolbarTest.checkInputStatus({
+    typed:  "edit css#style2 -1",
+    markup: "VVVVVVVVVVVVVVVVEE",
+    status: "ERROR",
+    directTabText: "",
+    emptyParameters: [ ],
+  });
+}
+
+var windowListener = {
+  onOpenWindow: function(win) {
+    // Wait for the window to finish loading
+    let win = win.QueryInterface(Ci.nsIInterfaceRequestor)
+            .getInterface(Ci.nsIDOMWindowInternal || Ci.nsIDOMWindow);
+    win.addEventListener("load", function onLoad() {
+      win.removeEventListener("load", onLoad, false);
+      win.close();
+    }, false);
+    win.addEventListener("unload", function onUnload() {
+      win.removeEventListener("unload", onUnload, false);
+      Services.wm.removeListener(windowListener);
+      finish();
+    }, false);
+  },
+  onCloseWindow: function(win) { },
+  onWindowTitleChange: function(win, title) { }
+};
+
+function testEditExec(browser, tab) {
+
+  Services.wm.addListener(windowListener);
+
+  var style2 = browser.contentDocument.getElementById("style2");
+  DeveloperToolbarTest.exec({
+    typed: "edit css#style2",
+    args: {
+      resource: function(resource) {
+        return resource.element.ownerNode == style2;
+      },
+      line: 1
+    },
+    completed: true,
+    blankOutput: true,
+  });
+}
old mode 100755
new mode 100644
--- a/browser/devtools/commandline/test/browser_gcli_pref.js
+++ b/browser/devtools/commandline/test/browser_gcli_pref.js
@@ -90,25 +90,25 @@ function testPrefStatus() {
     typed:  "pref show tempTBo",
     markup: "VVVVVVVVVVEEEEEEE",
     status: "ERROR",
     emptyParameters: [ ]
   });
 
   DeveloperToolbarTest.checkInputStatus({
     typed:  "pref show devtools.toolbar.ena",
-    markup: "VVVVVVVVVVVVVVVVVVVVVVVVVVVVVV",
+    markup: "VVVVVVVVVVIIIIIIIIIIIIIIIIIIII",
     directTabText: "bled",
     status: "ERROR",
     emptyParameters: [ ]
   });
 
   DeveloperToolbarTest.checkInputStatus({
     typed:  "pref show hideIntro",
-    markup: "VVVVVVVVVVVVVVVVVVV",
+    markup: "VVVVVVVVVVIIIIIIIII",
     directTabText: "",
     arrowTabText: "devtools.gcli.hideIntro",
     status: "ERROR",
     emptyParameters: [ ]
   });
 
   DeveloperToolbarTest.checkInputStatus({
     typed:  "pref show devtools.toolbar.enabled",
--- a/browser/devtools/commandline/test/head.js
+++ b/browser/devtools/commandline/test/head.js
@@ -1,12 +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/. */
 
+const TEST_BASE_HTTP = "http://example.com/browser/browser/devtools/commandline/test/";
+const TEST_BASE_HTTPS = "https://example.com/browser/browser/devtools/commandline/test/";
+
 let console = (function() {
   let tempScope = {};
   Components.utils.import("resource:///modules/devtools/Console.jsm", tempScope);
   return tempScope.console;
 })();
 
 /**
  * Open a new tab at a URL and call a callback on load
@@ -78,67 +81,79 @@ let DeveloperToolbarTest = {
    *   typed: "ech",           // Required
    *   cursor: 3,              // Optional cursor position
    *
    *   // Thing to check
    *   status: "INCOMPLETE",   // One of "VALID", "ERROR", "INCOMPLETE"
    *   emptyParameters: [ "<message>" ], // Still to type
    *   directTabText: "o",     // Simple completion text
    *   arrowTabText: "",       // When the completion is not an extension
+   *   markup: "VVVIIIEEE",    // What state should the error markup be in
    * });
    */
-  checkInputStatus: function DTT_checkInputStatus(test) {
-    if (test.typed) {
-      DeveloperToolbar.display.inputter.setInput(test.typed);
+  checkInputStatus: function DTT_checkInputStatus(tests) {
+    let display = DeveloperToolbar.display;
+
+    if (tests.typed) {
+      display.inputter.setInput(tests.typed);
     }
     else {
-     ok(false, "Missing typed for " + JSON.stringify(test));
-     return;
+      ok(false, "Missing typed for " + JSON.stringify(tests));
+      return;
     }
 
-    if (test.cursor) {
-      DeveloperToolbar.display.inputter.setCursor(test.cursor)
+    if (tests.cursor) {
+      display.inputter.setCursor(tests.cursor)
     }
 
-    if (test.status) {
-      is(DeveloperToolbar.display.requisition.getStatus().toString(),
-         test.status,
-         "status for " + test.typed);
+    if (tests.status) {
+      is(display.requisition.getStatus().toString(),
+              tests.status, "status for " + tests.typed);
     }
 
-    if (test.emptyParameters == null) {
-      test.emptyParameters = [];
+    if (tests.emptyParameters == null) {
+      tests.emptyParameters = [];
     }
 
-    let completer = DeveloperToolbar.display.completer;
-    let realParams = completer.emptyParameters;
-    is(realParams.length, test.emptyParameters.length,
-       'emptyParameters.length for \'' + test.typed + '\'');
+    let realParams = display.completer.emptyParameters;
+    is(realParams.length, tests.emptyParameters.length,
+            'emptyParameters.length for \'' + tests.typed + '\'');
 
-    if (realParams.length === test.emptyParameters.length) {
+    if (realParams.length === tests.emptyParameters.length) {
       for (let i = 0; i < realParams.length; i++) {
-        is(realParams[i].replace(/\u00a0/g, ' '), test.emptyParameters[i],
-           'emptyParameters[' + i + '] for \'' + test.typed + '\'');
+        is(realParams[i].replace(/\u00a0/g, ' '), tests.emptyParameters[i],
+                'emptyParameters[' + i + '] for \'' + tests.typed + '\'');
       }
     }
 
-    if (test.directTabText) {
-      is(completer.directTabText, test.directTabText,
-         'directTabText for \'' + test.typed + '\'');
+    if (tests.directTabText) {
+      is(display.completer.directTabText, tests.directTabText,
+              'directTabText for \'' + tests.typed + '\'');
     }
     else {
-      is(completer.directTabText, '', 'directTabText for \'' + test.typed + '\'');
+      is(display.completer.directTabText, '',
+              'directTabText for \'' + tests.typed + '\'');
     }
 
-    if (test.arrowTabText) {
-      is(completer.arrowTabText, ' \u00a0\u21E5 ' + test.arrowTabText,
-         'arrowTabText for \'' + test.typed + '\'');
+    if (tests.arrowTabText) {
+      is(display.completer.arrowTabText, ' \u00a0\u21E5 ' + tests.arrowTabText,
+              'arrowTabText for \'' + tests.typed + '\'');
     }
     else {
-      is(completer.arrowTabText, '', 'arrowTabText for \'' + test.typed + '\'');
+      is(display.completer.arrowTabText, '',
+              'arrowTabText for \'' + tests.typed + '\'');
+    }
+
+    if (tests.markup) {
+      let cursor = tests.cursor ? tests.cursor.start : tests.typed.length;
+      let statusMarkup = display.requisition.getInputStatusMarkup(cursor);
+      let actualMarkup = statusMarkup.map(function(s) {
+        return Array(s.string.length + 1).join(s.status.toString()[0]);
+      }).join('');
+      is(tests.markup, actualMarkup, 'markup for ' + tests.typed);
     }
   },
 
   /**
    * Execute a command:
    *
    * DeveloperToolbarTest.exec({
    *   // Test inputs
@@ -146,86 +161,91 @@ let DeveloperToolbarTest = {
    *
    *   // Thing to check
    *   args: { message: "hi" }, // Check that the args were understood properly
    *   outputMatch: /^hi$/,     // RegExp to test against textContent of output
    *                            // (can also be array of RegExps)
    *   blankOutput: true,       // Special checks when there is no output
    * });
    */
-  exec: function DTT_exec(test) {
-    test = test || {};
+  exec: function DTT_exec(tests) {
+    tests = tests || {};
 
-    if (test.typed) {
-      DeveloperToolbar.display.inputter.setInput(test.typed);
+    if (tests.typed) {
+      DeveloperToolbar.display.inputter.setInput(tests.typed);
     }
 
     let typed = DeveloperToolbar.display.inputter.getInputState().typed;
     let output = DeveloperToolbar.display.requisition.exec();
 
     is(typed, output.typed, 'output.command for: ' + typed);
 
-    if (test.completed !== false) {
+    if (tests.completed !== false) {
       ok(output.completed, 'output.completed false for: ' + typed);
     }
     else {
       // It is actually an error if we say something is async and it turns
       // out not to be? For now we're saying 'no'
       // ok(!output.completed, 'output.completed true for: ' + typed);
     }
 
-    if (test.args != null) {
-      is(Object.keys(test.args).length, Object.keys(output.args).length,
+    if (tests.args != null) {
+      is(Object.keys(tests.args).length, Object.keys(output.args).length,
          'arg count for ' + typed);
 
       Object.keys(output.args).forEach(function(arg) {
-        let expectedArg = test.args[arg];
+        let expectedArg = tests.args[arg];
         let actualArg = output.args[arg];
 
-        if (Array.isArray(expectedArg)) {
-          if (!Array.isArray(actualArg)) {
-            ok(false, 'actual is not an array. ' + typed + '/' + arg);
-            return;
-          }
-
-          is(expectedArg.length, actualArg.length,
-             'array length: ' + typed + '/' + arg);
-          for (let i = 0; i < expectedArg.length; i++) {
-            is(expectedArg[i], actualArg[i],
-               'member: "' + typed + '/' + arg + '/' + i);
-          }
+        if (typeof expectedArg === 'function') {
+          ok(expectedArg(actualArg), 'failed test func. ' + typed + '/' + arg);
         }
         else {
-          is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg);
+          if (Array.isArray(expectedArg)) {
+            if (!Array.isArray(actualArg)) {
+              ok(false, 'actual is not an array. ' + typed + '/' + arg);
+              return;
+            }
+
+            is(expectedArg.length, actualArg.length,
+                    'array length: ' + typed + '/' + arg);
+            for (let i = 0; i < expectedArg.length; i++) {
+              is(expectedArg[i], actualArg[i],
+                      'member: "' + typed + '/' + arg + '/' + i);
+            }
+          }
+          else {
+            is(expectedArg, actualArg, 'typed: "' + typed + '" arg: ' + arg);
+          }
         }
       });
     }
 
     let displayed = DeveloperToolbar.outputPanel._div.textContent;
 
-    if (test.outputMatch) {
+    if (tests.outputMatch) {
       function doTest(match, against) {
         if (!match.test(against)) {
           ok(false, "html output for " + typed + " against " + match.source +
                   " (textContent sent to info)");
           info("Actual textContent");
           info(against);
         }
       }
-      if (Array.isArray(test.outputMatch)) {
-        test.outputMatch.forEach(function(match) {
+      if (Array.isArray(tests.outputMatch)) {
+        tests.outputMatch.forEach(function(match) {
           doTest(match, displayed);
         });
       }
       else {
-        doTest(test.outputMatch, displayed);
+        doTest(tests.outputMatch, displayed);
       }
     }
 
-    if (test.blankOutput != null) {
+    if (tests.blankOutput != null) {
       if (!/^$/.test(displayed)) {
         ok(false, "html output for " + typed + " (textContent sent to info)");
         info("Actual textContent");
         info(displayed);
       }
     }
   },
 
@@ -255,16 +275,18 @@ let DeveloperToolbarTest = {
         menuItem.hidden = true;
       }
       if (command) {
         command.setAttribute("disabled", "true");
       }
       if (appMenuItem) {
         appMenuItem.hidden = true;
       }
+
+      // leakHunt({ DeveloperToolbar: DeveloperToolbar });
     });
 
     // a.k.a: Services.prefs.setBoolPref("devtools.toolbar.enabled", true);
     if (menuItem) {
       menuItem.hidden = false;
     }
     if (command) {
       command.removeAttribute("disabled");
@@ -284,8 +306,163 @@ let DeveloperToolbarTest = {
           console.error(ex);
           finish();
           throw ex;
         }
       });
     });
   },
 };
+
+
+/**
+ * Memory leak hunter. Walks a tree of objects looking for DOM nodes.
+ * Usage:
+ * leakHunt({
+ *   thing: thing,
+ *   otherthing: otherthing
+ * });
+ */
+
+var noRecurse = [
+  /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/,
+  /^Window$/, /^Document$/,
+  /^XULDocument$/, /^XULElement$/,
+  /^DOMWindow$/, /^HTMLDocument$/, /^HTML.*Element$/
+];
+
+var hide = [ /^string$/, /^number$/, /^boolean$/, /^null/, /^undefined/ ];
+
+function leakHunt(root, path, seen) {
+  path = path || [];
+  seen = seen || [];
+
+  try {
+    var output = leakHuntInner(root, path, seen);
+    output.forEach(function(line) {
+      dump(line + '\n');
+    });
+  }
+  catch (ex) {
+    dump(ex + '\n');
+  }
+}
+
+function leakHuntInner(root, path, seen) {
+  var prefix = new Array(path.length).join('  ');
+
+  var reply = [];
+  function log(msg) {
+    reply.push(msg);
+  }
+
+  var direct
+  try {
+    direct = Object.keys(root);
+  }
+  catch (ex) {
+    log(prefix + '  Error enumerating: ' + ex);
+    return reply;
+  }
+
+  for (var prop in root) {
+    var newPath = path.slice();
+    newPath.push(prop);
+    prefix = new Array(newPath.length).join('  ');
+
+    var data;
+    try {
+      data = root[prop];
+    }
+    catch (ex) {
+      log(prefix + prop + '  Error reading: ' + ex);
+      continue;
+    }
+
+    var recurse = true;
+    var message = getType(data);
+
+    if (matchesAnyPattern(message, hide)) {
+      continue;
+    }
+
+    if (message === 'function' && direct.indexOf(prop) == -1) {
+      continue;
+    }
+
+    if (message === 'string') {
+      var extra = data.length > 10 ? data.substring(0, 9) + '_' : data;
+      message += ' "' + extra.replace(/\n/g, "|") + '"';
+      recurse = false;
+    }
+    else if (matchesAnyPattern(message, noRecurse)) {
+      message += ' (no recurse)'
+      recurse = false;
+    }
+    else if (seen.indexOf(data) !== -1) {
+      message += ' (already seen)';
+      recurse = false;
+    }
+
+    if (recurse) {
+      seen.push(data);
+      var lines = leakHuntInner(data, newPath, seen);
+      if (lines.length == 0) {
+        if (message !== 'function') {
+          log(prefix + prop + ' = ' + message + ' { }');
+        }
+      }
+      else {
+        log(prefix + prop + ' = ' + message + ' {');
+        lines.forEach(function(line) {
+          reply.push(line);
+        });
+        log(prefix + '}');
+      }
+    }
+    else {
+      log(prefix + prop + ' = ' + message);
+    }
+  }
+
+  return reply;
+}
+
+function matchesAnyPattern(str, patterns) {
+  var match = false;
+  patterns.forEach(function(pattern) {
+    if (str.match(pattern)) {
+      match = true;
+    }
+  });
+  return match;
+}
+
+function getType(data) {
+  if (data === null) {
+    return 'null';
+  }
+  if (data === undefined) {
+    return 'undefined';
+  }
+
+  var type = typeof data;
+  if (type === 'object' || type === 'Object') {
+    type = getCtorName(data);
+  }
+
+  return type;
+}
+
+function getCtorName(aObj) {
+  try {
+    if (aObj.constructor && aObj.constructor.name) {
+      return aObj.constructor.name;
+    }
+  }
+  catch (ex) {
+    return 'UnknownObject';
+  }
+
+  // If that fails, use Objects toString which sometimes gives something
+  // better than 'Object', and at least defaults to Object if nothing better
+  return Object.prototype.toString.call(aObj).slice(8, -1);
+}
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/resources.html
@@ -0,0 +1,50 @@
+<!DOCTYPE html>
+<html>
+<!-- Any copyright is dedicated to the Public Domain.
+     http://creativecommons.org/publicdomain/zero/1.0/ -->
+<head>
+  <meta charset="utf-8">
+  <title>Resources</title>
+  <script type="text/javascript" id="script1">
+    window.addEventListener('load', function() {
+      var pid = document.getElementById('pid');
+      var div = document.createElement('div');
+      div.id = 'divid';
+      div.classList.add('divclass');
+      div.appendChild(document.createTextNode('div'));
+      div.setAttribute('data-a1', 'div');
+      pid.parentNode.appendChild(div);
+    });
+  </script>
+  <script src="resources_inpage.js"></script>
+  <link rel="stylesheet" type="text/css" href="resources_inpage1.css"/>
+  <link rel="stylesheet" type="text/css" href="resources_inpage2.css"/>
+  <style type="text/css">
+    p { color: #800; }
+    div { color: #008; }
+    h4 { color: #080; }
+    h3 { color: #880; }
+  </style>
+</head>
+<body>
+  <style type="text/css" id=style2>
+    .pclass { background-color: #FEE; }
+    .divclass { background-color: #EEF; }
+    .h4class { background-color: #EFE; }
+    .h3class { background-color: #FFE; }
+  </style>
+
+  <p class="pclass" id="pid" data-a1="p">paragraph</p>
+
+  <script>
+    var pid = document.getElementById('pid');
+    var h4 = document.createElement('h4');
+    h4.id = 'h4id';
+    h4.classList.add('h4class');
+    h4.appendChild(document.createTextNode('h4'));
+    h4.setAttribute('data-a1', 'h4');
+    pid.parentNode.appendChild(h4);
+  </script>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/resources_inpage.js
@@ -0,0 +1,12 @@
+
+// This script is used from within browser_gcli_edit.html
+
+window.addEventListener('load', function() {
+  var pid = document.getElementById('pid');
+  var h3 = document.createElement('h3');
+  h3.id = 'h3id';
+  h3.classList.add('h3class');
+  h3.appendChild(document.createTextNode('h3'));
+  h3.setAttribute('data-a1', 'h3');
+  pid.parentNode.appendChild(h3);
+});
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/resources_inpage1.css
@@ -0,0 +1,11 @@
+@charset "utf-8";
+
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#pid   { border-top: 2px dotted #F00; }
+#divid { border-top: 2px dotted #00F; }
+#h4id  { border-top: 2px dotted #0F0; }
+#h3id  { border-top: 2px dotted #FF0; }
new file mode 100644
--- /dev/null
+++ b/browser/devtools/commandline/test/resources_inpage2.css
@@ -0,0 +1,11 @@
+@charset "utf-8";
+
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+*[data-a1=p]   { border-left: 4px solid #F00; }
+*[data-a1=div] { border-left: 4px solid #00F; }
+*[data-a1=h4]  { border-left: 4px solid #0F0; }
+*[data-a1=h3]  { border-left: 4px solid #FF0; }