Bug 751836 - After stepping in the debugger, any open nodes in the property view are collapsed, r=past

This commit is contained in:
Victor Porof 2012-10-26 23:28:54 +03:00
parent 9e4e19456d
commit 6bb97610d2
9 changed files with 471 additions and 7 deletions

View File

@ -5,6 +5,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const LAZY_EMPTY_DELAY = 150; // ms
Components.utils.import('resource://gre/modules/Services.jsm');
let EXPORTED_SYMBOLS = ["VariablesView", "create"];
@ -46,13 +48,28 @@ VariablesView.prototype = {
let scope = new Scope(this, aName);
this._store.set(scope.id, scope);
this._currHierarchy.set(aName, scope);
return scope;
},
/**
* Removes all items from this container.
*
* @param number aTimeout [optional]
* The number of milliseconds to delay the operation if
* lazy emptying of this container is enabled.
*/
empty: function VV_empty() {
empty: function VV_empty(aTimeout = LAZY_EMPTY_DELAY) {
// If there are no items in this container, emptying is useless.
if (!this._store.size()) {
return;
}
// Check if this empty operation may be executed lazily.
if (this.lazyEmpty && aTimeout > 0) {
this._emptySoon(aTimeout);
return;
}
let list = this._list;
let firstChild;
@ -64,6 +81,41 @@ VariablesView.prototype = {
this._appendEmptyNotice();
},
/**
* Emptying this container and rebuilding it immediately afterwards would
* result in a brief redraw flicker, because the previously expanded nodes
* may get asynchronously re-expanded, after fetching the prototype and
* properties from a server.
*
* To avoid such behaviour, a normal container list is rebuild, but not
* immediately attached to the parent container. The old container list
* is kept around for a short period of time, hopefully accounting for the
* data fetching delay. In the meantime, any operations can be executed
* normally.
*
* @see VariablesView.empty
* @see VariablesView.commitHierarchy
*/
_emptySoon: function VV__emptySoon(aTimeout) {
let window = this.window;
let document = this.document;
let prevList = this._list;
let currList = this._list = this.document.createElement("vbox");
this._store = new Map();
this._emptyTimeout = window.setTimeout(function() {
this._emptyTimeout = null;
this._parent.removeChild(prevList);
this._parent.appendChild(currList);
if (!this._store.size()) {
this._appendEmptyNotice();
}
}.bind(this), aTimeout);
},
/**
* Specifies if enumerable properties and variables should be displayed.
* @param boolean aFlag
@ -146,9 +198,11 @@ VariablesView.prototype = {
get window() this.document.defaultView,
eval: null,
lazyEmpty: false,
_store: null,
_prevHierarchy: null,
_currHierarchy: null,
_emptyTimeout: null,
_enumVisible: true,
_nonEnumVisible: true,
_list: null,
@ -702,7 +756,7 @@ create({ constructor: Variable, proto: Scope.prototype }, {
if (aDescriptor.get || aDescriptor.set) {
this.addProperty("get ", { value: aDescriptor.get });
this.addProperty("set ", { value: aDescriptor.set });
this.expand();
this.expand(true);
separatorLabel.hidden = true;
valueLabel.hidden = true;
}
@ -949,7 +1003,7 @@ VariablesView.prototype.createHierarchy = function VV_createHierarchy() {
/**
* Briefly flash the variables that changed between the previous and current
* scope/variable/property hierarchies.
* scope/variable/property hierarchies and reopen previously expanded nodes.
*/
VariablesView.prototype.commitHierarchy = function VV_commitHierarchy() {
let prevHierarchy = this._prevHierarchy;
@ -972,6 +1026,11 @@ VariablesView.prototype.commitHierarchy = function VV_commitHierarchy() {
let prevString = prevVariable._valueString;
let currString = currVariable._valueString;
changed = prevString != currString;
// Re-expand the variable if not previously collapsed.
if (prevVariable.expanded) {
currVariable.expand(true);
}
}
// Make sure this variable is not handled in ulteror commits for the
@ -986,14 +1045,14 @@ VariablesView.prototype.commitHierarchy = function VV_commitHierarchy() {
// Apply an attribute determining the flash type and duration.
// Dispatch this action after all the nodes have been drawn, so that
// the transition efects can take place.
Services.tm.currentThread.dispatch({ run: function(aTarget) {
this.window.setTimeout(function(aTarget) {
aTarget.setAttribute("changed", "");
aTarget.addEventListener("transitionend", function onEvent() {
aTarget.removeEventListener("transitionend", onEvent, false);
aTarget.removeAttribute("changed");
}, false);
}.bind(this, currVariable.target)}, 0);
}.bind(this, currVariable.target), LAZY_EMPTY_DELAY + 1);
}
};

View File

@ -405,7 +405,6 @@ StackFrames.prototype = {
return;
}
DebuggerView.StackFrames.empty();
DebuggerView.Variables.empty();
for (let frame of this.activeThread.cachedFrames) {
this._addFrame(frame);
@ -440,7 +439,7 @@ StackFrames.prototype = {
return;
}
DebuggerView.StackFrames.empty();
DebuggerView.Variables.empty();
DebuggerView.Variables.empty(0);
DebuggerView.Breakpoints.unhighlightBreakpoint();
window.dispatchEvent("Debugger:AfterFramesCleared");
},
@ -630,6 +629,10 @@ StackFrames.prototype = {
}
aVar.fetched = true;
// Signal that properties have been fetched.
window.dispatchEvent("Debugger:FetchedProperties");
DebuggerView.Variables.commitHierarchy();
}.bind(this));
},

View File

@ -40,6 +40,7 @@ let DebuggerView = {
this.Variables.emptyText = L10N.getStr("emptyVariablesText");
this.Variables.nonEnumVisible = Prefs.nonEnumVisible;
this.Variables.eval = DebuggerController.StackFrames.evaluate;
this.Variables.lazyEmpty = true;
this._initializePanes();
this._initializeEditor(aCallback)

View File

@ -33,6 +33,7 @@ MOCHITEST_BROWSER_TESTS = \
browser_dbg_propertyview-09.js \
browser_dbg_propertyview-10.js \
browser_dbg_propertyview-edit.js \
browser_dbg_propertyview-reexpand.js \
browser_dbg_reload-same-script.js \
browser_dbg_pane-collapse.js \
browser_dbg_panesize.js \

View File

@ -0,0 +1,365 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Make sure that the property view correctly re-expands nodes after pauses.
*/
const TAB_URL = EXAMPLE_URL + "browser_dbg_with-frame.html";
var gPane = null;
var gTab = null;
var gDebugger = null;
var gDebuggee = null;
function test()
{
debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gPane = aPane;
gDebugger = gPane.contentWindow;
gDebuggee = aDebuggee;
addBreakpoint();
});
}
function addBreakpoint()
{
gDebugger.DebuggerController.Breakpoints.addBreakpoint({
url: gDebugger.DebuggerView.Sources.selectedValue,
line: 16
}, function() {
// Wait for the resume...
gDebugger.gClient.addOneTimeListener("resumed", function() {
testVariablesExpand();
});
});
}
function testVariablesExpand()
{
let count = 0;
gDebugger.addEventListener("Debugger:FetchedVariables", function test() {
// We expect 4 Debugger:FetchedVariables events, one from the global object
// scope, two from the |with| scopes and the regular one.
if (++count < 4) {
info("Number of received Debugger:FetchedVariables events: " + count);
return;
}
gDebugger.removeEventListener("Debugger:FetchedVariables", test, false);
Services.tm.currentThread.dispatch({ run: function() {
var frames = gDebugger.DebuggerView.StackFrames._container._list,
scopes = gDebugger.DebuggerView.Variables._list,
innerScope = scopes.querySelectorAll(".scope")[0],
mathScope = scopes.querySelectorAll(".scope")[1],
testScope = scopes.querySelectorAll(".scope")[2],
loadScope = scopes.querySelectorAll(".scope")[3],
globalScope = scopes.querySelectorAll(".scope")[4];
let innerScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
innerScope.querySelector(".name").getAttribute("value"));
let mathScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
mathScope.querySelector(".name").getAttribute("value"));
let testScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
testScope.querySelector(".name").getAttribute("value"));
let loadScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
loadScope.querySelector(".name").getAttribute("value"));
let globalScopeItem = gDebugger.DebuggerView.Variables._currHierarchy.get(
globalScope.querySelector(".name").getAttribute("value"));
is(innerScope.querySelector(".arrow").hasAttribute("open"), true,
"The innerScope arrow should initially be expanded");
is(mathScope.querySelector(".arrow").hasAttribute("open"), false,
"The mathScope arrow should initially not be expanded");
is(testScope.querySelector(".arrow").hasAttribute("open"), false,
"The testScope arrow should initially not be expanded");
is(loadScope.querySelector(".arrow").hasAttribute("open"), false,
"The loadScope arrow should initially not be expanded");
is(globalScope.querySelector(".arrow").hasAttribute("open"), false,
"The globalScope arrow should initially not be expanded");
is(innerScope.querySelector(".details").hasAttribute("open"), true,
"The innerScope enumerables should initially be expanded");
is(mathScope.querySelector(".details").hasAttribute("open"), false,
"The mathScope enumerables should initially not be expanded");
is(testScope.querySelector(".details").hasAttribute("open"), false,
"The testScope enumerables should initially not be expanded");
is(loadScope.querySelector(".details").hasAttribute("open"), false,
"The loadScope enumerables should initially not be expanded");
is(globalScope.querySelector(".details").hasAttribute("open"), false,
"The globalScope enumerables should initially not be expanded");
is(innerScopeItem.expanded, true,
"The innerScope expanded getter should return true");
is(mathScopeItem.expanded, false,
"The mathScope expanded getter should return false");
is(testScopeItem.expanded, false,
"The testScope expanded getter should return false");
is(loadScopeItem.expanded, false,
"The loadScope expanded getter should return false");
is(globalScopeItem.expanded, false,
"The globalScope expanded getter should return false");
EventUtils.sendMouseEvent({ type: "mousedown" }, mathScope.querySelector(".arrow"), gDebuggee);
EventUtils.sendMouseEvent({ type: "mousedown" }, testScope.querySelector(".arrow"), gDebuggee);
EventUtils.sendMouseEvent({ type: "mousedown" }, loadScope.querySelector(".arrow"), gDebuggee);
EventUtils.sendMouseEvent({ type: "mousedown" }, globalScope.querySelector(".arrow"), gDebuggee);
is(innerScope.querySelector(".arrow").hasAttribute("open"), true,
"The innerScope arrow should now be expanded");
is(mathScope.querySelector(".arrow").hasAttribute("open"), true,
"The mathScope arrow should now be expanded");
is(testScope.querySelector(".arrow").hasAttribute("open"), true,
"The testScope arrow should now be expanded");
is(loadScope.querySelector(".arrow").hasAttribute("open"), true,
"The loadScope arrow should now be expanded");
is(globalScope.querySelector(".arrow").hasAttribute("open"), true,
"The globalScope arrow should now be expanded");
is(innerScope.querySelector(".details").hasAttribute("open"), true,
"The innerScope enumerables should now be expanded");
is(mathScope.querySelector(".details").hasAttribute("open"), true,
"The mathScope enumerables should now be expanded");
is(testScope.querySelector(".details").hasAttribute("open"), true,
"The testScope enumerables should now be expanded");
is(loadScope.querySelector(".details").hasAttribute("open"), true,
"The loadScope enumerables should now be expanded");
is(globalScope.querySelector(".details").hasAttribute("open"), true,
"The globalScope enumerables should now be expanded");
is(innerScopeItem.expanded, true,
"The innerScope expanded getter should return true");
is(mathScopeItem.expanded, true,
"The mathScope expanded getter should return true");
is(testScopeItem.expanded, true,
"The testScope expanded getter should return true");
is(loadScopeItem.expanded, true,
"The loadScope expanded getter should return true");
is(globalScopeItem.expanded, true,
"The globalScope expanded getter should return true");
let thisItem = innerScopeItem.get("this");
is(thisItem.expanded, false,
"The local scope 'this' should not be expanded yet");
gDebugger.addEventListener("Debugger:FetchedProperties", function test2() {
gDebugger.removeEventListener("Debugger:FetchedProperties", test2, false);
Services.tm.currentThread.dispatch({ run: function() {
let windowItem = thisItem.get("window");
is(windowItem.expanded, false,
"The local scope 'this.window' should not be expanded yet");
gDebugger.addEventListener("Debugger:FetchedProperties", function test3() {
gDebugger.removeEventListener("Debugger:FetchedProperties", test3, false);
Services.tm.currentThread.dispatch({ run: function() {
let documentItem = windowItem.get("document");
is(documentItem.expanded, false,
"The local scope 'this.window.document' should not be expanded yet");
gDebugger.addEventListener("Debugger:FetchedProperties", function test4() {
gDebugger.removeEventListener("Debugger:FetchedProperties", test4, false);
Services.tm.currentThread.dispatch({ run: function() {
let locationItem = documentItem.get("location");
is(locationItem.expanded, false,
"The local scope 'this.window.document.location' should not be expanded yet");
gDebugger.addEventListener("Debugger:FetchedProperties", function test5() {
gDebugger.removeEventListener("Debugger:FetchedProperties", test5, false);
Services.tm.currentThread.dispatch({ run: function() {
is(thisItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The thisItem arrow should still be expanded (1)");
is(windowItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The windowItem arrow should still be expanded (1)");
is(documentItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The documentItem arrow should still be expanded (1)");
is(locationItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The locationItem arrow should still be expanded (1)");
is(thisItem.target.querySelector(".details").hasAttribute("open"), true,
"The thisItem enumerables should still be expanded (1)");
is(windowItem.target.querySelector(".details").hasAttribute("open"), true,
"The windowItem enumerables should still be expanded (1)");
is(documentItem.target.querySelector(".details").hasAttribute("open"), true,
"The documentItem enumerables should still be expanded (1)");
is(locationItem.target.querySelector(".details").hasAttribute("open"), true,
"The locationItem enumerables should still be expanded (1)");
is(thisItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The thisItem non-enumerables should still be expanded (1)");
is(windowItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The windowItem non-enumerables should still be expanded (1)");
is(documentItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The documentItem non-enumerables should still be expanded (1)");
is(locationItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The locationItem non-enumerables should still be expanded (1)");
is(thisItem.expanded, true,
"The local scope 'this' should still be expanded (1)");
is(windowItem.expanded, true,
"The local scope 'this.window' should still be expanded (1)");
is(documentItem.expanded, true,
"The local scope 'this.window.document' should still be expanded (1)");
is(locationItem.expanded, true,
"The local scope 'this.window.document.location' should still be expanded (1)");
let count = 0;
gDebugger.addEventListener("Debugger:FetchedProperties", function test6() {
// We expect 4 Debugger:FetchedProperties events, one from the this
// reference, one for window, one for document and one for location.
if (++count < 4) {
info("Number of received Debugger:FetchedProperties events: " + count);
return;
}
gDebugger.removeEventListener("Debugger:FetchedProperties", test6, false);
Services.tm.currentThread.dispatch({ run: function() {
is(innerScope.querySelector(".arrow").hasAttribute("open"), true,
"The innerScope arrow should still be expanded");
is(mathScope.querySelector(".arrow").hasAttribute("open"), true,
"The mathScope arrow should still be expanded");
is(testScope.querySelector(".arrow").hasAttribute("open"), true,
"The testScope arrow should still be expanded");
is(loadScope.querySelector(".arrow").hasAttribute("open"), true,
"The loadScope arrow should still be expanded");
is(globalScope.querySelector(".arrow").hasAttribute("open"), true,
"The globalScope arrow should still be expanded");
is(innerScope.querySelector(".details").hasAttribute("open"), true,
"The innerScope enumerables should still be expanded");
is(mathScope.querySelector(".details").hasAttribute("open"), true,
"The mathScope enumerables should still be expanded");
is(testScope.querySelector(".details").hasAttribute("open"), true,
"The testScope enumerables should still be expanded");
is(loadScope.querySelector(".details").hasAttribute("open"), true,
"The loadScope enumerables should still be expanded");
is(globalScope.querySelector(".details").hasAttribute("open"), true,
"The globalScope enumerables should still be expanded");
is(innerScopeItem.expanded, true,
"The innerScope expanded getter should return true");
is(mathScopeItem.expanded, true,
"The mathScope expanded getter should return true");
is(testScopeItem.expanded, true,
"The testScope expanded getter should return true");
is(loadScopeItem.expanded, true,
"The loadScope expanded getter should return true");
is(globalScopeItem.expanded, true,
"The globalScope expanded getter should return true");
is(thisItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The thisItem arrow should still be expanded (2)");
is(windowItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The windowItem arrow should still be expanded (2)");
is(documentItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The documentItem arrow should still be expanded (2)");
is(locationItem.target.querySelector(".arrow").hasAttribute("open"), true,
"The locationItem arrow should still be expanded (2)");
is(thisItem.target.querySelector(".details").hasAttribute("open"), true,
"The thisItem enumerables should still be expanded (2)");
is(windowItem.target.querySelector(".details").hasAttribute("open"), true,
"The windowItem enumerables should still be expanded (2)");
is(documentItem.target.querySelector(".details").hasAttribute("open"), true,
"The documentItem enumerables should still be expanded (2)");
is(locationItem.target.querySelector(".details").hasAttribute("open"), true,
"The locationItem enumerables should still be expanded (2)");
is(thisItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The thisItem non-enumerables should still be expanded (2)");
is(windowItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The windowItem non-enumerables should still be expanded (2)");
is(documentItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The documentItem non-enumerables should still be expanded (2)");
is(locationItem.target.querySelector(".details.nonenum").hasAttribute("open"), true,
"The locationItem non-enumerables should still be expanded (2)");
is(thisItem.expanded, true,
"The local scope 'this' should still be expanded (2)");
is(windowItem.expanded, true,
"The local scope 'this.window' should still be expanded (2)");
is(documentItem.expanded, true,
"The local scope 'this.window.document' should still be expanded (2)");
is(locationItem.expanded, true,
"The local scope 'this.window.document.location' should still be expanded (2)");
executeSoon(function() {
closeDebuggerAndFinish();
});
}}, 0);
}, false);
executeSoon(function() {
EventUtils.sendMouseEvent({ type: "mousedown" },
gDebugger.document.querySelector("#step-in"),
gDebugger.window);
});
}}, 0);
}, false);
executeSoon(function() {
EventUtils.sendMouseEvent({ type: "mousedown" },
locationItem.target.querySelector(".arrow"),
gDebuggee.window);
is(locationItem.expanded, true,
"The local scope 'this.window.document.location' should be expanded now");
});
}}, 0);
}, false);
executeSoon(function() {
EventUtils.sendMouseEvent({ type: "mousedown" },
documentItem.target.querySelector(".arrow"),
gDebuggee.window);
is(documentItem.expanded, true,
"The local scope 'this.window.document' should be expanded now");
});
}}, 0);
}, false);
executeSoon(function() {
EventUtils.sendMouseEvent({ type: "mousedown" },
windowItem.target.querySelector(".arrow"),
gDebuggee.window);
is(windowItem.expanded, true,
"The local scope 'this.window' should be expanded now");
});
}}, 0);
}, false);
executeSoon(function() {
EventUtils.sendMouseEvent({ type: "mousedown" },
thisItem.target.querySelector(".arrow"),
gDebuggee.window);
is(thisItem.expanded, true,
"The local scope 'this' should be expanded now");
});
}}, 0);
}, false);
EventUtils.sendMouseEvent({ type: "click" },
gDebuggee.document.querySelector("button"),
gDebuggee.window);
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;
gTab = null;
gDebugger = null;
gDebuggee = null;
});

View File

@ -172,6 +172,7 @@ function debug_tab_pane(aURL, aOnDebugging) {
// Wait for the initial resume...
pane.contentWindow.gClient.addOneTimeListener("resumed", function() {
pane.contentWindow.DebuggerView.Variables.lazyEmpty = false;
aOnDebugging(tab, debuggee, pane);
});
}, true);
@ -192,6 +193,7 @@ function debug_remote(aURL, aOnDebugging, aBeforeTabAdded) {
// Wait for the initial resume...
win.contentWindow.gClient.addOneTimeListener("resumed", function() {
win._dbgwin.DebuggerView.Variables.lazyEmpty = false;
aOnDebugging(tab, debuggee, win);
});
}, true);

View File

@ -238,6 +238,17 @@
* Property element
*/
.property {
transition: background 1s ease-in-out;
background: #fff;
border-radius: 8px;
}
.property[changed] {
transition-duration: 0.4s;
background: rgba(255, 255, 0, 0.65);
}
.property > .title > .name {
color: #881090;
}

View File

@ -240,6 +240,17 @@
* Property element
*/
.property {
transition: background 1s ease-in-out;
background: #fff;
border-radius: 8px;
}
.property[changed] {
transition-duration: 0.4s;
background: rgba(255, 255, 0, 0.65);
}
.property > .title > .name {
color: #881090;
}

View File

@ -246,6 +246,17 @@
* Property element
*/
.property {
transition: background 1s ease-in-out;
background: #fff;
border-radius: 8px;
}
.property[changed] {
transition-duration: 0.4s;
background: rgba(255, 255, 0, 0.65);
}
.property > .title > .name {
color: #881090;
}