Bug 723062 - Allow the user to edit the value of a debuggee object's property in the debugger; r=past

This commit is contained in:
Victor Porof 2012-03-15 10:17:03 +02:00
parent f63b20b181
commit bab93ec7df
13 changed files with 1269 additions and 151 deletions

View File

@ -825,7 +825,7 @@ PropertiesView.prototype = {
// Contains generic nodes and functionality.
let element = this._createPropertyElement(aName, aId, "variable",
aScope.querySelector(".details"));
aScope.getElementsByClassName("details")[0]);
// Make sure the element was created successfully.
if (!element) {
@ -845,19 +845,25 @@ PropertiesView.prototype = {
// Setup the additional elements specific for a variable node.
element.refresh(function() {
let separator = document.createElement("span");
let info = document.createElement("span");
let title = element.querySelector(".title");
let arrow = element.querySelector(".arrow");
let value = document.createElement("span");
let title = element.getElementsByClassName("title")[0];
// Separator shouldn't be selectable.
separator.className = "unselectable";
separator.appendChild(document.createTextNode(": "));
// The variable information (type, class and/or value).
info.className = "info";
value.className = "value";
// Handle the click event when pressing the element value label.
value.addEventListener("click", this._activateElementInputMode.bind({
scope: this,
element: element,
value: value
}));
title.appendChild(separator);
title.appendChild(info);
title.appendChild(value);
}.bind(this));
@ -898,19 +904,37 @@ PropertiesView.prototype = {
aGrip = { type: "null" };
}
let info = aVar.querySelector(".info") || aVar.target.info;
let value = aVar.getElementsByClassName("value")[0];
// Make sure the info node exists.
if (!info) {
// Make sure the value node exists.
if (!value) {
return null;
}
info.textContent = this._propertyString(aGrip);
info.classList.add(this._propertyColor(aGrip));
this._applyGrip(value, aGrip);
return aVar;
},
/**
* Applies the necessary text content and class name to a value node based
* on a grip.
*
* @param object aValueNode
* The value node to apply the changes to.
* @param object aGrip
* @see DebuggerView.Properties._setGrip
*/
_applyGrip: function DVP__applyGrip(aValueNode, aGrip) {
let prevGrip = aValueNode.currentGrip;
if (prevGrip) {
aValueNode.classList.remove(this._propertyColor(prevGrip));
}
aValueNode.textContent = this._propertyString(aGrip);
aValueNode.classList.add(this._propertyColor(aGrip));
aValueNode.currentGrip = aGrip;
},
/**
* Adds multiple properties to a specified variable.
* This function handles two types of properties: data properties and
@ -1000,7 +1024,7 @@ PropertiesView.prototype = {
// Contains generic nodes and functionality.
let element = this._createPropertyElement(aName, aId, "property",
aVar.querySelector(".details"));
aVar.getElementsByClassName("details")[0]);
// Make sure the element was created successfully.
if (!element) {
@ -1021,15 +1045,14 @@ PropertiesView.prototype = {
element.refresh(function(pKey, pGrip) {
let propertyString = this._propertyString(pGrip);
let propertyColor = this._propertyColor(pGrip);
let key = document.createElement("div");
let value = document.createElement("div");
let title = element.getElementsByClassName("title")[0];
let name = title.getElementsByClassName("name")[0];
let separator = document.createElement("span");
let title = element.querySelector(".title");
let arrow = element.querySelector(".arrow");
let value = document.createElement("span");
// Use a key element to specify the property name.
key.className = "key";
key.appendChild(document.createTextNode(pKey));
name.className = "key";
name.appendChild(document.createTextNode(pKey));
// Use a value element to specify the property value.
value.className = "value";
@ -1041,18 +1064,19 @@ PropertiesView.prototype = {
separator.appendChild(document.createTextNode(": "));
if ("undefined" !== typeof pKey) {
title.appendChild(key);
title.appendChild(name);
}
if ("undefined" !== typeof pGrip) {
title.appendChild(separator);
title.appendChild(value);
}
// Make the property also behave as a variable, to allow
// recursively adding properties to properties.
element.target = {
info: value
};
// Handle the click event when pressing the element value label.
value.addEventListener("click", this._activateElementInputMode.bind({
scope: this,
element: element,
value: value
}));
// Save the property to the variable for easier access.
Object.defineProperty(aVar, pKey, { value: element,
@ -1065,6 +1089,143 @@ PropertiesView.prototype = {
return element;
},
/**
* Makes an element's (variable or priperty) value editable.
* Make sure 'this' is bound to an object containing the properties:
* {
* "scope": the original scope to be used, probably DebuggerView.Properties,
* "element": the element whose value should be made editable,
* "value": the node displaying the value
* }
*
* @param event aEvent [optional]
* The event requesting this action.
*/
_activateElementInputMode: function DVP__activateElementInputMode(aEvent) {
if (aEvent) {
aEvent.stopPropagation();
}
let self = this.scope;
let element = this.element;
let value = this.value;
let title = this.value.parentNode;
let initialTextContent = value.textContent;
// When editing an object we need to collapse it first, in order to avoid
// displaying an inconsistent state while the user is editing.
element._previouslyExpanded = element.expanded;
element._preventExpand = true;
element.collapse();
element.forceHideArrow();
// Create a texbox input element which will be shown in the current
// element's value location.
let textbox = document.createElement("textbox");
textbox.setAttribute("value", value.textContent);
textbox.className = "element-input";
textbox.width = value.clientWidth + 1;
// Save the new value when the texbox looses focus or ENTER is pressed.
function DVP_element_textbox_blur(aTextboxEvent) {
DVP_element_textbox_save();
}
function DVP_element_textbox_keydown(aTextboxEvent) {
if (aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_RETURN ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_ENTER) {
DVP_element_textbox_save();
return;
}
if (aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_ESCAPE) {
value.textContent = initialTextContent;
DVP_element_textbox_clear();
return;
}
value.textContent = textbox.value;
}
function DVP_element_textbox_keypress(aTextboxEvent) {
if (aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_LEFT ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_RIGHT ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_UP ||
aTextboxEvent.keyCode === aTextboxEvent.DOM_VK_DOWN) {
return;
}
if (value.clientWidth > textbox.width - 1) {
textbox.width = value.clientWidth + 30;
}
}
// The actual save mechanism for the new variable/property value.
function DVP_element_textbox_save() {
if (textbox.value !== value.textContent) {
// TODO: use the debugger client API to send the value to the debuggee,
// after bug 724862 lands.
let result = eval(textbox.value);
let grip;
// Construct the grip based on the evaluated expression in the textbox.
switch (typeof result) {
case "number":
case "boolean":
case "string":
grip = result;
break;
case "object":
if (result === null) {
grip = {
"type": "null"
};
} else {
grip = {
"type": "object",
"class": result.constructor.name || "Object"
};
}
break;
case "undefined":
grip = { type: "undefined" };
}
self._applyGrip(value, grip);
}
DVP_element_textbox_clear();
}
// Removes the event listeners and appends the value node again.
function DVP_element_textbox_clear() {
element._preventExpand = false;
if (element._previouslyExpanded) {
element._previouslyExpanded = false;
element.expand();
}
element.showArrow();
textbox.removeEventListener("blur", DVP_element_textbox_blur, false);
textbox.removeEventListener("keydown", DVP_element_textbox_keydown, false);
textbox.removeEventListener("keypress", DVP_element_textbox_keypress, false);
title.removeChild(textbox);
value.removeAttribute("offscreen");
}
textbox.addEventListener("blur", DVP_element_textbox_blur, false);
textbox.addEventListener("keydown", DVP_element_textbox_keydown, false);
textbox.addEventListener("keypress", DVP_element_textbox_keypress, false);
title.appendChild(textbox);
value.setAttribute("offscreen", "");
textbox.select();
// When the value is a string (displayed as "value"), then we probably want
// to change it to another string in the textbox, so to avoid typing the ""
// again, tackle with the selection bounds just a bit.
if (value.textContent.match(/^"[^"]*"$/)) {
textbox.selectionEnd--;
textbox.selectionStart++;
}
},
/**
* Returns a custom formatted property string for a type and a value.
*
@ -1170,11 +1331,18 @@ PropertiesView.prototype = {
// The title element, containing the arrow and the name.
title.className = "title";
title.addEventListener("click", function() { element.toggle(); }, true);
// The node element which will contain any added scope variables.
details.className = "details";
// Add the click event handler for the title, or arrow and name.
if (aClass === "scope") {
title.addEventListener("click", function() { element.toggle(); }, false);
} else {
arrow.addEventListener("click", function() { element.toggle(); }, false);
name.addEventListener("click", function() { element.toggle(); }, false);
}
title.appendChild(arrow);
title.appendChild(name);
@ -1217,6 +1385,9 @@ PropertiesView.prototype = {
* The same element.
*/
element.expand = function DVP_element_expand() {
if (element._preventExpand) {
return;
}
arrow.setAttribute("open", "");
details.setAttribute("open", "");
@ -1232,6 +1403,9 @@ PropertiesView.prototype = {
* The same element.
*/
element.collapse = function DVP_element_collapse() {
if (element._preventCollapse) {
return;
}
arrow.removeAttribute("open");
details.removeAttribute("open");
@ -1261,39 +1435,49 @@ PropertiesView.prototype = {
* The same element.
*/
element.showArrow = function DVP_element_showArrow() {
if (details.childNodes.length) {
if (element._forceShowArrow || details.childNodes.length) {
arrow.style.visibility = "visible";
}
return element;
};
/**
* Forces the element expand/collapse arrow to be visible, even if there
* are no child elements.
*
* @param boolean aPreventHideFlag
* Prevents the arrow to be hidden when requested.
* @return object
* The same element.
*/
element.forceShowArrow = function DVP_element_forceShowArrow(aPreventHideFlag) {
element._preventHide = aPreventHideFlag;
arrow.style.visibility = "visible";
return element;
};
/**
* Hides the element expand/collapse arrow.
* @return object
* The same element.
*/
element.hideArrow = function DVP_element_hideArrow() {
if (!element._preventHide) {
if (!element._forceShowArrow) {
arrow.style.visibility = "hidden";
}
return element;
};
/**
* Forces the element expand/collapse arrow to be visible, even if there
* are no child elements.
*
* @return object
* The same element.
*/
element.forceShowArrow = function DVP_element_forceShowArrow() {
element._forceShowArrow = true;
arrow.style.visibility = "visible";
return element;
};
/**
* Forces the element expand/collapse arrow to be hidden, even if there
* are some child elements.
*
* @return object
* The same element.
*/
element.forceHideArrow = function DVP_element_forceHideArrow() {
arrow.style.visibility = "hidden";
return element;
};
/**
* Returns if the element is visible.
* @return boolean
@ -1336,7 +1520,7 @@ PropertiesView.prototype = {
* The same element.
*/
element.empty = function DVP_element_empty() {
// this details node won't have any elements, so hide the arrow
// This details node won't have any elements, so hide the arrow.
arrow.style.visibility = "hidden";
while (details.firstChild) {
details.removeChild(details.firstChild);
@ -1362,6 +1546,24 @@ PropertiesView.prototype = {
return element;
};
/**
* Returns if the element expander (arrow) is visible.
* @return boolean
* True if the arrow is visible.
*/
Object.defineProperty(element, "arrowVisible", {
get: function DVP_element_getArrowVisible() {
return arrow.style.visibility !== "hidden";
},
set: function DVP_element_setExpanded(value) {
if (value) {
element.showArrow();
} else {
element.hideArrow();
}
}
});
/**
* Generic function refreshing the internal state of the element when
* it's modified (e.g. a child detail, variable, property is added).
@ -1377,8 +1579,8 @@ PropertiesView.prototype = {
}
let node = aParent.parentNode;
let arrow = node.querySelector(".arrow");
let children = node.querySelector(".details").childNodes.length;
let arrow = node.getElementsByClassName("arrow")[0];
let children = node.getElementsByClassName("details")[0].childNodes.length;
// If the parent details node has at least one element, set the
// expand/collapse arrow visible.

View File

@ -85,6 +85,7 @@
.variable > .title {
white-space: nowrap;
display: block;
}
.variable > .details {
@ -107,14 +108,7 @@
.property > .title {
white-space: nowrap;
}
.property > .title > .key {
display: inline-block;
}
.property > .title > .value {
display: inline-block;
display: block;
}
.property > .details {
@ -126,6 +120,21 @@
-moz-box-orient: vertical;
}
.value[offscreen] {
position: fixed;
top: -100000px;
left: -100000px;
}
/**
* Expand/collapse arrow
*/
.arrow {
display: inline-block;
vertical-align: middle;
}
/**
* Display helpers
*/

View File

@ -30,6 +30,8 @@ _BROWSER_TEST_FILES = \
browser_dbg_propertyview-06.js \
browser_dbg_propertyview-07.js \
browser_dbg_propertyview-08.js \
browser_dbg_propertyview-edit.js \
browser_dbg_propertyview-edit2.js \
browser_dbg_panesize.js \
browser_dbg_stack-01.js \
browser_dbg_stack-02.js \

View File

@ -43,6 +43,10 @@ function testSimpleCall() {
"Any new variable should have a details container with no child nodes.");
let properties = testVar.addProperties({ "child": { "value": { "type": "object",
"class": "Object" } } });
ok(!testVar.expanded,
"Any new created variable should be initially collapsed.");
@ -84,18 +88,87 @@ function testSimpleCall() {
"The testVar should remember it is collapsed even if it is hidden.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.querySelector(".title"),
testVar.querySelector(".name"),
gDebugger);
ok(testVar.expanded,
"Clicking the testScope tilte should expand it.");
"Clicking the testVar name should expand it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.querySelector(".name"),
gDebugger);
ok(!testVar.expanded,
"Clicking again the testVar name should collapse it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.querySelector(".arrow"),
gDebugger);
ok(testVar.expanded,
"Clicking the testVar arrow should expand it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.querySelector(".arrow"),
gDebugger);
ok(!testVar.expanded,
"Clicking again the testVar arrow should collapse it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.querySelector(".title"),
gDebugger);
ok(!testVar.expanded,
"Clicking again the testScope tilte should collapse it.");
"Clicking the testVar title div shouldn't expand it.");
testScope.show();
testScope.expand();
testVar.show();
testVar.expand();
ok(!testVar.child.expanded,
"The testVar child property should remember it is collapsed even if it is hidden.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.child.querySelector(".key"),
gDebugger);
ok(testVar.child.expanded,
"Clicking the testVar child property name should expand it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.child.querySelector(".key"),
gDebugger);
ok(!testVar.child.expanded,
"Clicking again the testVar child property name should collapse it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.child.querySelector(".arrow"),
gDebugger);
ok(testVar.child.expanded,
"Clicking the testVar child property arrow should expand it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.child.querySelector(".arrow"),
gDebugger);
ok(!testVar.child.expanded,
"Clicking again the testVar child property arrow should collapse it.");
EventUtils.sendMouseEvent({ type: "click" },
testVar.child.querySelector(".title"),
gDebugger);
ok(!testVar.child.expanded,
"Clicking the testVar child property title div shouldn't expand it.");
let emptyCallbackSender = null;

View File

@ -28,7 +28,7 @@ function testSimpleCall() {
testVar.setGrip(1.618);
is(testVar.querySelector(".info").textContent, "1.618",
is(testVar.querySelector(".value").textContent, "1.618",
"The grip information for the variable wasn't set correctly.");
is(testVar.querySelector(".details").childNodes.length, 0,
@ -40,7 +40,7 @@ function testSimpleCall() {
is(testVar.querySelector(".details").childNodes.length, 0,
"Adding type and class properties shouldn't add any new tree nodes.");
is(testVar.querySelector(".info").textContent, "[object Window]",
is(testVar.querySelector(".value").textContent, "[object Window]",
"The information for the variable wasn't set correctly.");

View File

@ -104,28 +104,28 @@ function testSimpleCall() {
"The localVar5.someProp5 doesn't contain all the created properties.");
is(windowVar.querySelector(".info").textContent, "[object Window]",
is(windowVar.querySelector(".value").textContent, "[object Window]",
"The grip information for the windowVar wasn't set correctly.");
is(documentVar.querySelector(".info").textContent, "[object HTMLDocument]",
is(documentVar.querySelector(".value").textContent, "[object HTMLDocument]",
"The grip information for the documentVar wasn't set correctly.");
is(localVar0.querySelector(".info").textContent, "42",
is(localVar0.querySelector(".value").textContent, "42",
"The grip information for the localVar0 wasn't set correctly.");
is(localVar1.querySelector(".info").textContent, "true",
is(localVar1.querySelector(".value").textContent, "true",
"The grip information for the localVar1 wasn't set correctly.");
is(localVar2.querySelector(".info").textContent, "\"nasu\"",
is(localVar2.querySelector(".value").textContent, "\"nasu\"",
"The grip information for the localVar2 wasn't set correctly.");
is(localVar3.querySelector(".info").textContent, "undefined",
is(localVar3.querySelector(".value").textContent, "undefined",
"The grip information for the localVar3 wasn't set correctly.");
is(localVar4.querySelector(".info").textContent, "null",
is(localVar4.querySelector(".value").textContent, "null",
"The grip information for the localVar4 wasn't set correctly.");
is(localVar5.querySelector(".info").textContent, "[object Object]",
is(localVar5.querySelector(".value").textContent, "[object Object]",
"The grip information for the localVar5 wasn't set correctly.");
gDebugger.DebuggerController.activeThread.resume(function() {

View File

@ -55,37 +55,37 @@ function testFrameParameters()
is(localNodes.length, 11,
"The localScope should contain all the created variable elements.");
is(localNodes[0].querySelector(".info").textContent, "[object Proxy]",
is(localNodes[0].querySelector(".value").textContent, "[object Proxy]",
"Should have the right property value for 'this'.");
is(localNodes[1].querySelector(".info").textContent, "[object Object]",
is(localNodes[1].querySelector(".value").textContent, "[object Object]",
"Should have the right property value for 'aArg'.");
is(localNodes[2].querySelector(".info").textContent, '"beta"',
is(localNodes[2].querySelector(".value").textContent, '"beta"',
"Should have the right property value for 'bArg'.");
is(localNodes[3].querySelector(".info").textContent, "3",
is(localNodes[3].querySelector(".value").textContent, "3",
"Should have the right property value for 'cArg'.");
is(localNodes[4].querySelector(".info").textContent, "false",
is(localNodes[4].querySelector(".value").textContent, "false",
"Should have the right property value for 'dArg'.");
is(localNodes[5].querySelector(".info").textContent, "null",
is(localNodes[5].querySelector(".value").textContent, "null",
"Should have the right property value for 'eArg'.");
is(localNodes[6].querySelector(".info").textContent, "undefined",
is(localNodes[6].querySelector(".value").textContent, "undefined",
"Should have the right property value for 'fArg'.");
is(localNodes[7].querySelector(".info").textContent, "1",
is(localNodes[7].querySelector(".value").textContent, "1",
"Should have the right property value for 'a'.");
is(localNodes[8].querySelector(".info").textContent, "[object Object]",
is(localNodes[8].querySelector(".value").textContent, "[object Object]",
"Should have the right property value for 'b'.");
is(localNodes[9].querySelector(".info").textContent, "[object Object]",
is(localNodes[9].querySelector(".value").textContent, "[object Object]",
"Should have the right property value for 'c'.");
is(localNodes[10].querySelector(".info").textContent, "[object Arguments]",
is(localNodes[10].querySelector(".value").textContent, "[object Arguments]",
"Should have the right property value for 'arguments'.");
resumeAndFinish();

View File

@ -53,7 +53,7 @@ function testFrameParameters()
is(localNodes.length, 11,
"The localScope should contain all the created variable elements.");
is(localNodes[0].querySelector(".info").textContent, "[object Proxy]",
is(localNodes[0].querySelector(".value").textContent, "[object Proxy]",
"Should have the right property value for 'this'.");
// Expand the 'this', 'arguments' and 'c' tree nodes. This causes
@ -86,7 +86,7 @@ function testFrameParameters()
.textContent.search(/object/) != -1,
"__proto__ should be an object.");
is(localNodes[9].querySelector(".info").textContent, "[object Object]",
is(localNodes[9].querySelector(".value").textContent, "[object Object]",
"Should have the right property value for 'c'.");
is(localNodes[9].querySelectorAll(".property > .title > .key")[1]
@ -97,7 +97,7 @@ function testFrameParameters()
.textContent, 1,
"Should have the right value for 'c.a'.");
is(localNodes[10].querySelector(".info").textContent,
is(localNodes[10].querySelector(".value").textContent,
"[object Arguments]",
"Should have the right property value for 'arguments'.");

View File

@ -0,0 +1,413 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
var gPane = null;
var gTab = null;
var gDebuggee = null;
var gDebugger = null;
function test() {
debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gDebuggee = aDebuggee;
gPane = aPane;
gDebugger = gPane.contentWindow;
testSimpleCall();
});
}
function testSimpleCall() {
gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
Services.tm.currentThread.dispatch({ run: function() {
let testScope = gDebugger.DebuggerView.Properties._addScope("test").expand();
let localVar0 = testScope.addVar("aNumber");
localVar0.setGrip(1.618);
let localVar1 = testScope.addVar("aBoolean");
localVar1.setGrip(false);
let localVar2 = testScope.addVar("aString");
localVar2.setGrip("hello world");
let localVar3 = testScope.addVar("aUndefined");
localVar3.setGrip({ type: "undefined" });
let localVar4 = testScope.addVar("aNull");
localVar4.setGrip({ type: "null" });
testVar0(localVar0, function() {
testVar1(localVar1, function() {
testVar2(localVar2, function() {
testVar2_bis(localVar2, function() {
testVar3(localVar3, function() {
testVar4_switch(localVar4, localVar0, function() {
resumeAndFinish();
});
});
});
});
});
});
}}, 0);
});
gDebuggee.simpleCall();
}
function testVar0(aVar, aCallback) {
var changeTo = "true";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendKey("RETURN");
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this variable yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for number variables.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple number variables shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The variable should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The variable shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the variable wasn't set correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for boolean variables after exiting 'input-mode'.");
ok(aVar.visible,
"The variable should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple number elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testVar1(aVar, aCallback) {
var changeTo = "\"nasu\"";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendKey("ENTER");
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this variable yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for boolean variables.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple boolean variables shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The variable should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The variable shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the variable wasn't set correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string variables after exiting 'input-mode'.");
ok(aVar.visible,
"The variable should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple string elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testVar2(aVar, aCallback) {
var changeTo = "1234.5678";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
gDebugger.editor.focus();
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this variable yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string variables.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple string variables shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The variable should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The variable shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, "\"" + changeTo + "\"",
"The grip information for the variable wasn't set correctly.");
// when changing a string, its contents are automatically selected
// so that the result is another string; for example, if the value was
// "hello", then writing 42 would automatically produce "42"
// see more details in _activateElementInputMode from DebuggerView
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string variables after exiting 'input-mode'.");
ok(aVar.visible,
"The variable should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple string elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testVar2_bis(aVar, aCallback) {
var changeTo = "42";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
gDebugger.editor.focus();
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this variable yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string variables.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple string variables shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The variable should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The variable shouldn't be expanded while in 'input-mode'.");
aVar.querySelector(".element-input").select();
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the variable wasn't set correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for number variables after exiting 'input-mode'.");
ok(aVar.visible,
"The variable should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple number elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testVar3(aVar, aCallback) {
var changeTo = "\"this will be ignored\"";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendKey("ESCAPE");
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this variable yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for undefined variables.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple undefined variables shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The variable should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The variable shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, "undefined",
"The grip information for the variable wasn't reverted correctly.");
isnot(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the variable wasn't reverted correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for undefined variables after exiting 'input-mode'.");
ok(aVar.visible,
"The variable should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple undefined elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testVar4_switch(aVar, aVarSwitch, aCallback) {
var changeTo = "\"this will not be ignored\"";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendMouseEvent({ type: "click" },
aVarSwitch.querySelector(".value"),
gDebugger);
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this variable yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for null variables.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple null variables shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The variable should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The variable shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the variable set correctly after the input element lost focus.");
executeSoon(function() {
aCallback();
});
});
});
}
function resumeAndFinish() {
gDebugger.DebuggerController.activeThread.resume(function() {
closeDebuggerAndFinish(gTab);
});
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;
gTab = null;
gDebuggee = null;
gDebugger = null;
});

View File

@ -0,0 +1,458 @@
/* vim:set ts=2 sw=2 sts=2 et: */
/*
* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
var gPane = null;
var gTab = null;
var gDebuggee = null;
var gDebugger = null;
function test() {
debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
gTab = aTab;
gDebuggee = aDebuggee;
gPane = aPane;
gDebugger = gPane.contentWindow;
testSimpleCall();
});
}
function testSimpleCall() {
gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
Services.tm.currentThread.dispatch({ run: function() {
let testScope = gDebugger.DebuggerView.Properties._addScope("test").expand();
let v = testScope.addVar("aObject");
v.setGrip({ type: "object", "class": "Object" }).expand();
v.addProperties({ "someProp0": { "value": 42 },
"someProp1": { "value": true },
"someProp2": { "value": "nasu" },
"someProp3": { "value": { "type": "undefined" } },
"someProp4": { "value": { "type": "null" } },
"someProp5": { "value": { "type": "object", "class": "Object" } } });
testPropContainer(v, function() {
testProp0(v.someProp0, function() {
testProp1(v.someProp1, function() {
testProp2(v.someProp2, function() {
testProp2_bis(v.someProp2, function() {
testProp3(v.someProp3, function() {
testProp4_switch(v.someProp4, v.someProp0, function() {
resumeAndFinish();
});
});
});
});
});
});
});
}}, 0);
});
gDebuggee.simpleCall();
}
function testPropContainer(aVar, aCallback) {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this variable yet.");
ok(aVar.arrowVisible,
"The arrow should be visible for variable containers.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(aVar.expanded,
"This variable container should have been previously expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The variable should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The variable shouldn't be expanded while in 'input-mode'.");
gDebugger.editor.focus();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, "[object Object]",
"The grip information for the variable wasn't reset correctly.");
ok(aVar.arrowVisible,
"The arrow should be visible for variable containers after exiting 'input-mode'.'");
ok(aVar.visible,
"The variable should be visible before entering 'input-mode'.");
ok(aVar.expanded,
"This variable container should have been previously expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testProp0(aVar, aCallback) {
var changeTo = "true";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendKey("RETURN");
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this property yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for number properties.'");
ok(aVar.visible,
"The property should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple number properties shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The property should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The property shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the property wasn't set correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for boolean properties after exiting 'input-mode'.");
ok(aVar.visible,
"The property should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple number elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testProp1(aVar, aCallback) {
var changeTo = "\"nasu\"";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendKey("ENTER");
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this property yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for boolean properties.'");
ok(aVar.visible,
"The property should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple boolean properties shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The property should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The property shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the property wasn't set correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string properties after exiting 'input-mode'.");
ok(aVar.visible,
"The property should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple string elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testProp2(aVar, aCallback) {
var changeTo = "1234.5678";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
gDebugger.editor.focus();
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this property yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string properties.'");
ok(aVar.visible,
"The property should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple string properties shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The property should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The property shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, "\"" + changeTo + "\"",
"The grip information for the property wasn't set correctly.");
// when changing a string, its contents are automatically selected
// so that the result is another string; for example, if the value was
// "hello", then writing 42 would automatically produce "42"
// see more details in _activateElementInputMode from DebuggerView
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string properties after exiting 'input-mode'.");
ok(aVar.visible,
"The property should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple string elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testProp2_bis(aVar, aCallback) {
var changeTo = "42";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
gDebugger.editor.focus();
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this property yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for string properties.'");
ok(aVar.visible,
"The property should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple string properties shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The property should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The property shouldn't be expanded while in 'input-mode'.");
aVar.querySelector(".element-input").select();
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the property wasn't set correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for number properties after exiting 'input-mode'.");
ok(aVar.visible,
"The property should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple number elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testProp3(aVar, aCallback) {
var changeTo = "\"this will be ignored\"";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendKey("ESCAPE");
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this property yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for undefined properties.'");
ok(aVar.visible,
"The property should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple undefined properties shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The property should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The property shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, "undefined",
"The grip information for the property wasn't reverted correctly.");
isnot(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the property wasn't reverted correctly.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for undefined properties after exiting 'input-mode'.");
ok(aVar.visible,
"The property should be visible after exiting 'input-mode'.");
ok(!aVar.expanded,
"Simple undefined elements shouldn't be expanded.");
executeSoon(function() {
aCallback();
});
});
});
}
function testProp4_switch(aVar, aVarSwitch, aCallback) {
var changeTo = "\"this will not be ignored\"";
function makeChangesAndExitInputMode() {
EventUtils.sendString(changeTo);
EventUtils.sendMouseEvent({ type: "click" },
aVarSwitch.querySelector(".value"),
gDebugger);
}
ok(!aVar.querySelector(".element-input"),
"There should be no input elements for this property yet.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible for null properties.'");
ok(aVar.visible,
"The property should be visible before entering 'input-mode'.");
ok(!aVar.expanded,
"Simple null properties shouldn't be expanded.");
EventUtils.sendMouseEvent({ type: "click" },
aVar.querySelector(".value"),
gDebugger);
executeSoon(function() {
ok(aVar.querySelector(".element-input"),
"There should be an input element created.");
ok(!aVar.arrowVisible,
"The arrow shouldn't be visible while in 'input-mode.'");
ok(aVar.visible,
"The property should be visible while in 'input-mode'.");
ok(!aVar.expanded,
"The property shouldn't be expanded while in 'input-mode'.");
makeChangesAndExitInputMode();
executeSoon(function() {
ok(!aVar.querySelector(".element-input"),
"There should be no input elements after exiting 'input-mode'.");
is(aVar.querySelector(".value").textContent, changeTo,
"The grip information for the property set correctly after the input element lost focus.");
executeSoon(function() {
aCallback();
});
});
});
}
function resumeAndFinish() {
gDebugger.DebuggerController.activeThread.resume(function() {
closeDebuggerAndFinish(gTab);
});
}
registerCleanupFunction(function() {
removeTab(gTab);
gPane = null;
gTab = null;
gDebuggee = null;
gDebugger = null;
});

View File

@ -9,8 +9,7 @@
}
div,
span,
a {
span {
font: inherit;
}
@ -132,12 +131,15 @@ a {
.variable {
-moz-margin-start: 1px;
-moz-margin-end: 1px;
margin-top: 2px;
border-bottom: 1px dotted #aaa;
}
.variable > .title > .arrow {
margin-top: -2px;
}
.variable > .title > .name {
padding-top: 2px;
padding-bottom: 2px;
color: #048;
font-weight: 600;
}
@ -146,15 +148,12 @@ a {
* Property element
*/
.property > .title > .key {
padding-top: 2px;
padding-bottom: 2px;
color: #881090;
.property > .title > .arrow {
margin-top: -2px;
}
.property > .title > .value {
padding-top: 2px;
padding-bottom: 2px;
.property > .title > .key {
color: #881090;
}
/**
@ -227,17 +226,3 @@ a {
-moz-transform: scaleY(1);
}
}
/**
* Display helpers
*/
.unselectable {
padding-top: 2px;
padding-bottom: 2px;
}
.info {
padding-top: 2px;
padding-bottom: 2px;
}

View File

@ -9,8 +9,7 @@
}
div,
span,
a {
span {
font: inherit;
}
@ -131,11 +130,15 @@ a {
.variable {
-moz-margin-start: 1px;
-moz-margin-end: 1px;
margin-top: 2px;
border-bottom: 1px dotted #aaa;
}
.variable > .title > .arrow {
margin-top: -2px;
}
.variable > .title > .name {
padding-top: 4px;
color: #048;
font-weight: 600;
}
@ -144,13 +147,12 @@ a {
* Property element
*/
.property > .title > .key {
padding-top: 4px;
color: #881090;
.property > .title > .arrow {
margin-top: -2px;
}
.property > .title > .value {
padding-top: 4px;
.property > .title > .key {
color: #881090;
}
/**
@ -221,15 +223,3 @@ a {
-moz-transform: scaleY(1);
}
}
/**
* Display helpers
*/
.unselectable {
padding-top: 4px;
}
.info {
padding-top: 4px;
}

View File

@ -9,8 +9,7 @@
}
div,
span,
a {
span {
font: inherit;
}
@ -132,12 +131,15 @@ a {
.variable {
-moz-margin-start: 1px;
-moz-margin-end: 1px;
margin-top: 2px;
border-bottom: 1px dotted #aaa;
}
.variable > .title > .arrow {
margin-top: -2px;
}
.variable > .title > .name {
padding-top: 2px;
padding-bottom: 2px;
color: #048;
font-weight: 600;
}
@ -146,15 +148,12 @@ a {
* Property element
*/
.property > .title > .key {
padding-top: 2px;
padding-bottom: 2px;
color: #881090;
.property > .title > .arrow {
margin-top: -2px;
}
.property > .title > .value {
padding-top: 2px;
padding-bottom: 2px;
.property > .title > .key {
color: #881090;
}
/**
@ -200,6 +199,7 @@ a {
height: 9px;
-moz-margin-start: 5px;
-moz-margin-end: 5px;
margin-top: -2px;
background: url("chrome://global/skin/tree/twisty-clsd.png") center center no-repeat;
}
@ -229,17 +229,3 @@ a {
-moz-transform: scaleY(1);
}
}
/**
* Display helpers
*/
.unselectable {
padding-top: 2px;
padding-bottom: 2px;
}
.info {
padding-top: 2px;
padding-bottom: 2px;
}