зеркало из https://github.com/mozilla/gecko-dev.git
Merge fx-team to m-c.
This commit is contained in:
Коммит
33a583eacc
|
@ -869,7 +869,7 @@ let PlacesToolbarHelper = {
|
|||
return document.getElementById("PlacesToolbar");
|
||||
},
|
||||
|
||||
init: function PTH_init() {
|
||||
init: function PTH_init(forceToolbarOverflowCheck) {
|
||||
let viewElt = this._viewElt;
|
||||
if (!viewElt || viewElt._placesView)
|
||||
return;
|
||||
|
@ -886,6 +886,9 @@ let PlacesToolbarHelper = {
|
|||
return;
|
||||
|
||||
new PlacesToolbar(this._place);
|
||||
if (forceToolbarOverflowCheck) {
|
||||
viewElt._placesView.updateOverflowStatus();
|
||||
}
|
||||
},
|
||||
|
||||
customizeStart: function PTH_customizeStart() {
|
||||
|
@ -900,7 +903,7 @@ let PlacesToolbarHelper = {
|
|||
|
||||
customizeDone: function PTH_customizeDone() {
|
||||
this._isCustomizing = false;
|
||||
this.init();
|
||||
this.init(true);
|
||||
},
|
||||
|
||||
onPlaceholderCommand: function () {
|
||||
|
|
|
@ -1037,32 +1037,14 @@ PlacesToolbar.prototype = {
|
|||
this.updateChevron();
|
||||
break;
|
||||
case "overflow":
|
||||
if (aEvent.target != aEvent.currentTarget)
|
||||
if (!this._isOverflowStateEventRelevant(aEvent))
|
||||
return;
|
||||
|
||||
// Ignore purely vertical overflows.
|
||||
if (aEvent.detail == 0)
|
||||
return;
|
||||
|
||||
// Attach the popup binding to the chevron popup if it has not yet
|
||||
// been initialized.
|
||||
if (!this._chevronPopup.hasAttribute("type")) {
|
||||
this._chevronPopup.setAttribute("place", this.place);
|
||||
this._chevronPopup.setAttribute("type", "places");
|
||||
}
|
||||
this._chevron.collapsed = false;
|
||||
this.updateChevron();
|
||||
this._onOverflow();
|
||||
break;
|
||||
case "underflow":
|
||||
if (aEvent.target != aEvent.currentTarget)
|
||||
if (!this._isOverflowStateEventRelevant(aEvent))
|
||||
return;
|
||||
|
||||
// Ignore purely vertical underflows.
|
||||
if (aEvent.detail == 0)
|
||||
return;
|
||||
|
||||
this.updateChevron();
|
||||
this._chevron.collapsed = true;
|
||||
this._onUnderflow();
|
||||
break;
|
||||
case "TabOpen":
|
||||
case "TabClose":
|
||||
|
@ -1103,6 +1085,35 @@ PlacesToolbar.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
updateOverflowStatus: function() {
|
||||
if (this._rootElt.scrollLeftMax > 0) {
|
||||
this._onOverflow();
|
||||
} else {
|
||||
this._onUnderflow();
|
||||
}
|
||||
},
|
||||
|
||||
_isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(aEvent) {
|
||||
// Ignore events not aimed at ourselves, as well as purely vertical ones:
|
||||
return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
|
||||
},
|
||||
|
||||
_onOverflow: function PT_onOverflow() {
|
||||
// Attach the popup binding to the chevron popup if it has not yet
|
||||
// been initialized.
|
||||
if (!this._chevronPopup.hasAttribute("type")) {
|
||||
this._chevronPopup.setAttribute("place", this.place);
|
||||
this._chevronPopup.setAttribute("type", "places");
|
||||
}
|
||||
this._chevron.collapsed = false;
|
||||
this.updateChevron();
|
||||
},
|
||||
|
||||
_onUnderflow: function PT_onUnderflow() {
|
||||
this.updateChevron();
|
||||
this._chevron.collapsed = true;
|
||||
},
|
||||
|
||||
updateChevron: function PT_updateChevron() {
|
||||
// If the chevron is collapsed there's nothing to update.
|
||||
if (this._chevron.collapsed)
|
||||
|
|
|
@ -1508,12 +1508,34 @@ let SessionStoreInternal = {
|
|||
throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores the given state |aState| for a given window |aWindow|.
|
||||
*
|
||||
* @param aWindow (xul window)
|
||||
* The window that the given state will be restored to.
|
||||
* @param aState (string)
|
||||
* The state that will be applied to the given window.
|
||||
* @param aOverwrite (bool)
|
||||
* When true, existing tabs in the given window will be re-used or
|
||||
* removed. When false, only new tabs will be added, no existing ones
|
||||
8 will be removed or overwritten.
|
||||
*/
|
||||
setWindowState: function ssi_setWindowState(aWindow, aState, aOverwrite) {
|
||||
if (!aWindow.__SSi) {
|
||||
throw Components.Exception("Window is not tracked", Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
|
||||
this.restoreWindow(aWindow, aState, {overwriteTabs: aOverwrite});
|
||||
let winState = JSON.parse(aState);
|
||||
if (!winState) {
|
||||
throw Components.Exception("Invalid state string: not JSON", Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
|
||||
if (!winState.windows || !winState.windows[0]) {
|
||||
throw Components.Exception("Invalid window state passed", Cr.NS_ERROR_INVALID_ARG);
|
||||
}
|
||||
|
||||
let state = {windows: [winState.windows[0]]};
|
||||
this.restoreWindow(aWindow, state, {overwriteTabs: aOverwrite});
|
||||
},
|
||||
|
||||
getTabState: function ssi_getTabState(aTab) {
|
||||
|
@ -2280,7 +2302,7 @@ let SessionStoreInternal = {
|
|||
* @param aWindow
|
||||
* Window reference
|
||||
* @param aState
|
||||
* JS object or its eval'able source
|
||||
* JS object
|
||||
* @param aOptions
|
||||
* {overwriteTabs: true} to overwrite existing tabs w/ new ones
|
||||
* {isFollowUp: true} if this is not the restoration of the 1st window
|
||||
|
@ -2300,17 +2322,10 @@ let SessionStoreInternal = {
|
|||
if (aWindow && (!aWindow.__SSi || !this._windows[aWindow.__SSi]))
|
||||
this.onLoad(aWindow);
|
||||
|
||||
try {
|
||||
var root = typeof aState == "string" ? JSON.parse(aState) : aState;
|
||||
if (!root.windows[0]) {
|
||||
this._sendRestoreCompletedNotifications();
|
||||
return; // nothing to restore
|
||||
}
|
||||
}
|
||||
catch (ex) { // invalid state object - don't restore anything
|
||||
debug(ex);
|
||||
var root = aState;
|
||||
if (!root.windows[0]) {
|
||||
this._sendRestoreCompletedNotifications();
|
||||
return;
|
||||
return; // nothing to restore
|
||||
}
|
||||
|
||||
TelemetryStopwatch.start("FX_SESSION_RESTORE_RESTORE_WINDOW_MS");
|
||||
|
|
|
@ -78,44 +78,38 @@ function test() {
|
|||
}
|
||||
}
|
||||
|
||||
// open a window and add the above closed window list
|
||||
let newWin = openDialog(location, "_blank", "chrome,all,dialog=no");
|
||||
newWin.addEventListener("load", function(aEvent) {
|
||||
this.removeEventListener("load", arguments.callee, false);
|
||||
gPrefService.setIntPref("browser.sessionstore.max_windows_undo",
|
||||
test_state._closedWindows.length);
|
||||
ss.setWindowState(newWin, JSON.stringify(test_state), true);
|
||||
gPrefService.setIntPref("browser.sessionstore.max_windows_undo",
|
||||
test_state._closedWindows.length);
|
||||
ss.setBrowserState(JSON.stringify(test_state), true);
|
||||
|
||||
let closedWindows = JSON.parse(ss.getClosedWindowData());
|
||||
is(closedWindows.length, test_state._closedWindows.length,
|
||||
"Closed window list has the expected length");
|
||||
is(countByTitle(closedWindows, FORGET),
|
||||
test_state._closedWindows.length - remember_count,
|
||||
"The correct amount of windows are to be forgotten");
|
||||
is(countByTitle(closedWindows, REMEMBER), remember_count,
|
||||
"Everything is set up.");
|
||||
let closedWindows = JSON.parse(ss.getClosedWindowData());
|
||||
is(closedWindows.length, test_state._closedWindows.length,
|
||||
"Closed window list has the expected length");
|
||||
is(countByTitle(closedWindows, FORGET),
|
||||
test_state._closedWindows.length - remember_count,
|
||||
"The correct amount of windows are to be forgotten");
|
||||
is(countByTitle(closedWindows, REMEMBER), remember_count,
|
||||
"Everything is set up.");
|
||||
|
||||
// all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
|
||||
ok(testForError(function() ss.forgetClosedWindow(-1)),
|
||||
"Invalid window for forgetClosedWindow throws");
|
||||
ok(testForError(function() ss.forgetClosedWindow(test_state._closedWindows.length + 1)),
|
||||
"Invalid window for forgetClosedWindow throws");
|
||||
// all of the following calls with illegal arguments should throw NS_ERROR_ILLEGAL_VALUE
|
||||
ok(testForError(function() ss.forgetClosedWindow(-1)),
|
||||
"Invalid window for forgetClosedWindow throws");
|
||||
ok(testForError(function() ss.forgetClosedWindow(test_state._closedWindows.length + 1)),
|
||||
"Invalid window for forgetClosedWindow throws");
|
||||
|
||||
// Remove third window, then first window
|
||||
ss.forgetClosedWindow(2);
|
||||
ss.forgetClosedWindow(null);
|
||||
// Remove third window, then first window
|
||||
ss.forgetClosedWindow(2);
|
||||
ss.forgetClosedWindow(null);
|
||||
|
||||
closedWindows = JSON.parse(ss.getClosedWindowData());
|
||||
is(closedWindows.length, remember_count,
|
||||
"The correct amount of windows were removed");
|
||||
is(countByTitle(closedWindows, FORGET), 0,
|
||||
"All windows specifically forgotten were indeed removed");
|
||||
is(countByTitle(closedWindows, REMEMBER), remember_count,
|
||||
"... and windows not specifically forgetten weren't.");
|
||||
closedWindows = JSON.parse(ss.getClosedWindowData());
|
||||
is(closedWindows.length, remember_count,
|
||||
"The correct amount of windows were removed");
|
||||
is(countByTitle(closedWindows, FORGET), 0,
|
||||
"All windows specifically forgotten were indeed removed");
|
||||
is(countByTitle(closedWindows, REMEMBER), remember_count,
|
||||
"... and windows not specifically forgetten weren't.");
|
||||
|
||||
// clean up
|
||||
newWin.close();
|
||||
gPrefService.clearUserPref("browser.sessionstore.max_windows_undo");
|
||||
finish();
|
||||
}, false);
|
||||
// clean up
|
||||
gPrefService.clearUserPref("browser.sessionstore.max_windows_undo");
|
||||
finish();
|
||||
}
|
||||
|
|
|
@ -686,9 +686,9 @@ SourcesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
// The container is not empty and an actual item was selected.
|
||||
DebuggerView.setEditorLocation(sourceItem.value);
|
||||
|
||||
// Set window title.
|
||||
let script = sourceItem.value.split(" -> ").pop();
|
||||
document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", script);
|
||||
// Set window title. No need to split the url by " -> " here, because it was
|
||||
// already sanitized when the source was added.
|
||||
document.title = L10N.getFormatStr("DebuggerWindowScriptTitle", sourceItem.value);
|
||||
|
||||
DebuggerView.maybeShowBlackBoxMessage();
|
||||
this.updateToolbarButtonsState();
|
||||
|
@ -1085,38 +1085,14 @@ let SourceUtils = {
|
|||
|
||||
try {
|
||||
// Use an nsIURL to parse all the url path parts.
|
||||
var uri = Services.io.newURI(aUrl, null, null).QueryInterface(Ci.nsIURL);
|
||||
let url = aUrl.split(" -> ").pop();
|
||||
var uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
|
||||
} catch (e) {
|
||||
// This doesn't look like a url, or nsIURL can't handle it.
|
||||
return "";
|
||||
}
|
||||
|
||||
let { scheme, directory, fileName } = uri;
|
||||
let hostPort;
|
||||
// Add-on SDK jar: URLs will cause accessing hostPort to throw.
|
||||
if (scheme != "jar") {
|
||||
hostPort = uri.hostPort;
|
||||
}
|
||||
let lastDir = directory.split("/").reverse()[1];
|
||||
let group = [];
|
||||
|
||||
// Only show interesting schemes, http is implicit.
|
||||
if (scheme != "http") {
|
||||
group.push(scheme);
|
||||
}
|
||||
// Hostnames don't always exist for files or some resource urls.
|
||||
// e.g. file://foo/bar.js or resource:///foo/bar.js don't have a host.
|
||||
if (hostPort) {
|
||||
// If the hostname is a dot-separated identifier, show the first 2 parts.
|
||||
group.push(hostPort.split(".").slice(0, 2).join("."));
|
||||
}
|
||||
// Append the last directory if the path leads to an actual file.
|
||||
// e.g. http://foo.org/bar/ should only show "foo.org", not "foo.org bar"
|
||||
if (fileName) {
|
||||
group.push(lastDir);
|
||||
}
|
||||
|
||||
let groupLabel = group.join(" ");
|
||||
let groupLabel = uri.prePath;
|
||||
let unicodeLabel = NetworkHelper.convertToUnicode(unescape(groupLabel));
|
||||
this._groupsCache.set(aUrl, unicodeLabel)
|
||||
return unicodeLabel;
|
||||
|
|
|
@ -399,14 +399,6 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
initialize: function() {
|
||||
dumpn("Initializing the StackFramesView");
|
||||
|
||||
let commandset = this._commandset = document.createElement("commandset");
|
||||
let menupopup = this._menupopup = document.createElement("menupopup");
|
||||
commandset.id = "stackframesCommandset";
|
||||
menupopup.id = "stackframesMenupopup";
|
||||
|
||||
document.getElementById("debuggerPopupset").appendChild(menupopup);
|
||||
document.getElementById("debuggerCommands").appendChild(commandset);
|
||||
|
||||
this.widget = new BreadcrumbsWidget(document.getElementById("stackframes"));
|
||||
this.widget.addEventListener("select", this._onSelect, false);
|
||||
this.widget.addEventListener("scroll", this._onScroll, true);
|
||||
|
@ -414,6 +406,9 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
|
||||
this.autoFocusOnFirstItem = false;
|
||||
this.autoFocusOnSelection = false;
|
||||
|
||||
// This view's contents are also mirrored in a different container.
|
||||
this._mirror = DebuggerView.StackFramesClassicList;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -453,24 +448,22 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
this._prevBlackBoxedUrl = null;
|
||||
}
|
||||
|
||||
// Create the element node and menu entry for the stack frame item.
|
||||
// Create the element node for the stack frame item.
|
||||
let frameView = this._createFrameView.apply(this, arguments);
|
||||
let menuEntry = this._createMenuEntry.apply(this, arguments);
|
||||
|
||||
// Append a stack frame item to this container.
|
||||
this.push([frameView, aTitle, aUrl], {
|
||||
index: 0, /* specifies on which position should the item be appended */
|
||||
attachment: {
|
||||
popup: menuEntry,
|
||||
depth: aDepth
|
||||
},
|
||||
attributes: [
|
||||
["contextmenu", "stackframesMenupopup"]
|
||||
],
|
||||
// Make sure that when the stack frame item is removed, the corresponding
|
||||
// menuitem and command are also destroyed.
|
||||
// mirrored item in the classic list is also removed.
|
||||
finalize: this._onStackframeRemoved
|
||||
});
|
||||
|
||||
// Mirror this newly inserted item inside the "Call Stack" tab.
|
||||
this._mirror.addFrame(aTitle, aUrl, aLine, aDepth);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -542,65 +535,6 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* Customization function for populating an item's context menu.
|
||||
*
|
||||
* @param string aTitle
|
||||
* The frame title to be displayed in the list.
|
||||
* @param string aUrl
|
||||
* The frame source url.
|
||||
* @param string aLine
|
||||
* The frame line number.
|
||||
* @param number aDepth
|
||||
* The frame depth in the stack.
|
||||
* @param boolean aIsBlackBoxed
|
||||
* Whether or not the frame is black boxed.
|
||||
* @return object
|
||||
* An object containing the stack frame command and menu item.
|
||||
*/
|
||||
_createMenuEntry: function(aTitle, aUrl, aLine, aDepth, aIsBlackBoxed) {
|
||||
let frameDescription = SourceUtils.trimUrlLength(
|
||||
SourceUtils.getSourceLabel(aUrl),
|
||||
STACK_FRAMES_POPUP_SOURCE_URL_MAX_LENGTH,
|
||||
STACK_FRAMES_POPUP_SOURCE_URL_TRIM_SECTION) +
|
||||
SEARCH_LINE_FLAG + aLine;
|
||||
|
||||
let prefix = "sf-cMenu-"; // "stackframes context menu"
|
||||
let commandId = prefix + aDepth + "-" + "-command";
|
||||
let menuitemId = prefix + aDepth + "-" + "-menuitem";
|
||||
|
||||
let command = document.createElement("command");
|
||||
command.id = commandId;
|
||||
command.addEventListener("command", () => this.selectedDepth = aDepth, false);
|
||||
|
||||
let menuitem = document.createElement("menuitem");
|
||||
menuitem.id = menuitemId;
|
||||
menuitem.className = "dbg-stackframe-menuitem";
|
||||
menuitem.setAttribute("type", "checkbox");
|
||||
menuitem.setAttribute("command", commandId);
|
||||
menuitem.setAttribute("tooltiptext", aUrl);
|
||||
|
||||
let labelNode = document.createElement("label");
|
||||
labelNode.className = "plain dbg-stackframe-menuitem-title";
|
||||
labelNode.setAttribute("value", aTitle);
|
||||
labelNode.setAttribute("flex", "1");
|
||||
|
||||
let descriptionNode = document.createElement("label");
|
||||
descriptionNode.className = "plain dbg-stackframe-menuitem-details";
|
||||
descriptionNode.setAttribute("value", frameDescription);
|
||||
|
||||
menuitem.appendChild(labelNode);
|
||||
menuitem.appendChild(descriptionNode);
|
||||
|
||||
this._commandset.appendChild(command);
|
||||
this._menupopup.appendChild(menuitem);
|
||||
|
||||
return {
|
||||
command: command,
|
||||
menuitem: menuitem
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Function called each time a stack frame item is removed.
|
||||
*
|
||||
|
@ -610,10 +544,9 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
_onStackframeRemoved: function(aItem) {
|
||||
dumpn("Finalizing stackframe item: " + aItem);
|
||||
|
||||
// Destroy the context menu item for the stack frame.
|
||||
let contextItem = aItem.attachment.popup;
|
||||
contextItem.command.remove();
|
||||
contextItem.menuitem.remove();
|
||||
// Remove the mirrored item in the classic list.
|
||||
let depth = aItem.attachment.depth;
|
||||
this._mirror.remove(this._mirror.getItemForAttachment(e => e.depth == depth));
|
||||
|
||||
// Forget the previously blackboxed stack frame url.
|
||||
this._prevBlackBoxedUrl = null;
|
||||
|
@ -626,17 +559,13 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
let stackframeItem = this.selectedItem;
|
||||
if (stackframeItem) {
|
||||
// The container is not empty and an actual item was selected.
|
||||
DebuggerController.StackFrames.selectFrame(stackframeItem.attachment.depth);
|
||||
let depth = stackframeItem.attachment.depth;
|
||||
DebuggerController.StackFrames.selectFrame(depth);
|
||||
|
||||
// Update the context menu to show the currently selected stackframe item
|
||||
// as a checked entry.
|
||||
for (let otherItem of this) {
|
||||
if (otherItem != stackframeItem) {
|
||||
otherItem.attachment.popup.menuitem.removeAttribute("checked");
|
||||
} else {
|
||||
otherItem.attachment.popup.menuitem.setAttribute("checked", "");
|
||||
}
|
||||
}
|
||||
// Mirror the selected item in the classic list.
|
||||
this.suppressSelectionEvents = true;
|
||||
this._mirror.selectedItem = e => e.attachment.depth == depth;
|
||||
this.suppressSelectionEvents = false;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -673,11 +602,142 @@ StackFramesView.prototype = Heritage.extend(WidgetMethods, {
|
|||
}
|
||||
},
|
||||
|
||||
_commandset: null,
|
||||
_menupopup: null,
|
||||
_mirror: null,
|
||||
_prevBlackBoxedUrl: null
|
||||
});
|
||||
|
||||
/*
|
||||
* Functions handling the stackframes classic list UI.
|
||||
* Controlled by the DebuggerView.StackFrames isntance.
|
||||
*/
|
||||
function StackFramesClassicListView() {
|
||||
dumpn("StackFramesClassicListView was instantiated");
|
||||
|
||||
this._onSelect = this._onSelect.bind(this);
|
||||
}
|
||||
|
||||
StackFramesClassicListView.prototype = Heritage.extend(WidgetMethods, {
|
||||
/**
|
||||
* Initialization function, called when the debugger is started.
|
||||
*/
|
||||
initialize: function() {
|
||||
dumpn("Initializing the StackFramesClassicListView");
|
||||
|
||||
this.widget = new SideMenuWidget(document.getElementById("callstack-list"), {
|
||||
theme: "light"
|
||||
});
|
||||
this.widget.addEventListener("select", this._onSelect, false);
|
||||
|
||||
this.emptyText = L10N.getStr("noStackFramesText");
|
||||
this.autoFocusOnFirstItem = false;
|
||||
this.autoFocusOnSelection = false;
|
||||
|
||||
// This view's contents are also mirrored in a different container.
|
||||
this._mirror = DebuggerView.StackFrames;
|
||||
|
||||
// Show an empty label by default.
|
||||
this.empty();
|
||||
},
|
||||
|
||||
/**
|
||||
* Destruction function, called when the debugger is closed.
|
||||
*/
|
||||
destroy: function() {
|
||||
dumpn("Destroying the StackFramesClassicListView");
|
||||
|
||||
this.widget.removeEventListener("select", this._onSelect, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a frame in this stackframes container.
|
||||
*
|
||||
* @param string aTitle
|
||||
* The frame title (function name).
|
||||
* @param string aUrl
|
||||
* The frame source url.
|
||||
* @param string aLine
|
||||
* The frame line number.
|
||||
* @param number aDepth
|
||||
* The frame depth in the stack.
|
||||
*/
|
||||
addFrame: function(aTitle, aUrl, aLine, aDepth) {
|
||||
// Create the element node for the stack frame item.
|
||||
let frameView = this._createFrameView.apply(this, arguments);
|
||||
|
||||
// Append a stack frame item to this container.
|
||||
this.push([frameView, aUrl], {
|
||||
attachment: {
|
||||
depth: aDepth
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Customization function for creating an item's UI.
|
||||
*
|
||||
* @param string aTitle
|
||||
* The frame title to be displayed in the list.
|
||||
* @param string aUrl
|
||||
* The frame source url.
|
||||
* @param string aLine
|
||||
* The frame line number.
|
||||
* @param number aDepth
|
||||
* The frame depth in the stack.
|
||||
* @return nsIDOMNode
|
||||
* The stack frame view.
|
||||
*/
|
||||
_createFrameView: function(aTitle, aUrl, aLine, aDepth) {
|
||||
let container = document.createElement("hbox");
|
||||
container.id = "classic-stackframe-" + aDepth;
|
||||
container.className = "dbg-classic-stackframe";
|
||||
container.setAttribute("flex", "1");
|
||||
|
||||
let frameTitleNode = document.createElement("label");
|
||||
frameTitleNode.className = "plain dbg-classic-stackframe-title";
|
||||
frameTitleNode.setAttribute("value", aTitle);
|
||||
frameTitleNode.setAttribute("crop", "center");
|
||||
|
||||
let frameDetailsNode = document.createElement("hbox");
|
||||
frameDetailsNode.className = "plain dbg-classic-stackframe-details";
|
||||
|
||||
let frameUrlNode = document.createElement("label");
|
||||
frameUrlNode.className = "plain dbg-classic-stackframe-details-url";
|
||||
frameUrlNode.setAttribute("value", SourceUtils.getSourceLabel(aUrl));
|
||||
frameUrlNode.setAttribute("crop", "center");
|
||||
frameDetailsNode.appendChild(frameUrlNode);
|
||||
|
||||
let frameDetailsSeparator = document.createElement("label");
|
||||
frameDetailsSeparator.className = "plain dbg-classic-stackframe-details-sep";
|
||||
frameDetailsSeparator.setAttribute("value", SEARCH_LINE_FLAG);
|
||||
frameDetailsNode.appendChild(frameDetailsSeparator);
|
||||
|
||||
let frameLineNode = document.createElement("label");
|
||||
frameLineNode.className = "plain dbg-classic-stackframe-details-line";
|
||||
frameLineNode.setAttribute("value", aLine);
|
||||
frameDetailsNode.appendChild(frameLineNode);
|
||||
|
||||
container.appendChild(frameTitleNode);
|
||||
container.appendChild(frameDetailsNode);
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* The select listener for the stackframes container.
|
||||
*/
|
||||
_onSelect: function(e) {
|
||||
let stackframeItem = this.selectedItem;
|
||||
if (stackframeItem) {
|
||||
// The container is not empty and an actual item was selected.
|
||||
// Mirror the selected item in the breadcrumbs list.
|
||||
let depth = stackframeItem.attachment.depth;
|
||||
this._mirror.selectedItem = e => e.attachment.depth == depth;
|
||||
}
|
||||
},
|
||||
|
||||
_mirror: null
|
||||
});
|
||||
|
||||
/**
|
||||
* Functions handling the filtering UI.
|
||||
*/
|
||||
|
@ -1533,3 +1593,4 @@ DebuggerView.FilteredSources = new FilteredSourcesView();
|
|||
DebuggerView.FilteredFunctions = new FilteredFunctionsView();
|
||||
DebuggerView.ChromeGlobals = new ChromeGlobalsView();
|
||||
DebuggerView.StackFrames = new StackFramesView();
|
||||
DebuggerView.StackFramesClassicList = new StackFramesClassicListView();
|
||||
|
|
|
@ -9,8 +9,6 @@ const SOURCE_SYNTAX_HIGHLIGHT_MAX_FILE_SIZE = 1048576; // 1 MB in bytes
|
|||
const SOURCE_URL_DEFAULT_MAX_LENGTH = 64; // chars
|
||||
const STACK_FRAMES_SOURCE_URL_MAX_LENGTH = 15; // chars
|
||||
const STACK_FRAMES_SOURCE_URL_TRIM_SECTION = "center";
|
||||
const STACK_FRAMES_POPUP_SOURCE_URL_MAX_LENGTH = 32; // chars
|
||||
const STACK_FRAMES_POPUP_SOURCE_URL_TRIM_SECTION = "center";
|
||||
const STACK_FRAMES_SCROLL_DELAY = 100; // ms
|
||||
const BREAKPOINT_LINE_TOOLTIP_MAX_LENGTH = 1000; // chars
|
||||
const BREAKPOINT_CONDITIONAL_POPUP_POSITION = "before_start";
|
||||
|
@ -59,6 +57,7 @@ let DebuggerView = {
|
|||
this.FilteredFunctions.initialize();
|
||||
this.ChromeGlobals.initialize();
|
||||
this.StackFrames.initialize();
|
||||
this.StackFramesClassicList.initialize();
|
||||
this.Sources.initialize();
|
||||
this.VariableBubble.initialize();
|
||||
this.WatchExpressions.initialize();
|
||||
|
@ -93,6 +92,7 @@ let DebuggerView = {
|
|||
this.FilteredFunctions.destroy();
|
||||
this.ChromeGlobals.destroy();
|
||||
this.StackFrames.destroy();
|
||||
this.StackFramesClassicList.destroy();
|
||||
this.Sources.destroy();
|
||||
this.VariableBubble.destroy();
|
||||
this.WatchExpressions.destroy();
|
||||
|
@ -531,7 +531,7 @@ let DebuggerView = {
|
|||
animated: true,
|
||||
delayed: true,
|
||||
callback: aCallback
|
||||
}, 0);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -4,14 +4,9 @@
|
|||
* 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/. */
|
||||
|
||||
/* Sources search view */
|
||||
|
||||
#globalsearch {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Instruments pane view (watch expressions, variables, events...) */
|
||||
/* Side pane views */
|
||||
|
||||
#sources-pane > tabpanels > tabpanel,
|
||||
#instruments-pane > tabpanels > tabpanel {
|
||||
-moz-box-orient: vertical;
|
||||
}
|
||||
|
@ -21,6 +16,10 @@
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#globalsearch {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Toolbar controls */
|
||||
|
||||
.devtools-toolbarbutton:not([label]) > .toolbarbutton-text {
|
||||
|
|
|
@ -321,23 +321,35 @@
|
|||
<scrollbox id="globalsearch" orient="vertical" hidden="true"/>
|
||||
<splitter class="devtools-horizontal-splitter" hidden="true"/>
|
||||
<hbox id="debugger-widgets" flex="1">
|
||||
<vbox id="sources-pane">
|
||||
<vbox id="sources" flex="1"/>
|
||||
<toolbar id="sources-toolbar" class="devtools-toolbar">
|
||||
<hbox id="sources-controls">
|
||||
<toolbarbutton id="black-box"
|
||||
tooltiptext="&debuggerUI.sources.blackBoxTooltip;"
|
||||
command="blackBoxCommand"
|
||||
class="devtools-toolbarbutton"/>
|
||||
<toolbarbutton id="pretty-print"
|
||||
label="{}"
|
||||
tooltiptext="&debuggerUI.sources.prettyPrint;"
|
||||
class="devtools-toolbarbutton devtools-monospace"
|
||||
command="prettyPrintCommand"
|
||||
hidden="true"/>
|
||||
</hbox>
|
||||
</toolbar>
|
||||
</vbox>
|
||||
<tabbox id="sources-pane"
|
||||
class="devtools-sidebar-tabs">
|
||||
<tabs>
|
||||
<tab id="sources-tab" label="&debuggerUI.tabs.sources;"/>
|
||||
<tab id="callstack-tab" label="&debuggerUI.tabs.callstack;"/>
|
||||
</tabs>
|
||||
<tabpanels flex="1">
|
||||
<tabpanel id="sources-tabpanel">
|
||||
<vbox id="sources" flex="1"/>
|
||||
<toolbar id="sources-toolbar" class="devtools-toolbar">
|
||||
<hbox id="sources-controls">
|
||||
<toolbarbutton id="black-box"
|
||||
tooltiptext="&debuggerUI.sources.blackBoxTooltip;"
|
||||
command="blackBoxCommand"
|
||||
class="devtools-toolbarbutton"/>
|
||||
<toolbarbutton id="pretty-print"
|
||||
label="{}"
|
||||
tooltiptext="&debuggerUI.sources.prettyPrint;"
|
||||
class="devtools-toolbarbutton devtools-monospace"
|
||||
command="prettyPrintCommand"
|
||||
hidden="true"/>
|
||||
</hbox>
|
||||
</toolbar>
|
||||
</tabpanel>
|
||||
<tabpanel id="callstack-tabpanel">
|
||||
<vbox id="callstack-list" flex="1"/>
|
||||
</tabpanel>
|
||||
</tabpanels>
|
||||
</tabbox>
|
||||
<splitter id="sources-and-editor-splitter"
|
||||
class="devtools-side-splitter"/>
|
||||
<deck id="editor-deck" flex="4">
|
||||
|
@ -367,12 +379,12 @@
|
|||
<tab id="events-tab" label="&debuggerUI.tabs.events;"/>
|
||||
</tabs>
|
||||
<tabpanels flex="1">
|
||||
<tabpanel id="variables-tabpanel">
|
||||
<tabpanel id="variables-tabpanel" class="theme-body">
|
||||
<vbox id="expressions"/>
|
||||
<splitter class="devtools-horizontal-splitter"/>
|
||||
<vbox id="variables" flex="1"/>
|
||||
</tabpanel>
|
||||
<tabpanel id="events-tabpanel">
|
||||
<tabpanel id="events-tabpanel" class="theme-body">
|
||||
<vbox id="event-listeners" flex="1"/>
|
||||
</tabpanel>
|
||||
</tabpanels>
|
||||
|
|
|
@ -48,34 +48,40 @@ function test() {
|
|||
}
|
||||
|
||||
function checkNavigationWhileFocused() {
|
||||
let deferred = promise.defer();
|
||||
return Task.spawn(function() {
|
||||
yield promise.all([
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
|
||||
EventUtils.sendKey("UP", gDebugger)
|
||||
]);
|
||||
checkState({ frame: 2, source: 1, line: 6 });
|
||||
|
||||
EventUtils.sendKey("UP", gDebugger);
|
||||
checkState({ frame: 2, source: 1, line: 6 });
|
||||
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 5).then(() => {
|
||||
yield promise.all([
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 5),
|
||||
EventUtils.sendKey("UP", gDebugger)
|
||||
]);
|
||||
checkState({ frame: 1, source: 0, line: 5 });
|
||||
|
||||
EventUtils.sendKey("UP", gDebugger);
|
||||
yield promise.all([
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
|
||||
EventUtils.sendKey("UP", gDebugger)
|
||||
]);
|
||||
checkState({ frame: 0, source: 0, line: 5 });
|
||||
|
||||
waitForSourceAndCaret(gPanel, "-02.js", 6).then(() => {
|
||||
checkState({ frame: 3, source: 1, line: 6 });
|
||||
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 5).then(() => {
|
||||
checkState({ frame: 0, source: 0, line: 5 });
|
||||
deferred.resolve();
|
||||
});
|
||||
yield promise.all([
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
|
||||
waitForSourceAndCaret(gPanel, "-02.js", 6),
|
||||
EventUtils.sendKey("END", gDebugger)
|
||||
]);
|
||||
checkState({ frame: 3, source: 1, line: 6 });
|
||||
|
||||
yield promise.all([
|
||||
waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES),
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 5),
|
||||
EventUtils.sendKey("HOME", gDebugger)
|
||||
});
|
||||
|
||||
EventUtils.sendKey("END", gDebugger)
|
||||
]);
|
||||
checkState({ frame: 0, source: 0, line: 5 });
|
||||
});
|
||||
|
||||
EventUtils.sendKey("UP", gDebugger)
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function checkState({ frame, source, line }) {
|
||||
|
|
|
@ -46,7 +46,7 @@ function performTest() {
|
|||
|
||||
is(gSources.widget.getAttribute("label"), "doc_recursion-stack.html",
|
||||
"The sources widget should have a correct label attribute.");
|
||||
is(gSources.widget.getAttribute("tooltiptext"), "example.com test",
|
||||
is(gSources.widget.getAttribute("tooltiptext"), "http://example.com",
|
||||
"The sources widget should have a correct tooltip text attribute.");
|
||||
|
||||
is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice-container").length, 0,
|
||||
|
|
|
@ -48,7 +48,7 @@ function testLocationChange() {
|
|||
|
||||
is(gSources.widget.getAttribute("label"), "doc_inline-debugger-statement.html",
|
||||
"The sources widget should have a correct label attribute.");
|
||||
is(gSources.widget.getAttribute("tooltiptext"), "example.com test",
|
||||
is(gSources.widget.getAttribute("tooltiptext"), "http://example.com",
|
||||
"The sources widget should have a correct tooltip text attribute.");
|
||||
|
||||
is(gDebugger.document.querySelectorAll("#sources .side-menu-widget-empty-notice-container").length, 0,
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gFrames;
|
||||
let gFrames, gClassicFrames;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
|
@ -17,6 +17,7 @@ function test() {
|
|||
gPanel = aPanel;
|
||||
gDebugger = gPanel.panelWin;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, ".html", 14).then(performTest);
|
||||
gDebuggee.simpleCall();
|
||||
|
@ -28,6 +29,8 @@ function performTest() {
|
|||
"Should only be getting stack frames while paused.");
|
||||
is(gFrames.itemCount, 1,
|
||||
"Should have only one frame.");
|
||||
is(gClassicFrames.itemCount, 1,
|
||||
"Should also have only one frame in the mirrored view.");
|
||||
|
||||
resumeDebuggerThenCloseAndFinish(gPanel);
|
||||
}
|
||||
|
@ -38,4 +41,5 @@ registerCleanupFunction(function() {
|
|||
gPanel = null;
|
||||
gDebugger = null;
|
||||
gFrames = null;
|
||||
gClassicFrames = null;
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gFrames;
|
||||
let gFrames, gClassicFrames;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
|
@ -17,6 +17,7 @@ function test() {
|
|||
gPanel = aPanel;
|
||||
gDebugger = gPanel.panelWin;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, ".html", 18).then(performTest);
|
||||
gDebuggee.evalCall();
|
||||
|
@ -28,21 +29,32 @@ function performTest() {
|
|||
"Should only be getting stack frames while paused.");
|
||||
is(gFrames.itemCount, 2,
|
||||
"Should have two frames.");
|
||||
is(gClassicFrames.itemCount, 2,
|
||||
"Should also have only two in the mirrored view.");
|
||||
|
||||
is(gFrames.getItemAtIndex(0).value,
|
||||
"evalCall", "Oldest frame name should be correct.");
|
||||
is(gFrames.getItemAtIndex(0).description,
|
||||
TAB_URL, "Oldest frame url should be correct.");
|
||||
is(gClassicFrames.getItemAtIndex(0).value,
|
||||
TAB_URL, "Oldest frame name is mirrored correctly.");
|
||||
|
||||
is(gFrames.getItemAtIndex(1).value,
|
||||
"(eval)", "Newest frame name should be correct.");
|
||||
is(gFrames.getItemAtIndex(1).description,
|
||||
TAB_URL, "Newest frame url should be correct.");
|
||||
is(gClassicFrames.getItemAtIndex(1).value,
|
||||
TAB_URL, "Newest frame name is mirrored correctly.");
|
||||
|
||||
is(gFrames.selectedIndex, 1,
|
||||
"Newest frame should be selected by default.");
|
||||
is(gClassicFrames.selectedIndex, 0,
|
||||
"Newest frame should be selected by default in the mirrored view.");
|
||||
|
||||
isnot(gFrames.selectedIndex, 0,
|
||||
"Oldest frame should not be selected.");
|
||||
isnot(gClassicFrames.selectedIndex, 1,
|
||||
"Oldest frame should not be selected in the mirrored view.");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
gFrames.getItemAtIndex(0).target,
|
||||
|
@ -50,8 +62,13 @@ function performTest() {
|
|||
|
||||
isnot(gFrames.selectedIndex, 1,
|
||||
"Newest frame should not be selected after click.");
|
||||
isnot(gClassicFrames.selectedIndex, 0,
|
||||
"Newest frame in the mirrored view should not be selected.");
|
||||
|
||||
is(gFrames.selectedIndex, 0,
|
||||
"Oldest frame should be selected after click.");
|
||||
is(gClassicFrames.selectedIndex, 1,
|
||||
"Oldest frame in the mirrored view should be selected.");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
gFrames.getItemAtIndex(1).target.querySelector(".dbg-stackframe-title"),
|
||||
|
@ -59,8 +76,13 @@ function performTest() {
|
|||
|
||||
is(gFrames.selectedIndex, 1,
|
||||
"Newest frame should be selected after click inside the newest frame.");
|
||||
is(gClassicFrames.selectedIndex, 0,
|
||||
"Newest frame in the mirrored view should be selected.");
|
||||
|
||||
isnot(gFrames.selectedIndex, 0,
|
||||
"Oldest frame should not be selected after click inside the newest frame.");
|
||||
isnot(gClassicFrames.selectedIndex, 1,
|
||||
"Oldest frame in the mirrored view should not be selected.");
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
gFrames.getItemAtIndex(0).target.querySelector(".dbg-stackframe-details"),
|
||||
|
@ -68,8 +90,13 @@ function performTest() {
|
|||
|
||||
isnot(gFrames.selectedIndex, 1,
|
||||
"Newest frame should not be selected after click inside the oldest frame.");
|
||||
isnot(gClassicFrames.selectedIndex, 0,
|
||||
"Newest frame in the mirrored view should not be selected.");
|
||||
|
||||
is(gFrames.selectedIndex, 0,
|
||||
"Oldest frame should be selected after click inside the oldest frame.");
|
||||
is(gClassicFrames.selectedIndex, 1,
|
||||
"Oldest frame in the mirrored view should be selected.");
|
||||
|
||||
resumeDebuggerThenCloseAndFinish(gPanel);
|
||||
}
|
||||
|
@ -80,4 +107,5 @@ registerCleanupFunction(function() {
|
|||
gPanel = null;
|
||||
gDebugger = null;
|
||||
gFrames = null;
|
||||
gClassicFrames = null;
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gFrames, gFramesScrollingInterval;
|
||||
let gFrames, gClassicFrames, gFramesScrollingInterval;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
|
@ -17,6 +17,7 @@ function test() {
|
|||
gPanel = aPanel;
|
||||
gDebugger = gPanel.panelWin;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, ".html", 26).then(performTest);
|
||||
|
||||
|
@ -30,14 +31,20 @@ function performTest() {
|
|||
"Should only be getting stack frames while paused.");
|
||||
is(gFrames.itemCount, gDebugger.gCallStackPageSize,
|
||||
"Should have only the max limit of frames.");
|
||||
is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize,
|
||||
"Should have only the max limit of frames in the mirrored view as well.")
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
|
||||
is(gFrames.itemCount, gDebugger.gCallStackPageSize * 2,
|
||||
"Should now have twice the max limit of frames.");
|
||||
is(gClassicFrames.itemCount, gDebugger.gCallStackPageSize * 2,
|
||||
"Should now have twice the max limit of frames in the mirrored view as well.");
|
||||
|
||||
gDebugger.gThreadClient.addOneTimeListener("framesadded", () => {
|
||||
is(gFrames.itemCount, gDebuggee.gRecurseLimit,
|
||||
"Should have reached the recurse limit.");
|
||||
is(gClassicFrames.itemCount, gDebuggee.gRecurseLimit,
|
||||
"Should have reached the recurse limit in the mirrored view as well.");
|
||||
|
||||
gDebugger.gThreadClient.resume(() => {
|
||||
window.clearInterval(gFramesScrollingInterval);
|
||||
|
@ -60,4 +67,5 @@ registerCleanupFunction(function() {
|
|||
gPanel = null;
|
||||
gDebugger = null;
|
||||
gFrames = null;
|
||||
gClassicFrames = null;
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
const TAB_URL = EXAMPLE_URL + "doc_recursion-stack.html";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gFrames;
|
||||
let gFrames, gClassicFrames;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
|
@ -17,6 +17,7 @@ function test() {
|
|||
gPanel = aPanel;
|
||||
gDebugger = gPanel.panelWin;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, ".html", 18).then(performTest);
|
||||
gDebuggee.evalCall();
|
||||
|
@ -28,10 +29,14 @@ function performTest() {
|
|||
"Should only be getting stack frames while paused.");
|
||||
is(gFrames.itemCount, 2,
|
||||
"Should have two frames.");
|
||||
is(gClassicFrames.itemCount, 2,
|
||||
"Should also have two frames in the mirrored view.");
|
||||
|
||||
gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
|
||||
is(gFrames.itemCount, 0,
|
||||
"Should have no frames after resume.");
|
||||
is(gClassicFrames.itemCount, 0,
|
||||
"Should also have no frames in the mirrored view after resume.");
|
||||
|
||||
closeDebuggerAndFinish(gPanel);
|
||||
}, true);
|
||||
|
@ -45,4 +50,5 @@ registerCleanupFunction(function() {
|
|||
gPanel = null;
|
||||
gDebugger = null;
|
||||
gFrames = null;
|
||||
gClassicFrames = null;
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gEditor, gSources, gFrames;
|
||||
let gEditor, gSources, gFrames, gClassicFrames;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
|
@ -20,6 +20,7 @@ function test() {
|
|||
gEditor = gDebugger.DebuggerView.editor;
|
||||
gSources = gDebugger.DebuggerView.Sources;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6)
|
||||
.then(initialChecks)
|
||||
|
@ -40,6 +41,8 @@ function initialChecks() {
|
|||
"Should only be getting stack frames while paused.");
|
||||
is(gFrames.itemCount, 4,
|
||||
"Should have four frames.");
|
||||
is(gClassicFrames.itemCount, 4,
|
||||
"Should also have four frames in the mirrored view.");
|
||||
}
|
||||
|
||||
function testNewestTwoFrames() {
|
||||
|
@ -47,6 +50,8 @@ function testNewestTwoFrames() {
|
|||
|
||||
is(gFrames.selectedIndex, 3,
|
||||
"Newest frame should be selected by default.");
|
||||
is(gClassicFrames.selectedIndex, 0,
|
||||
"Newest frame should be selected in the mirrored view as well.");
|
||||
is(gSources.selectedIndex, 1,
|
||||
"The second source is selected in the widget.");
|
||||
ok(isCaretPos(gPanel, 6),
|
||||
|
@ -63,6 +68,8 @@ function testNewestTwoFrames() {
|
|||
|
||||
is(gFrames.selectedIndex, 2,
|
||||
"Third frame should be selected after click.");
|
||||
is(gClassicFrames.selectedIndex, 1,
|
||||
"Third frame should be selected in the mirrored view as well.");
|
||||
is(gSources.selectedIndex, 1,
|
||||
"The second source is still selected in the widget.");
|
||||
ok(isCaretPos(gPanel, 6),
|
||||
|
@ -83,9 +90,11 @@ function testNewestTwoFrames() {
|
|||
function testOldestTwoFrames() {
|
||||
let deferred = promise.defer();
|
||||
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 5).then(() => {
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 5).then(waitForTick).then(() => {
|
||||
is(gFrames.selectedIndex, 1,
|
||||
"Second frame should be selected after click.");
|
||||
is(gClassicFrames.selectedIndex, 2,
|
||||
"Second frame should be selected in the mirrored view as well.");
|
||||
is(gSources.selectedIndex, 0,
|
||||
"The first source is now selected in the widget.");
|
||||
ok(isCaretPos(gPanel, 5),
|
||||
|
@ -102,6 +111,8 @@ function testOldestTwoFrames() {
|
|||
|
||||
is(gFrames.selectedIndex, 0,
|
||||
"Oldest frame should be selected after click.");
|
||||
is(gClassicFrames.selectedIndex, 3,
|
||||
"Oldest frame should be selected in the mirrored view as well.");
|
||||
is(gSources.selectedIndex, 0,
|
||||
"The first source is still selected in the widget.");
|
||||
ok(isCaretPos(gPanel, 5),
|
||||
|
@ -118,7 +129,7 @@ function testOldestTwoFrames() {
|
|||
});
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
gFrames.getItemAtIndex(1).target,
|
||||
gDebugger.document.querySelector("#stackframe-2"),
|
||||
gDebugger);
|
||||
|
||||
return deferred.promise;
|
||||
|
@ -130,6 +141,8 @@ function testAfterResume() {
|
|||
gDebugger.once(gDebugger.EVENTS.AFTER_FRAMES_CLEARED, () => {
|
||||
is(gFrames.itemCount, 0,
|
||||
"Should have no frames after resume.");
|
||||
is(gClassicFrames.itemCount, 0,
|
||||
"Should have no frames in the mirrored view as well.");
|
||||
ok(isCaretPos(gPanel, 5),
|
||||
"Editor caret location is correct after resume.");
|
||||
is(gEditor.getDebugLocation(), null,
|
||||
|
@ -150,5 +163,6 @@ registerCleanupFunction(function() {
|
|||
gDebugger = null;
|
||||
gEditor = null;
|
||||
gFrames = null;
|
||||
gClassicFrames = null;
|
||||
});
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gEditor, gSources, gFrames;
|
||||
let gEditor, gSources, gFrames, gClassicFrames;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
|
@ -20,6 +20,7 @@ function test() {
|
|||
gEditor = gDebugger.DebuggerView.editor;
|
||||
gSources = gDebugger.DebuggerView.Sources;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6).then(performTest);
|
||||
gDebuggee.firstCall();
|
||||
|
@ -29,16 +30,20 @@ function test() {
|
|||
function performTest() {
|
||||
is(gFrames.selectedIndex, 3,
|
||||
"Newest frame should be selected by default.");
|
||||
is(gClassicFrames.selectedIndex, 0,
|
||||
"Newest frame should also be selected in the mirrored view.");
|
||||
is(gSources.selectedIndex, 1,
|
||||
"The second source is selected in the widget.");
|
||||
is(gEditor.getText().search(/firstCall/), -1,
|
||||
"The first source is not displayed.");
|
||||
is(gEditor.getText().search(/debugger/), 172,
|
||||
"The second source is displayed.")
|
||||
"The second source is displayed.");
|
||||
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 6).then(() => {
|
||||
waitForSourceAndCaret(gPanel, "-01.js", 6).then(waitForTick).then(() => {
|
||||
is(gFrames.selectedIndex, 0,
|
||||
"Oldest frame should be selected after click.");
|
||||
is(gClassicFrames.selectedIndex, 3,
|
||||
"Oldest frame should also be selected in the mirrored view.");
|
||||
is(gSources.selectedIndex, 0,
|
||||
"The first source is now selected in the widget.");
|
||||
is(gEditor.getText().search(/firstCall/), 118,
|
||||
|
@ -46,7 +51,24 @@ function performTest() {
|
|||
is(gEditor.getText().search(/debugger/), -1,
|
||||
"The second source is not displayed.");
|
||||
|
||||
resumeDebuggerThenCloseAndFinish(gPanel);
|
||||
waitForSourceAndCaret(gPanel, "-02.js", 6).then(waitForTick).then(() => {
|
||||
is(gFrames.selectedIndex, 3,
|
||||
"Newest frame should be selected again after click.");
|
||||
is(gClassicFrames.selectedIndex, 0,
|
||||
"Newest frame should also be selected again in the mirrored view.");
|
||||
is(gSources.selectedIndex, 1,
|
||||
"The second source is selected in the widget.");
|
||||
is(gEditor.getText().search(/firstCall/), -1,
|
||||
"The first source is not displayed.");
|
||||
is(gEditor.getText().search(/debugger/), 172,
|
||||
"The second source is displayed.");
|
||||
|
||||
resumeDebuggerThenCloseAndFinish(gPanel);
|
||||
});
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
gDebugger.document.querySelector("#classic-stackframe-0"),
|
||||
gDebugger);
|
||||
});
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "mousedown" },
|
||||
|
@ -62,4 +84,5 @@ registerCleanupFunction(function() {
|
|||
gEditor = null;
|
||||
gSources = null;
|
||||
gFrames = null;
|
||||
gClassicFrames = null;
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
const TAB_URL = EXAMPLE_URL + "doc_script-switching-01.html";
|
||||
|
||||
let gTab, gDebuggee, gPanel, gDebugger;
|
||||
let gEditor, gSources, gFrames, gToolbar;
|
||||
let gEditor, gSources, gFrames, gClassicFrames, gToolbar;
|
||||
|
||||
function test() {
|
||||
initDebugger(TAB_URL).then(([aTab, aDebuggee, aPanel]) => {
|
||||
|
@ -21,6 +21,7 @@ function test() {
|
|||
gEditor = gDebugger.DebuggerView.editor;
|
||||
gSources = gDebugger.DebuggerView.Sources;
|
||||
gFrames = gDebugger.DebuggerView.StackFrames;
|
||||
gClassicFrames = gDebugger.DebuggerView.StackFramesClassicList;
|
||||
gToolbar = gDebugger.DebuggerView.Toolbar;
|
||||
|
||||
waitForSourceAndCaretAndScopes(gPanel, "-02.js", 6).then(performTest);
|
||||
|
@ -53,13 +54,15 @@ function performTest() {
|
|||
|
||||
function selectBottomFrame() {
|
||||
let updated = waitForDebuggerEvents(gPanel, gDebugger.EVENTS.FETCHED_SCOPES);
|
||||
gFrames.selectedIndex = 0;
|
||||
gClassicFrames.selectedIndex = gClassicFrames.itemCount - 1;
|
||||
return updated.then(waitForTick);
|
||||
}
|
||||
|
||||
function testBottomFrame(debugLocation) {
|
||||
is(gFrames.selectedIndex, 0,
|
||||
"Oldest frame should be selected after click.");
|
||||
is(gClassicFrames.selectedIndex, gFrames.itemCount - 1,
|
||||
"Oldest frame should also be selected in the mirrored view.");
|
||||
is(gSources.selectedIndex, 0,
|
||||
"The first source is now selected in the widget.");
|
||||
is(gEditor.getText().search(/firstCall/), 118,
|
||||
|
@ -82,6 +85,8 @@ function performTest() {
|
|||
function testTopFrame(frameIndex) {
|
||||
is(gFrames.selectedIndex, frameIndex,
|
||||
"Topmost frame should be selected after click.");
|
||||
is(gClassicFrames.selectedIndex, gFrames.itemCount - frameIndex - 1,
|
||||
"Topmost frame should also be selected in the mirrored view.");
|
||||
is(gSources.selectedIndex, 1,
|
||||
"The second source is now selected in the widget.");
|
||||
is(gEditor.getText().search(/firstCall/), -1,
|
||||
|
@ -99,5 +104,6 @@ registerCleanupFunction(function() {
|
|||
gEditor = null;
|
||||
gSources = null;
|
||||
gFrames = null;
|
||||
gClassicFrames = null;
|
||||
gToolbar = null;
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"use strict";
|
||||
|
||||
const { Cc, Ci, Cu, Cr } = require("chrome");
|
||||
const promise = require("sdk/core/promise");
|
||||
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const { WebGLFront } = require("devtools/server/actors/webgl");
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Cu.import("resource:///modules/devtools/SideMenuWidget.jsm");
|
|||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
|
||||
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
|
||||
const promise = require("sdk/core/promise");
|
||||
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {Tooltip} = require("devtools/shared/widgets/Tooltip");
|
||||
const Editor = require("devtools/sourceeditor/editor");
|
||||
|
@ -28,7 +28,13 @@ const EVENTS = {
|
|||
SOURCES_SHOWN: "ShaderEditor:SourcesShown",
|
||||
|
||||
// When a shader's source was edited and compiled via the editor.
|
||||
SHADER_COMPILED: "ShaderEditor:ShaderCompiled"
|
||||
SHADER_COMPILED: "ShaderEditor:ShaderCompiled",
|
||||
|
||||
// When the UI is reset from tab navigation
|
||||
UI_RESET: "ShaderEditor:UIReset",
|
||||
|
||||
// When the editor's error markers are all removed
|
||||
EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned"
|
||||
};
|
||||
|
||||
const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties"
|
||||
|
@ -114,15 +120,17 @@ let EventsHandler = {
|
|||
_onTabNavigated: function(event) {
|
||||
switch (event) {
|
||||
case "will-navigate": {
|
||||
// Make sure the backend is prepared to handle WebGL contexts.
|
||||
gFront.setup({ reload: false });
|
||||
Task.spawn(function() {
|
||||
// Make sure the backend is prepared to handle WebGL contexts.
|
||||
gFront.setup({ reload: false });
|
||||
|
||||
// Reset UI.
|
||||
ShadersListView.empty();
|
||||
ShadersEditorsView.setText({ vs: "", fs: "" });
|
||||
$("#reload-notice").hidden = true;
|
||||
$("#waiting-notice").hidden = false;
|
||||
$("#content").hidden = true;
|
||||
// Reset UI.
|
||||
ShadersListView.empty();
|
||||
$("#reload-notice").hidden = true;
|
||||
$("#waiting-notice").hidden = false;
|
||||
yield ShadersEditorsView.setText({ vs: "", fs: "" });
|
||||
$("#content").hidden = true;
|
||||
}).then(() => window.emit(EVENTS.UI_RESET));
|
||||
break;
|
||||
}
|
||||
case "navigate": {
|
||||
|
@ -272,13 +280,16 @@ let ShadersListView = Heritage.extend(WidgetMethods, {
|
|||
]);
|
||||
}
|
||||
function showSources([vertexShaderText, fragmentShaderText]) {
|
||||
ShadersEditorsView.setText({
|
||||
return ShadersEditorsView.setText({
|
||||
vs: vertexShaderText,
|
||||
fs: fragmentShaderText
|
||||
});
|
||||
}
|
||||
|
||||
getShaders().then(getSources).then(showSources).then(null, Cu.reportError);
|
||||
getShaders()
|
||||
.then(getSources)
|
||||
.then(showSources)
|
||||
.then(null, Cu.reportError);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -351,19 +362,24 @@ let ShadersEditorsView = {
|
|||
* An object containing the following properties
|
||||
* - vs: the vertex shader source code
|
||||
* - fs: the fragment shader source code
|
||||
* @return object
|
||||
* A promise resolving upon completion of text setting.
|
||||
*/
|
||||
setText: function(sources) {
|
||||
let view = this;
|
||||
function setTextAndClearHistory(editor, text) {
|
||||
editor.setText(text);
|
||||
editor.clearHistory();
|
||||
}
|
||||
|
||||
this._toggleListeners("off");
|
||||
this._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs));
|
||||
this._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs));
|
||||
this._toggleListeners("on");
|
||||
|
||||
window.emit(EVENTS.SOURCES_SHOWN, sources);
|
||||
return Task.spawn(function() {
|
||||
yield view._toggleListeners("off");
|
||||
yield promise.all([
|
||||
view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)),
|
||||
view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs))
|
||||
]);
|
||||
yield view._toggleListeners("on");
|
||||
}).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -372,10 +388,12 @@ let ShadersEditorsView = {
|
|||
* @param string type
|
||||
* Specifies for which shader type should an editor be retrieved,
|
||||
* either are "vs" for a vertex, or "fs" for a fragment shader.
|
||||
* @return object
|
||||
* Returns a promise that resolves to an editor instance
|
||||
*/
|
||||
_getEditor: function(type) {
|
||||
if ($("#content").hidden) {
|
||||
return promise.reject(null);
|
||||
return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created."));
|
||||
}
|
||||
if (this._editorPromises.has(type)) {
|
||||
return this._editorPromises.get(type);
|
||||
|
@ -399,14 +417,16 @@ let ShadersEditorsView = {
|
|||
*
|
||||
* @param string flag
|
||||
* Either "on" to enable the event listeners, "off" to disable them.
|
||||
* @return object
|
||||
* A promise resolving upon completion of toggling the listeners.
|
||||
*/
|
||||
_toggleListeners: function(flag) {
|
||||
["vs", "fs"].forEach(type => {
|
||||
this._getEditor(type).then(editor => {
|
||||
return promise.all(["vs", "fs"].map(type => {
|
||||
return this._getEditor(type).then(editor => {
|
||||
editor[flag]("focus", this["_" + type + "Focused"]);
|
||||
editor[flag]("change", this["_" + type + "Changed"]);
|
||||
});
|
||||
});
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -486,7 +506,7 @@ let ShadersEditorsView = {
|
|||
}
|
||||
function sanitizeValidMatches(e) {
|
||||
return {
|
||||
// Drivers might yield retarded line numbers under some obscure
|
||||
// Drivers might yield confusing line numbers under some obscure
|
||||
// circumstances. Don't throw the errors away in those cases,
|
||||
// just display them on the currently edited line.
|
||||
line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1,
|
||||
|
@ -554,6 +574,7 @@ let ShadersEditorsView = {
|
|||
editor.removeAllMarkers("errors");
|
||||
this._errors[type].forEach(e => editor.removeLineClass(e.line));
|
||||
this._errors[type].length = 0;
|
||||
window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -14,8 +14,7 @@ function ifWebGLSupported() {
|
|||
yield reloaded;
|
||||
|
||||
let navigated = navigate(target, MULTIPLE_CONTEXTS_URL);
|
||||
let secondProgram = yield once(gFront, "program-linked");
|
||||
let thirdProgram = yield once(gFront, "program-linked");
|
||||
let [secondProgram, thirdProgram] = yield getPrograms(gFront, 2);
|
||||
yield navigated;
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
|
@ -56,9 +55,3 @@ function ifWebGLSupported() {
|
|||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -8,14 +8,18 @@
|
|||
|
||||
function ifWebGLSupported() {
|
||||
let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
|
||||
let { gFront, ShadersEditorsView } = panel.panelWin;
|
||||
let { gFront, ShadersEditorsView, EVENTS } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
yield once(gFront, "program-linked");
|
||||
yield promise.all([
|
||||
once(gFront, "program-linked"),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]);
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
||||
|
||||
is(vsEditor.getText().indexOf("gl_Position"), 170,
|
||||
"The vertex shader editor contains the correct text.");
|
||||
is(fsEditor.getText().indexOf("gl_FragColor"), 97,
|
||||
|
|
|
@ -11,55 +11,61 @@ function ifWebGLSupported() {
|
|||
let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
yield once(gFront, "program-linked");
|
||||
yield promise.all([
|
||||
once(gFront, "program-linked"),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]);
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
||||
vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
|
||||
let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
let [, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
checkHasVertFirstError(true, vertError);
|
||||
checkHasVertSecondError(false, vertError);
|
||||
info("Error marks added in the vertex shader editor.");
|
||||
|
||||
vsEditor.insertText(" ", { line: 1, ch: 0 });
|
||||
yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
|
||||
is(vsEditor.getText(1), " precision lowp float;", "Typed space.");
|
||||
checkHasVertFirstError(false, vertError);
|
||||
checkHasVertSecondError(false, vertError);
|
||||
info("Error marks removed while typing in the vertex shader editor.");
|
||||
|
||||
let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
[, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
checkHasVertFirstError(true, vertError);
|
||||
checkHasVertSecondError(false, vertError);
|
||||
info("Error marks were re-added after recompiling the vertex shader.");
|
||||
|
||||
fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
|
||||
let fragError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
let [, fragError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
checkHasVertFirstError(true, vertError);
|
||||
checkHasVertSecondError(false, vertError);
|
||||
checkHasFragError(true, fragError);
|
||||
info("Error marks added in the fragment shader editor.");
|
||||
|
||||
fsEditor.insertText(" ", { line: 1, ch: 0 });
|
||||
yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
|
||||
is(fsEditor.getText(1), " precision lowp float;", "Typed space.");
|
||||
checkHasVertFirstError(true, vertError);
|
||||
checkHasVertSecondError(false, vertError);
|
||||
checkHasFragError(false, fragError);
|
||||
info("Error marks removed while typing in the fragment shader editor.");
|
||||
|
||||
let fragError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
[, fragError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
checkHasVertFirstError(true, vertError);
|
||||
checkHasVertSecondError(false, vertError);
|
||||
checkHasFragError(true, fragError);
|
||||
info("Error marks were re-added after recompiling the fragment shader.");
|
||||
|
||||
vsEditor.replaceText("2", { line: 3, ch: 19 }, { line: 3, ch: 20 });
|
||||
yield once(panel.panelWin, EVENTS.EDITOR_ERROR_MARKERS_REMOVED);
|
||||
checkHasVertFirstError(false, vertError);
|
||||
checkHasVertSecondError(false, vertError);
|
||||
checkHasFragError(true, fragError);
|
||||
info("Error marks removed while typing in the vertex shader editor again.");
|
||||
|
||||
let vertError = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
[, vertError] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
checkHasVertFirstError(true, vertError);
|
||||
checkHasVertSecondError(true, vertError);
|
||||
checkHasFragError(true, fragError);
|
||||
|
@ -148,9 +154,3 @@ function ifWebGLSupported() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -11,7 +11,10 @@ function ifWebGLSupported() {
|
|||
let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
yield once(gFront, "program-linked");
|
||||
yield promise.all([
|
||||
once(gFront, "program-linked"),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]);
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
@ -51,9 +54,3 @@ function ifWebGLSupported() {
|
|||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -7,10 +7,13 @@
|
|||
|
||||
function ifWebGLSupported() {
|
||||
let [target, debuggee, panel] = yield initShaderEditor(SIMPLE_CANVAS_URL);
|
||||
let { gFront, $, ShadersListView, ShadersEditorsView } = panel.panelWin;
|
||||
let { gFront, $, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
yield once(gFront, "program-linked");
|
||||
yield promise.all([
|
||||
once(gFront, "program-linked"),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]);
|
||||
|
||||
is($("#reload-notice").hidden, true,
|
||||
"The 'reload this page' notice should be hidden after linking.");
|
||||
|
@ -39,6 +42,7 @@ function ifWebGLSupported() {
|
|||
navigate(target, "about:blank");
|
||||
|
||||
yield navigating;
|
||||
yield once(panel.panelWin, EVENTS.UI_RESET);
|
||||
|
||||
is($("#reload-notice").hidden, true,
|
||||
"The 'reload this page' notice should be hidden while navigating.");
|
||||
|
@ -54,19 +58,17 @@ function ifWebGLSupported() {
|
|||
is(ShadersListView.selectedIndex, -1,
|
||||
"The shaders list has a negative index.");
|
||||
|
||||
try {
|
||||
yield ShadersEditorsView._getEditor("vs");
|
||||
yield ShadersEditorsView._getEditor("vs").then(() => {
|
||||
ok(false, "The promise for a vertex shader editor should be rejected.");
|
||||
} catch (e) {
|
||||
}, () => {
|
||||
ok(true, "The vertex shader editors wasn't initialized.");
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
yield ShadersEditorsView._getEditor("fs");
|
||||
yield ShadersEditorsView._getEditor("fs").then(() => {
|
||||
ok(false, "The promise for a fragment shader editor should be rejected.");
|
||||
} catch (e) {
|
||||
}, () => {
|
||||
ok(true, "The fragment shader editors wasn't initialized.");
|
||||
}
|
||||
});
|
||||
|
||||
yield navigated;
|
||||
|
||||
|
|
|
@ -14,8 +14,10 @@ function ifWebGLSupported() {
|
|||
});
|
||||
|
||||
reload(target);
|
||||
let firstProgramActor = yield once(gFront, "program-linked");
|
||||
let secondProgramActor = yield once(gFront, "program-linked");
|
||||
let [firstProgramActor, secondProgramActor] = yield promise.all([
|
||||
getPrograms(gFront, 2),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]).then(([programs]) => programs);
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
|
|
@ -11,8 +11,10 @@ function ifWebGLSupported() {
|
|||
let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
let firstProgramActor = yield once(gFront, "program-linked");
|
||||
let secondProgramActor = yield once(gFront, "program-linked");
|
||||
let [firstProgramActor, secondProgramActor] = yield promise.all([
|
||||
getPrograms(gFront, 2),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]).then(([programs]) => programs);
|
||||
|
||||
yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
|
||||
yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
|
||||
|
@ -59,9 +61,3 @@ function getBlackBoxCheckbox(aPanel, aIndex) {
|
|||
return aPanel.panelWin.document.querySelectorAll(
|
||||
".side-menu-widget-item-checkbox")[aIndex];
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -7,10 +7,14 @@
|
|||
|
||||
function ifWebGLSupported() {
|
||||
let [target, debuggee, panel] = yield initShaderEditor(MULTIPLE_CONTEXTS_URL);
|
||||
let { gFront, ShadersListView, ShadersEditorsView } = panel.panelWin;
|
||||
let { EVENTS, gFront, ShadersListView, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
let programActor = yield once(gFront, "program-linked");
|
||||
let [programActor] = yield promise.all([
|
||||
getPrograms(gFront, 1),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]).then(([programs]) => programs);
|
||||
|
||||
let programItem = ShadersListView.selectedItem;
|
||||
|
||||
is(programItem.attachment.programActor, programActor,
|
||||
|
|
|
@ -14,8 +14,10 @@ function ifWebGLSupported() {
|
|||
});
|
||||
|
||||
reload(target);
|
||||
let firstProgramActor = yield once(gFront, "program-linked");
|
||||
let secondProgramActor = yield once(gFront, "program-linked");
|
||||
let [firstProgramActor, secondProgramActor] = yield promise.all([
|
||||
getPrograms(gFront, 2),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]).then(([programs]) => programs);
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
@ -89,9 +91,3 @@ function getBlackBoxCheckbox(aPanel, aIndex) {
|
|||
return aPanel.panelWin.document.querySelectorAll(
|
||||
".side-menu-widget-item-checkbox")[aIndex];
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,10 @@ function ifWebGLSupported() {
|
|||
let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
let firstProgramActor = yield once(gFront, "program-linked");
|
||||
let secondProgramActor = yield once(gFront, "program-linked");
|
||||
let [firstProgramActor, secondProgramActor] = yield promise.all([
|
||||
getPrograms(gFront, 2),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]).then(([programs]) => programs);
|
||||
|
||||
yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 127, g: 127, b: 127, a: 255 }, true);
|
||||
yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 127, b: 127, a: 127 }, true);
|
||||
|
@ -45,9 +47,3 @@ function getItemLabel(aPanel, aIndex) {
|
|||
return aPanel.panelWin.document.querySelectorAll(
|
||||
".side-menu-widget-item-label")[aIndex];
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -19,23 +19,17 @@ function ifWebGLSupported() {
|
|||
|
||||
reload(target);
|
||||
|
||||
let firstProgramActor = yield once(gFront, "program-linked");
|
||||
|
||||
is(ShadersListView.itemCount, 1,
|
||||
"The shaders list contains one entry.");
|
||||
is(ShadersListView.selectedItem, ShadersListView.items[0],
|
||||
"The shaders list has a correct item selected.");
|
||||
is(ShadersListView.selectedIndex, 0,
|
||||
"The shaders list has a correct index selected.");
|
||||
|
||||
let secondProgramActor = yield once(gFront, "program-linked");
|
||||
|
||||
is(ShadersListView.itemCount, 2,
|
||||
"The shaders list contains two entries.");
|
||||
is(ShadersListView.selectedItem, ShadersListView.items[0],
|
||||
"The shaders list has a correct item selected.");
|
||||
is(ShadersListView.selectedIndex, 0,
|
||||
"The shaders list has a correct index selected.");
|
||||
let [firstProgramActor, secondProgramActor] = yield promise.all([
|
||||
getPrograms(gFront, 2, (actors) => {
|
||||
// Fired upon each actor addition, we want to check only
|
||||
// after the first actor has been added so we can test state
|
||||
if (actors.length === 1)
|
||||
checkFirstProgram();
|
||||
if (actors.length === 2)
|
||||
checkSecondProgram();
|
||||
}),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]).then(([programs, ]) => programs);
|
||||
|
||||
is(ShadersListView.labels[0], L10N.getFormatStr("shadersList.programLabel", 0),
|
||||
"The correct first label is shown in the shaders list.");
|
||||
|
@ -73,10 +67,21 @@ function ifWebGLSupported() {
|
|||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, deferred.resolve);
|
||||
return deferred.promise;
|
||||
function checkFirstProgram () {
|
||||
is(ShadersListView.itemCount, 1,
|
||||
"The shaders list contains one entry.");
|
||||
is(ShadersListView.selectedItem, ShadersListView.items[0],
|
||||
"The shaders list has a correct item selected.");
|
||||
is(ShadersListView.selectedIndex, 0,
|
||||
"The shaders list has a correct index selected.");
|
||||
}
|
||||
function checkSecondProgram () {
|
||||
is(ShadersListView.itemCount, 2,
|
||||
"The shaders list contains two entries.");
|
||||
is(ShadersListView.selectedItem, ShadersListView.items[0],
|
||||
"The shaders list has a correct item selected.");
|
||||
is(ShadersListView.selectedIndex, 0,
|
||||
"The shaders list has a correct index selected.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,10 @@ function ifWebGLSupported() {
|
|||
let { gFront, $, EVENTS, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
yield once(gFront, "program-linked");
|
||||
yield promise.all([
|
||||
once(gFront, "program-linked"),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]);
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
@ -68,9 +71,3 @@ function ifWebGLSupported() {
|
|||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -11,13 +11,16 @@ function ifWebGLSupported() {
|
|||
let { gFront, EVENTS, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
yield once(gFront, "program-linked");
|
||||
yield promise.all([
|
||||
once(gFront, "program-linked"),
|
||||
once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
]);
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
||||
vsEditor.replaceText("vec3", { line: 7, ch: 22 }, { line: 7, ch: 26 });
|
||||
let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
|
||||
ok(error,
|
||||
"The new vertex shader source was compiled with errors.");
|
||||
|
@ -33,7 +36,7 @@ function ifWebGLSupported() {
|
|||
"An assignment error is contained in the linkage status.");
|
||||
|
||||
fsEditor.replaceText("vec4", { line: 2, ch: 14 }, { line: 2, ch: 18 });
|
||||
let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
|
||||
ok(error,
|
||||
"The new fragment shader source was compiled with errors.");
|
||||
|
@ -50,11 +53,11 @@ function ifWebGLSupported() {
|
|||
yield ensurePixelIs(debuggee, { x: 511, y: 511 }, { r: 0, g: 255, b: 0, a: 255 }, true);
|
||||
|
||||
vsEditor.replaceText("vec4", { line: 7, ch: 22 }, { line: 7, ch: 26 });
|
||||
let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
ok(!error, "The new vertex shader source was compiled successfully.");
|
||||
|
||||
fsEditor.replaceText("vec3", { line: 2, ch: 14 }, { line: 2, ch: 18 });
|
||||
let error = yield once(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
let [, error] = yield onceSpread(panel.panelWin, EVENTS.SHADER_COMPILED);
|
||||
ok(!error, "The new fragment shader source was compiled successfully.");
|
||||
|
||||
yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 0, b: 0, a: 255 }, true);
|
||||
|
@ -63,9 +66,3 @@ function ifWebGLSupported() {
|
|||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, (aName, aData) => deferred.resolve(aData));
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,13 @@ function ifWebGLSupported() {
|
|||
let { gFront, EVENTS, ShadersListView, ShadersEditorsView } = panel.panelWin;
|
||||
|
||||
reload(target);
|
||||
let firstProgramActor = yield once(gFront, "program-linked");
|
||||
let secondProgramActor = yield once(gFront, "program-linked");
|
||||
|
||||
yield promise.all([
|
||||
once(gFront, "program-linked"),
|
||||
once(gFront, "program-linked")
|
||||
]);
|
||||
|
||||
yield once(panel.panelWin, EVENTS.SOURCES_SHOWN)
|
||||
|
||||
let vsEditor = yield ShadersEditorsView._getEditor("vs");
|
||||
let fsEditor = yield ShadersEditorsView._getEditor("fs");
|
||||
|
@ -78,9 +83,3 @@ function ifWebGLSupported() {
|
|||
yield teardown(panel);
|
||||
finish();
|
||||
}
|
||||
|
||||
function once(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, deferred.resolve);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ function ifWebGLSupported() {
|
|||
let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
|
||||
front.setup({ reload: true });
|
||||
|
||||
let firstProgramActor = yield once(front, "program-linked");
|
||||
let secondProgramActor = yield once(front, "program-linked");
|
||||
let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
|
||||
|
||||
isnot(firstProgramActor, secondProgramActor,
|
||||
"Two distinct program actors were recevide from two separate contexts.");
|
||||
|
|
|
@ -10,8 +10,8 @@ function ifWebGLSupported() {
|
|||
let [target, debuggee, front] = yield initBackend(MULTIPLE_CONTEXTS_URL);
|
||||
front.setup({ reload: true });
|
||||
|
||||
let firstProgramActor = yield once(front, "program-linked");
|
||||
let secondProgramActor = yield once(front, "program-linked");
|
||||
let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
|
||||
|
||||
let firstFragmentShader = yield firstProgramActor.getFragmentShader();
|
||||
let secondFragmentShader = yield secondProgramActor.getFragmentShader();
|
||||
|
||||
|
|
|
@ -16,8 +16,7 @@ function ifWebGLSupported() {
|
|||
ok(true, "The cached programs behave correctly before the navigation.");
|
||||
|
||||
navigate(target, MULTIPLE_CONTEXTS_URL);
|
||||
let secondProgram = yield once(front, "program-linked");
|
||||
let thirdProgram = yield once(front, "program-linked");
|
||||
let [secondProgram, thirdProgram] = yield getPrograms(front, 2);
|
||||
yield checkSecondCachedPrograms(firstProgram, [secondProgram, thirdProgram]);
|
||||
yield checkHighlightingInTheSecondPage(secondProgram, thirdProgram);
|
||||
ok(true, "The cached programs behave correctly after the navigation.");
|
||||
|
|
|
@ -23,8 +23,7 @@ function ifWebGLSupported() {
|
|||
// 1. Perform a simple navigation.
|
||||
|
||||
navigate(target, MULTIPLE_CONTEXTS_URL);
|
||||
let secondProgram = yield once(front, "program-linked");
|
||||
let thirdProgram = yield once(front, "program-linked");
|
||||
let [secondProgram, thirdProgram] = yield getPrograms(front, 2);
|
||||
let programs = yield front.getPrograms();
|
||||
is(programs.length, 2,
|
||||
"The second and third programs should be returned by a call to getPrograms().");
|
||||
|
@ -65,8 +64,7 @@ function ifWebGLSupported() {
|
|||
is(programs.length, 0,
|
||||
"There should be no cached program actors yet.");
|
||||
|
||||
yield once(front, "program-linked");
|
||||
yield once(front, "program-linked");
|
||||
yield getPrograms(front, 2);
|
||||
yield globalCreated;
|
||||
let programs = yield front.getPrograms();
|
||||
is(programs.length, 2,
|
||||
|
|
|
@ -10,8 +10,7 @@ function ifWebGLSupported() {
|
|||
let [target, debuggee, front] = yield initBackend(OVERLAPPING_GEOMETRY_CANVAS_URL);
|
||||
front.setup({ reload: true });
|
||||
|
||||
let firstProgramActor = yield once(front, "program-linked");
|
||||
let secondProgramActor = yield once(front, "program-linked");
|
||||
let [firstProgramActor, secondProgramActor] = yield getPrograms(front, 2);
|
||||
|
||||
yield ensurePixelIs(debuggee, { x: 0, y: 0 }, { r: 255, g: 255, b: 0, a: 255 }, true);
|
||||
yield ensurePixelIs(debuggee, { x: 64, y: 64 }, { r: 0, g: 255, b: 255, a: 255 }, true);
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
window.onload = function() {
|
||||
canvas = document.querySelector("canvas");
|
||||
gl = canvas.getContext("webgl");
|
||||
gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
initProgram(0);
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
window.onload = function() {
|
||||
for (let i = 0; i < 2; i++) {
|
||||
canvas[i] = document.querySelector("#canvas" + (i + 1));
|
||||
gl[i] = canvas[i].getContext("webgl");
|
||||
gl[i] = canvas[i].getContext("webgl", { preserveDrawingBuffer: true });
|
||||
gl[i].clearColor(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
initProgram(i);
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
window.onload = function() {
|
||||
canvas = document.querySelector("canvas");
|
||||
gl = canvas.getContext("webgl");
|
||||
gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
initProgram(0);
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
|
||||
window.onload = function() {
|
||||
canvas = document.querySelector("canvas");
|
||||
gl = canvas.getContext("webgl");
|
||||
gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
|
||||
let shaderProgram = gl.createProgram();
|
||||
let vertexShader, fragmentShader;
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
window.onload = function() {
|
||||
canvas = document.querySelector("canvas");
|
||||
gl = canvas.getContext("webgl");
|
||||
gl = canvas.getContext("webgl", { preserveDrawingBuffer: true });
|
||||
gl.clearColor(0.0, 0.0, 0.0, 1.0);
|
||||
|
||||
initProgram();
|
||||
|
|
|
@ -12,7 +12,7 @@ let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log");
|
|||
Services.prefs.setBoolPref("devtools.debugger.log", true);
|
||||
|
||||
let { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
|
||||
let { Promise: promise } = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
|
||||
let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
|
||||
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
|
||||
|
@ -124,14 +124,14 @@ function once(aTarget, aEventName, aUseCapture = false) {
|
|||
let deferred = promise.defer();
|
||||
|
||||
for (let [add, remove] of [
|
||||
["on", "off"], // Use event emitter before DOM events for consistency
|
||||
["addEventListener", "removeEventListener"],
|
||||
["addListener", "removeListener"],
|
||||
["on", "off"]
|
||||
["addListener", "removeListener"]
|
||||
]) {
|
||||
if ((add in aTarget) && (remove in aTarget)) {
|
||||
aTarget[add](aEventName, function onEvent(...aArgs) {
|
||||
aTarget[remove](aEventName, onEvent, aUseCapture);
|
||||
deferred.resolve.apply(deferred, aArgs);
|
||||
deferred.resolve(...aArgs);
|
||||
}, aUseCapture);
|
||||
break;
|
||||
}
|
||||
|
@ -140,6 +140,16 @@ function once(aTarget, aEventName, aUseCapture = false) {
|
|||
return deferred.promise;
|
||||
}
|
||||
|
||||
// Hack around `once`, as that only resolves to a single (first) argument
|
||||
// and discards the rest. `onceSpread` is similar, except resolves to an
|
||||
// array of all of the arguments in the handler. These should be consolidated
|
||||
// into the same function, but many tests will need to be changed.
|
||||
function onceSpread(aTarget, aEvent) {
|
||||
let deferred = promise.defer();
|
||||
aTarget.once(aEvent, (...args) => deferred.resolve(args));
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function observe(aNotificationName, aOwnsWeak = false) {
|
||||
info("Waiting for observer notification: '" + aNotificationName + ".");
|
||||
|
||||
|
@ -273,3 +283,27 @@ function teardown(aPanel) {
|
|||
removeTab(aPanel.target.tab)
|
||||
]);
|
||||
}
|
||||
|
||||
// Due to `program-linked` events firing synchronously, we cannot
|
||||
// just yield/chain them together, as then we miss all actors after the
|
||||
// first event since they're fired consecutively. This allows us to capture
|
||||
// all actors and returns an array containing them.
|
||||
//
|
||||
// Takes a `front` object that is an event emitter, the number of
|
||||
// programs that should be listened to and waited on, and an optional
|
||||
// `onAdd` function that calls with the entire actors array on program link
|
||||
function getPrograms(front, count, onAdd) {
|
||||
let actors = [];
|
||||
let deferred = promise.defer();
|
||||
front.on("program-linked", function onLink (actor) {
|
||||
if (actors.length !== count) {
|
||||
actors.push(actor);
|
||||
if (typeof onAdd === 'function') onAdd(actors)
|
||||
}
|
||||
if (actors.length === count) {
|
||||
front.off("program-linked", onLink);
|
||||
deferred.resolve(actors);
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ this.VariablesView = function VariablesView(aParentNode, aFlags = {}) {
|
|||
|
||||
this._parent = aParentNode;
|
||||
this._parent.classList.add("variables-view-container");
|
||||
this._parent.classList.add("theme-body");
|
||||
this._appendEmptyNotice();
|
||||
|
||||
this._onSearchboxInput = this._onSearchboxInput.bind(this);
|
||||
|
|
|
@ -654,7 +654,7 @@ this.WidgetMethods = {
|
|||
* - relaxed: true if this container should allow dupes & degenerates
|
||||
* - attachment: some attached primitive/object for the item
|
||||
* - attributes: a batch of attributes set to the displayed element
|
||||
* - finalize: function invokde when the item is removed
|
||||
* - finalize: function invoked when the item is removed
|
||||
* @return Item
|
||||
* The item associated with the displayed element if an unstaged push,
|
||||
* undefined if the item was staged for a later commit.
|
||||
|
@ -733,6 +733,7 @@ this.WidgetMethods = {
|
|||
}
|
||||
this._widget.removeChild(aItem._target);
|
||||
this._untangleItem(aItem);
|
||||
if (!this.itemCount) this.empty();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1009,7 +1010,9 @@ this.WidgetMethods = {
|
|||
// a redundant selection event, so return early.
|
||||
if (targetElement != prevElement) {
|
||||
this._widget.selectedItem = targetElement;
|
||||
ViewHelpers.dispatchEvent(targetElement || prevElement, "select", aItem);
|
||||
let dispTarget = targetElement || prevElement;
|
||||
let dispName = this.suppressSelectionEvents ? "suppressed-select" : "select";
|
||||
ViewHelpers.dispatchEvent(dispTarget, dispName, aItem);
|
||||
}
|
||||
|
||||
// Updates this container to reflect the information provided by the
|
||||
|
@ -1044,6 +1047,15 @@ this.WidgetMethods = {
|
|||
set selectedValue(aValue)
|
||||
this.selectedItem = this._itemsByValue.get(aValue),
|
||||
|
||||
/**
|
||||
* Specifies if "select" events dispatched from the elements in this container
|
||||
* when their respective items are selected should be suppressed or not.
|
||||
*
|
||||
* If this flag is set to true, then consumers of this container won't
|
||||
* be normally notified when items are selected.
|
||||
*/
|
||||
suppressSelectionEvents: false,
|
||||
|
||||
/**
|
||||
* Focus this container the first time an element is inserted?
|
||||
*
|
||||
|
@ -1305,6 +1317,14 @@ this.WidgetMethods = {
|
|||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut function for getItemForPredicate which works on item attachments.
|
||||
* @see getItemForPredicate
|
||||
*/
|
||||
getItemForAttachment: function(aPredicate, aOwner = this) {
|
||||
return this.getItemForPredicate(e => aPredicate(e.attachment));
|
||||
},
|
||||
|
||||
/**
|
||||
* Finds the index of an item in the container.
|
||||
*
|
||||
|
|
|
@ -236,7 +236,13 @@ Editor.prototype = {
|
|||
|
||||
cm.on("focus", () => this.emit("focus"));
|
||||
cm.on("scroll", () => this.emit("scroll"));
|
||||
cm.on("change", () => this.emit("change"));
|
||||
cm.on("change", () => {
|
||||
this.emit("change");
|
||||
if (!this._lastDirty) {
|
||||
this._lastDirty = true;
|
||||
this.emit("dirty-change");
|
||||
}
|
||||
});
|
||||
cm.on("cursorActivity", (cm) => this.emit("cursorActivity"));
|
||||
|
||||
cm.on("gutterClick", (cm, line, gutter, ev) => {
|
||||
|
@ -614,6 +620,8 @@ Editor.prototype = {
|
|||
setClean: function () {
|
||||
let cm = editors.get(this);
|
||||
this.version = cm.changeGeneration();
|
||||
this._lastDirty = false;
|
||||
this.emit("dirty-change");
|
||||
return this.version;
|
||||
},
|
||||
|
||||
|
|
|
@ -234,7 +234,7 @@ StyleSheetEditor.prototype = {
|
|||
this.emit("source-editor-load");
|
||||
});
|
||||
|
||||
sourceEditor.on("change", this._onPropertyChange);
|
||||
sourceEditor.on("dirty-change", this._onPropertyChange);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -14,7 +14,6 @@ Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope);
|
|||
let FileUtils = tempScope.FileUtils;
|
||||
let NetUtil = tempScope.NetUtil;
|
||||
|
||||
|
||||
function test()
|
||||
{
|
||||
waitForExplicitFinish();
|
||||
|
@ -41,9 +40,22 @@ function test()
|
|||
|
||||
function runTests(editor)
|
||||
{
|
||||
editor.sourceEditor.once("dirty-change", () => {
|
||||
is(editor.sourceEditor.isClean(), false, "Editor is dirty.");
|
||||
ok(editor.summary.classList.contains("unsaved"),
|
||||
"Star icon is present in the corresponding summary.");
|
||||
});
|
||||
let beginCursor = {line: 0, ch: 0};
|
||||
editor.sourceEditor.replaceText("DIRTY TEXT", beginCursor, beginCursor);
|
||||
|
||||
editor.sourceEditor.once("dirty-change", () => {
|
||||
is(editor.sourceEditor.isClean(), true, "Editor is clean.");
|
||||
ok(!editor.summary.classList.contains("unsaved"),
|
||||
"Star icon is not present in the corresponding summary.");
|
||||
finish();
|
||||
});
|
||||
editor.saveToFile(null, function (file) {
|
||||
ok(file, "file should get saved directly when using a file:// URI");
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -3322,10 +3322,12 @@ JSTerm.prototype = {
|
|||
|
||||
iframe.addEventListener("load", function onIframeLoad(aEvent) {
|
||||
iframe.removeEventListener("load", onIframeLoad, true);
|
||||
iframe.style.visibility = "visible";
|
||||
deferred.resolve(iframe.contentWindow);
|
||||
}, true);
|
||||
|
||||
iframe.flex = 1;
|
||||
iframe.style.visibility = "hidden";
|
||||
iframe.setAttribute("src", VARIABLES_VIEW_URL);
|
||||
aOptions.targetElement.appendChild(iframe);
|
||||
}
|
||||
|
|
|
@ -129,8 +129,10 @@
|
|||
<!ENTITY debuggerUI.seMenuCondBreak "Add conditional breakpoint">
|
||||
<!ENTITY debuggerUI.seMenuCondBreak.key "B">
|
||||
|
||||
<!-- LOCALIZATION NOTE (debuggerUI.instruments.*): This is the text that
|
||||
- appears in the debugger's instruments pane tabs. -->
|
||||
<!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that
|
||||
- appears in the debugger's side pane tabs. -->
|
||||
<!ENTITY debuggerUI.tabs.sources "Sources">
|
||||
<!ENTITY debuggerUI.tabs.callstack "Call Stack">
|
||||
<!ENTITY debuggerUI.tabs.variables "Variables">
|
||||
<!ENTITY debuggerUI.tabs.events "Events">
|
||||
|
||||
|
|
|
@ -69,10 +69,14 @@ noGlobalsText=No globals
|
|||
# when there are no scripts.
|
||||
noSourcesText=This page has no sources.
|
||||
|
||||
# LOCALIZATION NOTE (noEventsTExt): The text to display in the events tab
|
||||
# LOCALIZATION NOTE (noEventListenersText): The text to display in the events tab
|
||||
# when there are no events.
|
||||
noEventListenersText=No event listeners to display
|
||||
|
||||
# LOCALIZATION NOTE (noStackFramesText): The text to display in the call stack tab
|
||||
# when there are no stack frames.
|
||||
noStackFramesText=No stack frames to display
|
||||
|
||||
# LOCALIZATION NOTE (eventCheckboxTooltip): The tooltip text to display when
|
||||
# the user hovers over the checkbox used to toggle an event breakpoint.
|
||||
eventCheckboxTooltip=Toggle breaking on this event
|
||||
|
|
|
@ -247,7 +247,7 @@ Desktop browser's sync prefs.
|
|||
completeselectedindex="true"
|
||||
placeholder="&urlbar.emptytext;"
|
||||
tabscrolling="true"
|
||||
onclick="SelectionHelperUI.urlbarClick();"/>
|
||||
onclick="SelectionHelperUI.urlbarTextboxClick(this);"/>
|
||||
|
||||
<toolbarbutton id="go-button" class="urlbar-button"
|
||||
command="cmd_go"/>
|
||||
|
|
|
@ -34,10 +34,9 @@ var ChromeSelectionHandler = {
|
|||
this._domWinUtils = Util.getWindowUtils(window);
|
||||
this._contentWindow = window;
|
||||
this._targetElement = this._domWinUtils.elementFromPoint(aJson.xPos, aJson.yPos, true, false);
|
||||
|
||||
this._targetIsEditable = this._targetElement instanceof Components.interfaces.nsIDOMXULTextBoxElement;
|
||||
if (!this._targetIsEditable) {
|
||||
this._onFail("not an editable?");
|
||||
this._onFail("not an editable?", this._targetElement);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -344,9 +344,9 @@ var SelectionHelperUI = {
|
|||
observe: function (aSubject, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "attach_edit_session_to_content":
|
||||
let event = aSubject;
|
||||
this.attachEditSession(Browser.selectedTab.browser,
|
||||
event.clientX, event.clientY);
|
||||
// We receive this from text input bindings when this module
|
||||
// isn't accessible.
|
||||
this.chromeTextboxClick(aSubject);
|
||||
break;
|
||||
|
||||
case "apzc-transform-begin":
|
||||
|
@ -514,6 +514,40 @@ var SelectionHelperUI = {
|
|||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Event handler on the navbar text input. Called from navbar bindings
|
||||
* when focus is applied to the edit.
|
||||
*/
|
||||
urlbarTextboxClick: function(aEdit) {
|
||||
// workaround for bug 925457: taping browser chrome resets last tap
|
||||
// co-ordinates to 'undefined' so that we know not to shift the browser
|
||||
// when the keyboard is up in SelectionHandler's _calcNewContentPosition().
|
||||
Browser.selectedTab.browser.messageManager.sendAsyncMessage("Browser:ResetLastPos", {
|
||||
xPos: null,
|
||||
yPos: null
|
||||
});
|
||||
|
||||
if (InputSourceHelper.isPrecise || !aEdit.textLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Enable selection when there's text in the control
|
||||
let innerRect = aEdit.inputField.getBoundingClientRect();
|
||||
this.attachEditSession(ChromeSelectionHandler,
|
||||
innerRect.left,
|
||||
innerRect.top);
|
||||
},
|
||||
|
||||
/*
|
||||
* Click handler for chrome pages loaded into the browser (about:config).
|
||||
* Called from the text input bindings via the attach_edit_session_to_content
|
||||
* observer.
|
||||
*/
|
||||
chromeTextboxClick: function (aEvent) {
|
||||
this.attachEditSession(Browser.selectedTab.browser,
|
||||
aEvent.clientX, aEvent.clientY);
|
||||
},
|
||||
|
||||
/*
|
||||
* Handy debug routines that work independent of selection. They
|
||||
* make use of the selection overlay for drawing points.
|
||||
|
@ -835,20 +869,10 @@ var SelectionHelperUI = {
|
|||
* Event handlers for document events
|
||||
*/
|
||||
|
||||
urlbarClick: function() {
|
||||
// Workaround for bug 925457: taping browser chrome resets last tap
|
||||
// co-ordinates to 'undefined' so that we know not to shift the browser
|
||||
// when the keyboard is up (in SelectionHandler._calcNewContentPosition())
|
||||
Browser.selectedTab.browser.messageManager.sendAsyncMessage("Browser:ResetLastPos", {
|
||||
xPos: null,
|
||||
yPos: null
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Handles taps that move the current caret around in text edits,
|
||||
* clear active selection and focus when neccessary, or change
|
||||
* modes.
|
||||
* modes. Only active afer SelectionHandlerUI is initialized.
|
||||
*/
|
||||
_onClick: function(aEvent) {
|
||||
if (this.layerMode == kChromeLayer && this._targetIsEditable) {
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
min-width: 50px;
|
||||
}
|
||||
|
||||
#sources-pane > tabs {
|
||||
-moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
|
||||
}
|
||||
|
||||
#sources-and-editor-splitter {
|
||||
-moz-border-start-color: transparent;
|
||||
}
|
||||
|
@ -20,7 +24,7 @@
|
|||
-moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
|
||||
}
|
||||
|
||||
#sources-toolbar .devtools-toolbarbutton {
|
||||
#sources-toolbar > #sources-controls > .devtools-toolbarbutton {
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
|
@ -87,15 +91,23 @@
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
.list-widget-item:not(.selected):not(.empty):hover {
|
||||
.theme-light .list-widget-item:not(.selected):not(.empty):hover {
|
||||
background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
|
||||
}
|
||||
|
||||
.list-widget-item.selected.light {
|
||||
.theme-light .list-widget-item.selected.light {
|
||||
background: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.8)), Highlight;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.theme-dark .list-widget-item:not(.selected):not(.empty):hover {
|
||||
background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
}
|
||||
|
||||
.theme-dark .list-widget-item.selected.light {
|
||||
background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.025));
|
||||
}
|
||||
|
||||
.list-widget-item.selected {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
|
@ -106,21 +118,52 @@
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Stack frames view */
|
||||
/* Breadcrumbs stack frames view */
|
||||
|
||||
.breadcrumbs-widget-item {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.dbg-stackframe-details {
|
||||
-moz-padding-start: 4px;
|
||||
}
|
||||
|
||||
.dbg-stackframe-menuitem[checked] {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
outline: 1px solid #eee;
|
||||
font-weight: 600;
|
||||
/* Classic stack frames view */
|
||||
|
||||
.dbg-classic-stackframe {
|
||||
display: block;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.dbg-stackframe-menuitem-details {
|
||||
-moz-padding-start: 16px;
|
||||
.dbg-classic-stackframe-title {
|
||||
font-weight: 600;
|
||||
color: #046;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details:-moz-locale-dir(ltr) {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details:-moz-locale-dir(rtl) {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-url {
|
||||
max-width: 90%;
|
||||
text-align: end;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-sep {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-line {
|
||||
color: #58b;
|
||||
}
|
||||
|
||||
#callstack-list .side-menu-widget-item.selected label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Sources and breakpoints view */
|
||||
|
@ -180,8 +223,9 @@
|
|||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#instruments-pane > tabpanels > tabpanel {
|
||||
background: #fff;
|
||||
#instruments-pane .side-menu-widget-container,
|
||||
#instruments-pane .side-menu-widget-empty-notice-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Watch expressions view */
|
||||
|
@ -203,6 +247,7 @@
|
|||
|
||||
.dbg-expression-input {
|
||||
-moz-padding-start: 2px !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Event listeners view */
|
||||
|
@ -227,6 +272,10 @@
|
|||
color: #666;
|
||||
}
|
||||
|
||||
#event-listeners .side-menu-widget-item.selected {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* Searchbox and the search operations help panel */
|
||||
|
||||
#searchbox {
|
||||
|
@ -380,6 +429,11 @@
|
|||
|
||||
/* Toolbar controls */
|
||||
|
||||
.devtools-sidebar-tabs > tabs > tab {
|
||||
min-height: 25px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#resumption-panel-desc {
|
||||
width: 200px;
|
||||
}
|
||||
|
@ -468,12 +522,17 @@
|
|||
max-height: 80vh;
|
||||
}
|
||||
|
||||
#body[layout=vertical] #sources-pane > tabs {
|
||||
-moz-border-end: none;
|
||||
}
|
||||
|
||||
#body[layout=vertical] #instruments-pane {
|
||||
margin: 0 !important;
|
||||
/* To prevent all the margin hacks to hide the sidebar. */
|
||||
}
|
||||
|
||||
#body[layout=vertical] .side-menu-widget-container {
|
||||
#body[layout=vertical] .side-menu-widget-container,
|
||||
#body[layout=vertical] .side-menu-widget-empty-notice-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -312,6 +312,10 @@ box.requests-menu-status[code^="5"] {
|
|||
|
||||
/* SideMenuWidget */
|
||||
|
||||
.side-menu-widget-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.side-menu-widget-item[odd] {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
@ -363,10 +367,6 @@ box.requests-menu-status[code^="5"] {
|
|||
-moz-padding-start: 3px;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-string {
|
||||
color: #10c !important;
|
||||
}
|
||||
|
||||
/* Headers tabpanel */
|
||||
|
||||
#headers-summary-status,
|
||||
|
|
|
@ -281,15 +281,17 @@
|
|||
|
||||
/* SideMenuWidget container */
|
||||
|
||||
.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
|
||||
.side-menu-widget-container:-moz-locale-dir(ltr),
|
||||
.side-menu-widget-empty-notice-container:-moz-locale-dir(ltr) {
|
||||
box-shadow: inset -1px 0 0 #222426;
|
||||
}
|
||||
|
||||
.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
|
||||
.side-menu-widget-container:-moz-locale-dir(rtl),
|
||||
.side-menu-widget-empty-notice-container:-moz-locale-dir(rtl) {
|
||||
box-shadow: inset 1px 0 0 #222426;
|
||||
}
|
||||
|
||||
.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
|
||||
.side-menu-widget-group {
|
||||
/* To allow visibility of the dark margin shadow. */
|
||||
-moz-margin-end: 1px;
|
||||
}
|
||||
|
@ -328,7 +330,9 @@
|
|||
|
||||
.side-menu-widget-item[theme="light"] {
|
||||
border-top: 1px solid hsla(210,8%,75%,.25);
|
||||
border-bottom: 1px solid hsla(210,16%,76%,.1);
|
||||
margin-top: -1px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="dark"]:last-of-type {
|
||||
|
@ -339,15 +343,11 @@
|
|||
box-shadow: inset 0 -1px 0 hsla(210,8%,75%,.25);
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="dark"].selected {
|
||||
.side-menu-widget-item.selected {
|
||||
background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
|
||||
box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15);
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="light"].selected {
|
||||
/* Nothing here yet */
|
||||
}
|
||||
|
||||
.side-menu-widget-item.selected > .side-menu-widget-item-arrow {
|
||||
background-size: auto, 1px 100%;
|
||||
background-repeat: no-repeat;
|
||||
|
@ -432,10 +432,6 @@
|
|||
|
||||
/* VariablesView */
|
||||
|
||||
.variables-view-container {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.variables-view-empty-notice {
|
||||
color: GrayText;
|
||||
padding: 2px;
|
||||
|
@ -445,11 +441,6 @@
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.variables-view-scope:focus > .title {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
}
|
||||
|
||||
.variables-view-scope > .variables-view-element-details:not(:empty) {
|
||||
-moz-margin-start: 2px;
|
||||
-moz-margin-end: 1px;
|
||||
|
@ -458,13 +449,12 @@
|
|||
/* Generic traits applied to both variables and properties */
|
||||
|
||||
.variable-or-property {
|
||||
transition: background 1s ease-in-out;
|
||||
color: #000;
|
||||
transition: background 1s ease-in-out, color 1s ease-in-out;
|
||||
}
|
||||
|
||||
.variable-or-property[changed] {
|
||||
background: rgba(255,255,0,0.65);
|
||||
transition-duration: 0.4s;
|
||||
color: black;
|
||||
transition-duration: .4s;
|
||||
}
|
||||
|
||||
.variable-or-property > .title > .value {
|
||||
|
@ -473,12 +463,6 @@
|
|||
-moz-padding-end: 4px;
|
||||
}
|
||||
|
||||
.variable-or-property:focus > .title {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.variable-or-property[editable] > .title > .value {
|
||||
cursor: text;
|
||||
}
|
||||
|
@ -495,52 +479,22 @@
|
|||
}
|
||||
|
||||
.variables-view-variable:not(:last-child) {
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, .15);
|
||||
}
|
||||
|
||||
.variables-view-variable > .title > .name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.variables-view-variable:not(:focus) > .title > .name {
|
||||
color: #048;
|
||||
}
|
||||
|
||||
.variables-view-property:not(:focus) > .title > .name {
|
||||
color: #881090;
|
||||
}
|
||||
|
||||
/* Token value colors */
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-undefined {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-null {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-boolean {
|
||||
color: #10c;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-number {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-string {
|
||||
color: #282;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-other {
|
||||
color: #333;
|
||||
.variable-or-property:focus > .title > label {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Custom configurable/enumerable/writable or frozen/sealed/extensible
|
||||
* variables and properties */
|
||||
|
||||
.variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.variable-or-property[non-configurable] > .title > .name {
|
||||
|
@ -551,6 +505,10 @@
|
|||
border-bottom: 1px dashed #f99;
|
||||
}
|
||||
|
||||
.variable-or-property[safe-getter] > .title > .name {
|
||||
border-bottom: 1px dashed #8b0;
|
||||
}
|
||||
|
||||
.variable-or-property-non-writable-icon {
|
||||
background: url("chrome://browser/skin/identity-icons-https.png") no-repeat;
|
||||
width: 16px;
|
||||
|
@ -577,27 +535,6 @@
|
|||
color: #666;
|
||||
}
|
||||
|
||||
/* Special variables and properties */
|
||||
|
||||
.variable-or-property[safe-getter] > .title > .name {
|
||||
border-bottom: 1px dashed #8b0;
|
||||
}
|
||||
|
||||
.variable-or-property[exception]:not(:focus) > .title > .name {
|
||||
color: #a00;
|
||||
text-shadow: 0 0 8px #fcc;
|
||||
}
|
||||
|
||||
.variable-or-property[return]:not(:focus) > .title > .name {
|
||||
color: #0a0;
|
||||
text-shadow: 0 0 8px #cfc;
|
||||
}
|
||||
|
||||
.variable-or-property[scope]:not(:focus) > .title > .name {
|
||||
color: #00a;
|
||||
text-shadow: 0 0 8px #ccf;
|
||||
}
|
||||
|
||||
/* Aligned values */
|
||||
|
||||
.variables-view-container[aligned-values] .title > .separator {
|
||||
|
@ -634,7 +571,7 @@
|
|||
.variable-or-property[non-configurable] > tooltip > label[value=configurable],
|
||||
.variable-or-property[non-writable] > tooltip > label[value=writable],
|
||||
.variable-or-property[non-extensible] > tooltip > label[value=extensible] {
|
||||
color: #f44;
|
||||
color: #800;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
|
@ -680,14 +617,13 @@
|
|||
.element-name-input {
|
||||
-moz-margin-start: -2px !important;
|
||||
-moz-margin-end: 2px !important;
|
||||
color: #048;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.element-value-input,
|
||||
.element-name-input {
|
||||
border: 1px solid #999 !important;
|
||||
box-shadow: 1px 2px 4px #aaa;
|
||||
border: 1px solid rgba(128, 128, 128, .5) !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Variables and properties searching */
|
||||
|
|
|
@ -3594,11 +3594,16 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
|
|||
#navigator-toolbox[inFullscreen]:not(:-moz-lwtheme)::before {
|
||||
height: calc(@tabHeight@ + 11px) !important;
|
||||
}
|
||||
#main-window[inFullscreen][privatebrowsingmode=temporary],
|
||||
#main-window[inFullscreen]:-moz-lwtheme {
|
||||
/* This additional padding matches the change in height in the pseudo-element
|
||||
* above. The rules combined force the top 22px of the background image to
|
||||
* be hidden, so there image doesn't jump around with the loss of the titlebar */
|
||||
* above. */
|
||||
padding-top: 11px;
|
||||
}
|
||||
#main-window[inFullscreen]:not([privatebrowsingmode=temporary]):-moz-lwtheme {
|
||||
/* In combination with the previous rule, forces the top 22px of the
|
||||
* background image to be hidden, so the image doesn't jump around with
|
||||
* the loss of the titlebar. */
|
||||
background-position: right -11px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,10 @@
|
|||
min-width: 50px;
|
||||
}
|
||||
|
||||
#sources-pane > tabs {
|
||||
-moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
|
||||
}
|
||||
|
||||
#sources-and-editor-splitter {
|
||||
-moz-border-start-color: transparent;
|
||||
}
|
||||
|
@ -22,7 +26,7 @@
|
|||
-moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
|
||||
}
|
||||
|
||||
#sources-toolbar .devtools-toolbarbutton {
|
||||
#sources-toolbar > #sources-controls > .devtools-toolbarbutton {
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
|
@ -89,15 +93,23 @@
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
.list-widget-item:not(.selected):not(.empty):hover {
|
||||
.theme-light .list-widget-item:not(.selected):not(.empty):hover {
|
||||
background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
|
||||
}
|
||||
|
||||
.list-widget-item.selected.light {
|
||||
.theme-light .list-widget-item.selected.light {
|
||||
background: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.8)), Highlight;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.theme-dark .list-widget-item:not(.selected):not(.empty):hover {
|
||||
background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
}
|
||||
|
||||
.theme-dark .list-widget-item.selected.light {
|
||||
background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.025));
|
||||
}
|
||||
|
||||
.list-widget-item.selected {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
|
@ -108,21 +120,52 @@
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Stack frames view */
|
||||
/* Breadcrumbs stack frames view */
|
||||
|
||||
.breadcrumbs-widget-item {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.dbg-stackframe-details {
|
||||
-moz-padding-start: 4px;
|
||||
}
|
||||
|
||||
.dbg-stackframe-menuitem[checked] {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
outline: 1px solid #eee;
|
||||
font-weight: 600;
|
||||
/* Classic stack frames view */
|
||||
|
||||
.dbg-classic-stackframe {
|
||||
display: block;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.dbg-stackframe-menuitem-details {
|
||||
-moz-padding-start: 16px;
|
||||
.dbg-classic-stackframe-title {
|
||||
font-weight: 600;
|
||||
color: #046;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details:-moz-locale-dir(ltr) {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details:-moz-locale-dir(rtl) {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-url {
|
||||
max-width: 90%;
|
||||
text-align: end;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-sep {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-line {
|
||||
color: #58b;
|
||||
}
|
||||
|
||||
#callstack-list .side-menu-widget-item.selected label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Sources and breakpoints view */
|
||||
|
@ -182,8 +225,9 @@
|
|||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#instruments-pane > tabpanels > tabpanel {
|
||||
background: #fff;
|
||||
#instruments-pane .side-menu-widget-container,
|
||||
#instruments-pane .side-menu-widget-empty-notice-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Watch expressions view */
|
||||
|
@ -205,6 +249,7 @@
|
|||
|
||||
.dbg-expression-input {
|
||||
-moz-padding-start: 2px !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Event listeners view */
|
||||
|
@ -229,6 +274,10 @@
|
|||
color: #666;
|
||||
}
|
||||
|
||||
#event-listeners .side-menu-widget-item.selected {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* Searchbox and the search operations help panel */
|
||||
|
||||
#searchbox {
|
||||
|
@ -382,6 +431,11 @@
|
|||
|
||||
/* Toolbar controls */
|
||||
|
||||
.devtools-sidebar-tabs > tabs > tab {
|
||||
min-height: 1em !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#resumption-panel-desc {
|
||||
width: 200px;
|
||||
}
|
||||
|
@ -470,12 +524,17 @@
|
|||
max-height: 80vh;
|
||||
}
|
||||
|
||||
#body[layout=vertical] #sources-pane > tabs {
|
||||
-moz-border-end: none;
|
||||
}
|
||||
|
||||
#body[layout=vertical] #instruments-pane {
|
||||
margin: 0 !important;
|
||||
/* To prevent all the margin hacks to hide the sidebar. */
|
||||
}
|
||||
|
||||
#body[layout=vertical] .side-menu-widget-container {
|
||||
#body[layout=vertical] .side-menu-widget-container,
|
||||
#body[layout=vertical] .side-menu-widget-empty-notice-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -312,6 +312,10 @@ box.requests-menu-status[code^="5"] {
|
|||
|
||||
/* SideMenuWidget */
|
||||
|
||||
.side-menu-widget-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.side-menu-widget-item[odd] {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
@ -363,10 +367,6 @@ box.requests-menu-status[code^="5"] {
|
|||
-moz-padding-start: 3px;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-string {
|
||||
color: #10c !important;
|
||||
}
|
||||
|
||||
/* Headers tabpanel */
|
||||
|
||||
#headers-summary-status,
|
||||
|
|
|
@ -281,15 +281,17 @@
|
|||
|
||||
/* SideMenuWidget container */
|
||||
|
||||
.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
|
||||
.side-menu-widget-container:-moz-locale-dir(ltr),
|
||||
.side-menu-widget-empty-notice-container:-moz-locale-dir(ltr) {
|
||||
box-shadow: inset -1px 0 0 #222426;
|
||||
}
|
||||
|
||||
.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
|
||||
.side-menu-widget-container:-moz-locale-dir(rtl),
|
||||
.side-menu-widget-empty-notice-container:-moz-locale-dir(rtl) {
|
||||
box-shadow: inset 1px 0 0 #222426;
|
||||
}
|
||||
|
||||
.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
|
||||
.side-menu-widget-group {
|
||||
/* To allow visibility of the dark margin shadow. */
|
||||
-moz-margin-end: 1px;
|
||||
}
|
||||
|
@ -328,7 +330,9 @@
|
|||
|
||||
.side-menu-widget-item[theme="light"] {
|
||||
border-top: 1px solid hsla(210,8%,75%,.25);
|
||||
border-bottom: 1px solid hsla(210,16%,76%,.1);
|
||||
margin-top: -1px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="dark"]:last-of-type {
|
||||
|
@ -339,15 +343,11 @@
|
|||
box-shadow: inset 0 -1px 0 hsla(210,8%,75%,.25);
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="dark"].selected {
|
||||
.side-menu-widget-item.selected {
|
||||
background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
|
||||
box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15);
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="light"].selected {
|
||||
/* Nothing here yet */
|
||||
}
|
||||
|
||||
.side-menu-widget-item.selected > .side-menu-widget-item-arrow {
|
||||
background-size: auto, 1px 100%;
|
||||
background-repeat: no-repeat;
|
||||
|
@ -426,10 +426,6 @@
|
|||
|
||||
/* VariablesView */
|
||||
|
||||
.variables-view-container {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.variables-view-empty-notice {
|
||||
color: GrayText;
|
||||
padding: 2px;
|
||||
|
@ -439,11 +435,6 @@
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.variables-view-scope:focus > .title {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
}
|
||||
|
||||
.variables-view-scope > .variables-view-element-details:not(:empty) {
|
||||
-moz-margin-start: 2px;
|
||||
-moz-margin-end: 1px;
|
||||
|
@ -452,13 +443,12 @@
|
|||
/* Generic traits applied to both variables and properties */
|
||||
|
||||
.variable-or-property {
|
||||
transition: background 1s ease-in-out;
|
||||
color: #000;
|
||||
transition: background 1s ease-in-out, color 1s ease-in-out;
|
||||
}
|
||||
|
||||
.variable-or-property[changed] {
|
||||
background: rgba(255,255,0,0.65);
|
||||
transition-duration: 0.4s;
|
||||
color: black;
|
||||
transition-duration: .4s;
|
||||
}
|
||||
|
||||
.variable-or-property > .title > .value {
|
||||
|
@ -467,12 +457,6 @@
|
|||
-moz-padding-end: 4px;
|
||||
}
|
||||
|
||||
.variable-or-property:focus > .title {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.variable-or-property[editable] > .title > .value {
|
||||
cursor: text;
|
||||
}
|
||||
|
@ -489,52 +473,22 @@
|
|||
}
|
||||
|
||||
.variables-view-variable:not(:last-child) {
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, .15);
|
||||
}
|
||||
|
||||
.variables-view-variable > .title > .name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.variables-view-variable:not(:focus) > .title > .name {
|
||||
color: #048;
|
||||
}
|
||||
|
||||
.variables-view-property:not(:focus) > .title > .name {
|
||||
color: #881090;
|
||||
}
|
||||
|
||||
/* Token value colors */
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-undefined {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-null {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-boolean {
|
||||
color: #10c;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-number {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-string {
|
||||
color: #282;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-other {
|
||||
color: #333;
|
||||
.variable-or-property:focus > .title > label {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Custom configurable/enumerable/writable or frozen/sealed/extensible
|
||||
* variables and properties */
|
||||
|
||||
.variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.variable-or-property[non-configurable] > .title > .name {
|
||||
|
@ -545,6 +499,10 @@
|
|||
border-bottom: 1px dashed #f99;
|
||||
}
|
||||
|
||||
.variable-or-property[safe-getter] > .title > .name {
|
||||
border-bottom: 1px dashed #8b0;
|
||||
}
|
||||
|
||||
.variable-or-property-non-writable-icon {
|
||||
background: url("chrome://browser/skin/identity-icons-https.png") no-repeat;
|
||||
width: 16px;
|
||||
|
@ -571,27 +529,6 @@
|
|||
color: #666;
|
||||
}
|
||||
|
||||
/* Special variables and properties */
|
||||
|
||||
.variable-or-property[safe-getter] > .title > .name {
|
||||
border-bottom: 1px dashed #8b0;
|
||||
}
|
||||
|
||||
.variable-or-property[exception]:not(:focus) > .title > .name {
|
||||
color: #a00;
|
||||
text-shadow: 0 0 8px #fcc;
|
||||
}
|
||||
|
||||
.variable-or-property[return]:not(:focus) > .title > .name {
|
||||
color: #0a0;
|
||||
text-shadow: 0 0 8px #cfc;
|
||||
}
|
||||
|
||||
.variable-or-property[scope]:not(:focus) > .title > .name {
|
||||
color: #00a;
|
||||
text-shadow: 0 0 8px #ccf;
|
||||
}
|
||||
|
||||
/* Aligned values */
|
||||
|
||||
.variables-view-container[aligned-values] .title > .separator {
|
||||
|
@ -674,14 +611,13 @@
|
|||
.element-name-input {
|
||||
-moz-margin-start: -2px !important;
|
||||
-moz-margin-end: 2px !important;
|
||||
color: #048;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.element-value-input,
|
||||
.element-name-input {
|
||||
border: 1px solid #999 !important;
|
||||
box-shadow: 1px 2px 4px #aaa;
|
||||
border: 1px solid rgba(128, 128, 128, .5) !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Variables and properties searching */
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
background-color: rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.theme-bg-contrast { /* contrast bg color to attract attention on a container */
|
||||
.theme-bg-contrast,
|
||||
.variable-or-property[changed] { /* contrast bg color to attract attention on a container */
|
||||
background: #a18650;
|
||||
}
|
||||
|
||||
|
@ -73,7 +74,9 @@
|
|||
.theme-comment,
|
||||
.cm-s-mozilla .cm-meta,
|
||||
.cm-s-mozilla .cm-hr,
|
||||
.cm-s-mozilla .cm-comment { /* grey */
|
||||
.cm-s-mozilla .cm-comment,
|
||||
.variable-or-property .token-undefined,
|
||||
.variable-or-property .token-null { /* grey */
|
||||
color: #5c6773;
|
||||
}
|
||||
|
||||
|
@ -88,7 +91,9 @@
|
|||
}
|
||||
|
||||
.theme-fg-color1,
|
||||
.cm-s-mozilla .cm-number { /* green */
|
||||
.cm-s-mozilla .cm-number,
|
||||
.variable-or-property .token-number,
|
||||
.variable-or-property[return] > .title > .name { /* green */
|
||||
color: #5c9966;
|
||||
}
|
||||
|
||||
|
@ -97,14 +102,18 @@
|
|||
.cm-s-mozilla .cm-variable,
|
||||
.cm-s-mozilla .cm-def,
|
||||
.cm-s-mozilla .cm-property,
|
||||
.cm-s-mozilla .cm-qualifier { /* blue */
|
||||
.cm-s-mozilla .cm-qualifier,
|
||||
.variables-view-variable > .title > .name,
|
||||
.variable-or-property[scope] > .title > .name { /* blue */
|
||||
color: #3689b2;
|
||||
}
|
||||
|
||||
.theme-fg-color3,
|
||||
.cm-s-mozilla .cm-builtin,
|
||||
.cm-s-mozilla .cm-tag,
|
||||
.cm-s-mozilla .cm-header { /* pink/lavender */
|
||||
.cm-s-mozilla .cm-header,
|
||||
.variables-view-property > .title > .name,
|
||||
.variable-or-property[safe-getter] > .title > .name { /* pink/lavender */
|
||||
color: #a673bf;
|
||||
}
|
||||
|
||||
|
@ -120,14 +129,17 @@
|
|||
|
||||
.theme-fg-color6,
|
||||
.cm-s-mozilla .cm-string,
|
||||
.cm-s-mozilla .cm-string-2 { /* Orange */
|
||||
.cm-s-mozilla .cm-string-2,
|
||||
.variable-or-property .token-string { /* Orange */
|
||||
color: #b26b47;
|
||||
}
|
||||
|
||||
.theme-fg-color7,
|
||||
.cm-s-mozilla .cm-atom,
|
||||
.cm-s-mozilla .cm-quote,
|
||||
.cm-s-mozilla .cm-error { /* Red */
|
||||
.cm-s-mozilla .cm-error,
|
||||
.variable-or-property .token-boolean,
|
||||
.variable-or-property[exception] > .title > .name { /* Red */
|
||||
color: #bf5656;
|
||||
}
|
||||
|
||||
|
@ -148,6 +160,12 @@
|
|||
box-shadow: 0 0 0 1px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.variables-view-scope:focus > .title,
|
||||
.variable-or-property:focus > .title {
|
||||
background: #3689b2; /* fg-color2 */
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* CodeMirror specific styles.
|
||||
* Best effort to match the existing theme, some of the colors
|
||||
* are duplicated here to prevent weirdness in the main theme. */
|
||||
|
|
|
@ -51,7 +51,8 @@
|
|||
background: #EFEFEF;
|
||||
}
|
||||
|
||||
.theme-bg-contrast { /* contrast bg color to attract attention on a container */
|
||||
.theme-bg-contrast,
|
||||
.variable-or-property[changed] { /* contrast bg color to attract attention on a container */
|
||||
background: #a18650;
|
||||
}
|
||||
|
||||
|
@ -72,7 +73,9 @@
|
|||
.theme-comment,
|
||||
.cm-s-mozilla .cm-meta,
|
||||
.cm-s-mozilla .cm-hr,
|
||||
.cm-s-mozilla .cm-comment { /* grey */
|
||||
.cm-s-mozilla .cm-comment,
|
||||
.variable-or-property .token-undefined,
|
||||
.variable-or-property .token-null { /* grey */
|
||||
color: hsl(90,2%,46%);
|
||||
}
|
||||
|
||||
|
@ -87,7 +90,9 @@
|
|||
}
|
||||
|
||||
.theme-fg-color1,
|
||||
.cm-s-mozilla .cm-number { /* green */
|
||||
.cm-s-mozilla .cm-number,
|
||||
.variable-or-property .token-number,
|
||||
.variable-or-property[return] > .title > .name { /* green */
|
||||
color: hsl(72,100%,27%);
|
||||
}
|
||||
|
||||
|
@ -96,14 +101,18 @@
|
|||
.cm-s-mozilla .cm-builtin,
|
||||
.cm-s-mozilla .cm-def,
|
||||
.cm-s-mozilla .cm-property,
|
||||
.cm-s-mozilla .cm-qualifier { /* blue */
|
||||
.cm-s-mozilla .cm-qualifier,
|
||||
.variables-view-variable > .title > .name,
|
||||
.variable-or-property[scope] > .title > .name { /* blue */
|
||||
color: hsl(208,56%,40%);
|
||||
}
|
||||
|
||||
.theme-fg-color3,
|
||||
.cm-s-mozilla .cm-variable,
|
||||
.cm-s-mozilla .cm-tag,
|
||||
.cm-s-mozilla .cm-header { /* dark blue */
|
||||
.cm-s-mozilla .cm-header,
|
||||
.variables-view-property > .title > .name,
|
||||
.variable-or-property[safe-getter] > .title > .name { /* dark blue */
|
||||
color: hsl(208,81%,21%)
|
||||
}
|
||||
|
||||
|
@ -119,14 +128,17 @@
|
|||
|
||||
.theme-fg-color6,
|
||||
.cm-s-mozilla .cm-string,
|
||||
.cm-s-mozilla .cm-string-2 { /* Orange */
|
||||
.cm-s-mozilla .cm-string-2,
|
||||
.variable-or-property .token-string { /* Orange */
|
||||
color: hsl(24,85%,39%);
|
||||
}
|
||||
|
||||
.theme-fg-color7,
|
||||
.cm-s-mozilla .cm-atom,
|
||||
.cm-s-mozilla .cm-quote,
|
||||
.cm-s-mozilla .cm-error { /* Red */
|
||||
.cm-s-mozilla .cm-error,
|
||||
.variable-or-property .token-boolean,
|
||||
.variable-or-property[exception] > .title > .name { /* Red */
|
||||
color: #bf5656;
|
||||
}
|
||||
|
||||
|
@ -147,6 +159,12 @@
|
|||
box-shadow: 0 0 0 1px #EFEFEF;
|
||||
}
|
||||
|
||||
.variables-view-scope:focus > .title,
|
||||
.variable-or-property:focus > .title {
|
||||
background: hsl(208,56%,40%); /* fg-color2 */
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* CodeMirror specific styles.
|
||||
* Best effort to match the existing theme, some of the colors
|
||||
* are duplicated here to prevent weirdness in the main theme. */
|
||||
|
|
|
@ -260,11 +260,15 @@
|
|||
}
|
||||
|
||||
.devtools-sidebar-tabs > tabs > tab {
|
||||
background-size: calc(100% - 2px) 100%, 1px 100%;
|
||||
background-size: calc(100% - 1px) 100%, 1px 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-position: 1px, 0;
|
||||
}
|
||||
|
||||
.devtools-sidebar-tabs > tabs > tab:not(:last-of-type) {
|
||||
background-size: calc(100% - 2px) 100%, 1px 100%;
|
||||
}
|
||||
|
||||
.devtools-sidebar-tabs:-moz-locale-dir(rtl) > tabs > tab {
|
||||
background-position: calc(100% - 1px), 100%;
|
||||
}
|
||||
|
|
|
@ -291,11 +291,11 @@ a {
|
|||
.inlined-variables-view iframe {
|
||||
display: block;
|
||||
flex: 1;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
-moz-margin-end: 15px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 0 12px #dfdfdf;
|
||||
border: 1px solid rgba(128, 128, 128, .5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
#webconsole-sidebar > tabs {
|
||||
|
@ -354,6 +354,10 @@ a {
|
|||
background: #131c26; /* mainBackgroundColor */
|
||||
}
|
||||
|
||||
.theme-dark .inlined-variables-view iframe {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.theme-light .jsterm-input-container {
|
||||
background-color: #fff; /* mainBackgroundColor */
|
||||
border-color: ThreeDShadow;
|
||||
|
@ -370,3 +374,8 @@ a {
|
|||
.theme-light .navigation-marker .url {
|
||||
background: #fff; /* mainBackgroundColor */
|
||||
}
|
||||
|
||||
.theme-light .inlined-variables-view iframe {
|
||||
border-color: #ccc;
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,10 @@
|
|||
min-width: 50px;
|
||||
}
|
||||
|
||||
#sources-pane > tabs {
|
||||
-moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
|
||||
}
|
||||
|
||||
#sources-and-editor-splitter {
|
||||
-moz-border-start-color: transparent;
|
||||
}
|
||||
|
@ -20,7 +24,7 @@
|
|||
-moz-border-end: 1px solid #222426; /* Match the sources list's dark margin. */
|
||||
}
|
||||
|
||||
#sources-toolbar .devtools-toolbarbutton {
|
||||
#sources-toolbar > #sources-controls > .devtools-toolbarbutton {
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
|
@ -87,15 +91,23 @@
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
.list-widget-item:not(.selected):not(.empty):hover {
|
||||
.theme-light .list-widget-item:not(.selected):not(.empty):hover {
|
||||
background: linear-gradient(rgba(255,255,255,0.9), rgba(255,255,255,0.85)), Highlight;
|
||||
}
|
||||
|
||||
.list-widget-item.selected.light {
|
||||
.theme-light .list-widget-item.selected.light {
|
||||
background: linear-gradient(rgba(255,255,255,0.85), rgba(255,255,255,0.8)), Highlight;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.theme-dark .list-widget-item:not(.selected):not(.empty):hover {
|
||||
background: linear-gradient(rgba(255,255,255,0.1), rgba(255,255,255,0.05));
|
||||
}
|
||||
|
||||
.theme-dark .list-widget-item.selected.light {
|
||||
background: linear-gradient(rgba(255,255,255,0.05), rgba(255,255,255,0.025));
|
||||
}
|
||||
|
||||
.list-widget-item.selected {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
|
@ -106,21 +118,52 @@
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Stack frames view */
|
||||
/* Breadcrumbs stack frames view */
|
||||
|
||||
.breadcrumbs-widget-item {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.dbg-stackframe-details {
|
||||
-moz-padding-start: 4px;
|
||||
}
|
||||
|
||||
.dbg-stackframe-menuitem[checked] {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 3px;
|
||||
outline: 1px solid #eee;
|
||||
font-weight: 600;
|
||||
/* Classic stack frames view */
|
||||
|
||||
.dbg-classic-stackframe {
|
||||
display: block;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.dbg-stackframe-menuitem-details {
|
||||
-moz-padding-start: 16px;
|
||||
.dbg-classic-stackframe-title {
|
||||
font-weight: 600;
|
||||
color: #046;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details:-moz-locale-dir(ltr) {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details:-moz-locale-dir(rtl) {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-url {
|
||||
max-width: 90%;
|
||||
text-align: end;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-sep {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.dbg-classic-stackframe-details-line {
|
||||
color: #58b;
|
||||
}
|
||||
|
||||
#callstack-list .side-menu-widget-item.selected label {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Sources and breakpoints view */
|
||||
|
@ -180,8 +223,9 @@
|
|||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#instruments-pane > tabpanels > tabpanel {
|
||||
background: #fff;
|
||||
#instruments-pane .side-menu-widget-container,
|
||||
#instruments-pane .side-menu-widget-empty-notice-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
/* Watch expressions view */
|
||||
|
@ -203,6 +247,7 @@
|
|||
|
||||
.dbg-expression-input {
|
||||
-moz-padding-start: 2px !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Event listeners view */
|
||||
|
@ -227,6 +272,10 @@
|
|||
color: #666;
|
||||
}
|
||||
|
||||
#event-listeners .side-menu-widget-item.selected {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
/* Searchbox and the search operations help panel */
|
||||
|
||||
#searchbox {
|
||||
|
@ -380,6 +429,11 @@
|
|||
|
||||
/* Toolbar controls */
|
||||
|
||||
.devtools-sidebar-tabs > tabs > tab {
|
||||
min-height: 25px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
#resumption-panel-desc {
|
||||
width: 200px;
|
||||
}
|
||||
|
@ -473,12 +527,17 @@
|
|||
max-height: 80vh;
|
||||
}
|
||||
|
||||
#body[layout=vertical] #sources-pane > tabs {
|
||||
-moz-border-end: none;
|
||||
}
|
||||
|
||||
#body[layout=vertical] #instruments-pane {
|
||||
margin: 0 !important;
|
||||
/* To prevent all the margin hacks to hide the sidebar. */
|
||||
}
|
||||
|
||||
#body[layout=vertical] .side-menu-widget-container {
|
||||
#body[layout=vertical] .side-menu-widget-container,
|
||||
#body[layout=vertical] .side-menu-widget-empty-notice-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -312,6 +312,10 @@ box.requests-menu-status[code^="5"] {
|
|||
|
||||
/* SideMenuWidget */
|
||||
|
||||
.side-menu-widget-container {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.side-menu-widget-item[odd] {
|
||||
background: rgba(255,255,255,0.05);
|
||||
}
|
||||
|
@ -363,10 +367,6 @@ box.requests-menu-status[code^="5"] {
|
|||
-moz-padding-start: 3px;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-string {
|
||||
color: #10c !important;
|
||||
}
|
||||
|
||||
/* Headers tabpanel */
|
||||
|
||||
#headers-summary-status,
|
||||
|
|
|
@ -285,15 +285,17 @@
|
|||
|
||||
/* SideMenuWidget container */
|
||||
|
||||
.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(ltr) {
|
||||
.side-menu-widget-container:-moz-locale-dir(ltr),
|
||||
.side-menu-widget-empty-notice-container:-moz-locale-dir(ltr) {
|
||||
box-shadow: inset -1px 0 0 #222426;
|
||||
}
|
||||
|
||||
.side-menu-widget-container[with-arrows=true]:-moz-locale-dir(rtl) {
|
||||
.side-menu-widget-container:-moz-locale-dir(rtl),
|
||||
.side-menu-widget-empty-notice-container:-moz-locale-dir(rtl) {
|
||||
box-shadow: inset 1px 0 0 #222426;
|
||||
}
|
||||
|
||||
.side-menu-widget-container[with-arrows=true] .side-menu-widget-group {
|
||||
.side-menu-widget-group {
|
||||
/* To allow visibility of the dark margin shadow. */
|
||||
-moz-margin-end: 1px;
|
||||
}
|
||||
|
@ -332,7 +334,9 @@
|
|||
|
||||
.side-menu-widget-item[theme="light"] {
|
||||
border-top: 1px solid hsla(210,8%,75%,.25);
|
||||
border-bottom: 1px solid hsla(210,16%,76%,.1);
|
||||
margin-top: -1px;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="dark"]:last-of-type {
|
||||
|
@ -343,15 +347,11 @@
|
|||
box-shadow: inset 0 -1px 0 hsla(210,8%,75%,.25);
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="dark"].selected {
|
||||
.side-menu-widget-item.selected {
|
||||
background: linear-gradient(hsl(206,61%,40%), hsl(206,61%,31%)) repeat-x top left !important;
|
||||
box-shadow: inset 0 1px 0 hsla(210,40%,83%,.15);
|
||||
}
|
||||
|
||||
.side-menu-widget-item[theme="light"].selected {
|
||||
/* Nothing here yet */
|
||||
}
|
||||
|
||||
.side-menu-widget-item.selected > .side-menu-widget-item-arrow {
|
||||
background-size: auto, 1px 100%;
|
||||
background-repeat: no-repeat;
|
||||
|
@ -429,10 +429,6 @@
|
|||
|
||||
/* VariablesView */
|
||||
|
||||
.variables-view-container {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.variables-view-empty-notice {
|
||||
color: GrayText;
|
||||
padding: 2px;
|
||||
|
@ -442,11 +438,6 @@
|
|||
color: #fff;
|
||||
}
|
||||
|
||||
.variables-view-scope:focus > .title {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
}
|
||||
|
||||
.variables-view-scope > .variables-view-element-details:not(:empty) {
|
||||
-moz-margin-start: 2px;
|
||||
-moz-margin-end: 1px;
|
||||
|
@ -455,13 +446,12 @@
|
|||
/* Generic traits applied to both variables and properties */
|
||||
|
||||
.variable-or-property {
|
||||
transition: background 1s ease-in-out;
|
||||
color: #000;
|
||||
transition: background 1s ease-in-out, color 1s ease-in-out;
|
||||
}
|
||||
|
||||
.variable-or-property[changed] {
|
||||
background: rgba(255,255,0,0.65);
|
||||
transition-duration: 0.4s;
|
||||
color: black;
|
||||
transition-duration: .4s;
|
||||
}
|
||||
|
||||
.variable-or-property > .title > .value {
|
||||
|
@ -470,12 +460,6 @@
|
|||
-moz-padding-end: 4px;
|
||||
}
|
||||
|
||||
.variable-or-property:focus > .title {
|
||||
background: Highlight;
|
||||
color: HighlightText;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.variable-or-property[editable] > .title > .value {
|
||||
cursor: text;
|
||||
}
|
||||
|
@ -492,52 +476,22 @@
|
|||
}
|
||||
|
||||
.variables-view-variable:not(:last-child) {
|
||||
border-bottom: 1px solid #eee;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, .15);
|
||||
}
|
||||
|
||||
.variables-view-variable > .title > .name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.variables-view-variable:not(:focus) > .title > .name {
|
||||
color: #048;
|
||||
}
|
||||
|
||||
.variables-view-property:not(:focus) > .title > .name {
|
||||
color: #881090;
|
||||
}
|
||||
|
||||
/* Token value colors */
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-undefined {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-null {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-boolean {
|
||||
color: #10c;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-number {
|
||||
color: #c00;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-string {
|
||||
color: #282;
|
||||
}
|
||||
|
||||
.variable-or-property:not(:focus) > .title > .token-other {
|
||||
color: #333;
|
||||
.variable-or-property:focus > .title > label {
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* Custom configurable/enumerable/writable or frozen/sealed/extensible
|
||||
* variables and properties */
|
||||
|
||||
.variable-or-property[non-enumerable]:not([self]):not([exception]):not([return]):not([scope]) > .title > .name {
|
||||
opacity: 0.5;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.variable-or-property[non-configurable] > .title > .name {
|
||||
|
@ -548,6 +502,10 @@
|
|||
border-bottom: 1px dashed #f99;
|
||||
}
|
||||
|
||||
.variable-or-property[safe-getter] > .title > .name {
|
||||
border-bottom: 1px dashed #8b0;
|
||||
}
|
||||
|
||||
.variable-or-property-non-writable-icon {
|
||||
background: url("chrome://browser/skin/identity-icons-https.png") no-repeat;
|
||||
width: 16px;
|
||||
|
@ -574,27 +532,6 @@
|
|||
color: #666;
|
||||
}
|
||||
|
||||
/* Special variables and properties */
|
||||
|
||||
.variable-or-property[safe-getter] > .title > .name {
|
||||
border-bottom: 1px dashed #8b0;
|
||||
}
|
||||
|
||||
.variable-or-property[exception]:not(:focus) > .title > .name {
|
||||
color: #a00;
|
||||
text-shadow: 0 0 8px #fcc;
|
||||
}
|
||||
|
||||
.variable-or-property[return]:not(:focus) > .title > .name {
|
||||
color: #0a0;
|
||||
text-shadow: 0 0 8px #cfc;
|
||||
}
|
||||
|
||||
.variable-or-property[scope]:not(:focus) > .title > .name {
|
||||
color: #00a;
|
||||
text-shadow: 0 0 8px #ccf;
|
||||
}
|
||||
|
||||
/* Aligned values */
|
||||
|
||||
.variables-view-container[aligned-values] .title > .separator {
|
||||
|
@ -677,14 +614,13 @@
|
|||
.element-name-input {
|
||||
-moz-margin-start: -2px !important;
|
||||
-moz-margin-end: 2px !important;
|
||||
color: #048;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.element-value-input,
|
||||
.element-name-input {
|
||||
border: 1px solid #999 !important;
|
||||
box-shadow: 1px 2px 4px #aaa;
|
||||
border: 1px solid rgba(128, 128, 128, .5) !important;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Variables and properties searching */
|
||||
|
|
|
@ -4,14 +4,12 @@
|
|||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.ArrayList;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.gfx.GeckoLayerClient;
|
||||
import org.mozilla.gecko.gfx.GeckoLayerClient.DrawListener;
|
||||
import org.mozilla.gecko.mozglue.GeckoLoader;
|
||||
import org.mozilla.gecko.sqlite.SQLiteBridge;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Instrumentation;
|
||||
|
@ -24,112 +22,65 @@ import android.view.KeyEvent;
|
|||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.ArrayList;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import com.jayway.android.robotium.solo.Solo;
|
||||
|
||||
import static org.mozilla.gecko.FennecNativeDriver.LogLevel;
|
||||
|
||||
public class FennecNativeActions implements Actions {
|
||||
private static final String LOGTAG = "FennecNativeActions";
|
||||
|
||||
private Solo mSolo;
|
||||
private Instrumentation mInstr;
|
||||
private Activity mGeckoApp;
|
||||
private Assert mAsserter;
|
||||
|
||||
// Objects for reflexive access of fennec classes.
|
||||
private ClassLoader mClassLoader;
|
||||
private Class mApiClass;
|
||||
private Class mEventListenerClass;
|
||||
private Class mDrawListenerClass;
|
||||
private Method mRegisterEventListener;
|
||||
private Method mUnregisterEventListener;
|
||||
private Method mBroadcastEvent;
|
||||
private Method mPreferencesGetEvent;
|
||||
private Method mPreferencesObserveEvent;
|
||||
private Method mPreferencesRemoveObserversEvent;
|
||||
private Method mSetDrawListener;
|
||||
private Method mQuerySql;
|
||||
private Object mRobocopApi;
|
||||
|
||||
private static final String LOGTAG = "FennecNativeActions";
|
||||
|
||||
public FennecNativeActions(Activity activity, Solo robocop, Instrumentation instrumentation, Assert asserter) {
|
||||
mSolo = robocop;
|
||||
mInstr = instrumentation;
|
||||
mGeckoApp = activity;
|
||||
mAsserter = asserter;
|
||||
// Set up reflexive access of java classes and methods.
|
||||
try {
|
||||
mClassLoader = activity.getClassLoader();
|
||||
|
||||
mApiClass = mClassLoader.loadClass("org.mozilla.gecko.RobocopAPI");
|
||||
mEventListenerClass = mClassLoader.loadClass("org.mozilla.gecko.util.GeckoEventListener");
|
||||
mDrawListenerClass = mClassLoader.loadClass("org.mozilla.gecko.gfx.GeckoLayerClient$DrawListener");
|
||||
|
||||
mRegisterEventListener = mApiClass.getMethod("registerEventListener", String.class, mEventListenerClass);
|
||||
mUnregisterEventListener = mApiClass.getMethod("unregisterEventListener", String.class, mEventListenerClass);
|
||||
mBroadcastEvent = mApiClass.getMethod("broadcastEvent", String.class, String.class);
|
||||
mPreferencesGetEvent = mApiClass.getMethod("preferencesGetEvent", Integer.TYPE, String[].class);
|
||||
mPreferencesObserveEvent = mApiClass.getMethod("preferencesObserveEvent", Integer.TYPE, String[].class);
|
||||
mPreferencesRemoveObserversEvent = mApiClass.getMethod("preferencesRemoveObserversEvent", Integer.TYPE);
|
||||
mSetDrawListener = mApiClass.getMethod("setDrawListener", mDrawListenerClass);
|
||||
mQuerySql = mApiClass.getMethod("querySql", String.class, String.class);
|
||||
|
||||
mRobocopApi = mApiClass.getConstructor(Activity.class).newInstance(activity);
|
||||
} catch (Exception e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
class wakeInvocationHandler implements InvocationHandler {
|
||||
private final GeckoEventExpecter mEventExpecter;
|
||||
|
||||
public wakeInvocationHandler(GeckoEventExpecter expecter) {
|
||||
mEventExpecter = expecter;
|
||||
}
|
||||
|
||||
public Object invoke(Object proxy, Method method, Object[] args) {
|
||||
String methodName = method.getName();
|
||||
//Depending on the method, return a completely different type.
|
||||
if(methodName.equals("toString")) {
|
||||
return this.toString();
|
||||
}
|
||||
if(methodName.equals("equals")) {
|
||||
return
|
||||
args[0] == null ? false :
|
||||
this.toString().equals(args[0].toString());
|
||||
}
|
||||
if(methodName.equals("clone")) {
|
||||
return this;
|
||||
}
|
||||
if(methodName.equals("hashCode")) {
|
||||
return 314;
|
||||
}
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
|
||||
"Waking up on "+methodName);
|
||||
mEventExpecter.notifyOfEvent(args);
|
||||
return null;
|
||||
}
|
||||
GeckoLoader.loadSQLiteLibs(activity, activity.getApplication().getPackageResourcePath());
|
||||
}
|
||||
|
||||
class GeckoEventExpecter implements RepeatedEventExpecter {
|
||||
private final String mGeckoEvent;
|
||||
private Object[] mRegistrationParams;
|
||||
private boolean mEventEverReceived;
|
||||
private String mEventData;
|
||||
private BlockingQueue<String> mEventDataQueue;
|
||||
private static final int MAX_WAIT_MS = 90000;
|
||||
|
||||
GeckoEventExpecter(String geckoEvent, Object[] registrationParams) {
|
||||
private volatile boolean mIsRegistered;
|
||||
|
||||
private final String mGeckoEvent;
|
||||
private final GeckoEventListener mListener;
|
||||
|
||||
private volatile boolean mEventEverReceived;
|
||||
private String mEventData;
|
||||
private BlockingQueue<String> mEventDataQueue;
|
||||
|
||||
GeckoEventExpecter(final String geckoEvent) {
|
||||
if (TextUtils.isEmpty(geckoEvent)) {
|
||||
throw new IllegalArgumentException("geckoEvent must not be empty");
|
||||
}
|
||||
if (registrationParams == null || registrationParams.length == 0) {
|
||||
throw new IllegalArgumentException("registrationParams must not be empty");
|
||||
}
|
||||
|
||||
mGeckoEvent = geckoEvent;
|
||||
mRegistrationParams = registrationParams;
|
||||
mEventDataQueue = new LinkedBlockingQueue<String>();
|
||||
|
||||
final GeckoEventExpecter expecter = this;
|
||||
mListener = new GeckoEventListener() {
|
||||
@Override
|
||||
public void handleMessage(final String event, final JSONObject message) {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
|
||||
"handleMessage called for: " + event + "; expecting: " + mGeckoEvent);
|
||||
mAsserter.is(event, mGeckoEvent, "Given message occurred for registered event");
|
||||
|
||||
expecter.notifyOfEvent(message);
|
||||
}
|
||||
};
|
||||
|
||||
GeckoAppShell.registerEventListener(mGeckoEvent, mListener);
|
||||
mIsRegistered = true;
|
||||
}
|
||||
|
||||
public void blockForEvent() {
|
||||
|
@ -137,9 +88,10 @@ public class FennecNativeActions implements Actions {
|
|||
}
|
||||
|
||||
private void blockForEvent(long millis, boolean failOnTimeout) {
|
||||
if (mRegistrationParams == null) {
|
||||
if (!mIsRegistered) {
|
||||
throw new IllegalStateException("listener not registered");
|
||||
}
|
||||
|
||||
try {
|
||||
mEventData = mEventDataQueue.poll(millis, TimeUnit.MILLISECONDS);
|
||||
} catch (InterruptedException ie) {
|
||||
|
@ -161,12 +113,13 @@ public class FennecNativeActions implements Actions {
|
|||
}
|
||||
|
||||
public void blockUntilClear(long millis) {
|
||||
if (mRegistrationParams == null) {
|
||||
if (!mIsRegistered) {
|
||||
throw new IllegalStateException("listener not registered");
|
||||
}
|
||||
if (millis <= 0) {
|
||||
throw new IllegalArgumentException("millis must be > 0");
|
||||
}
|
||||
|
||||
// wait for at least one event
|
||||
try {
|
||||
mEventData = mEventDataQueue.poll(MAX_WAIT_MS, TimeUnit.MILLISECONDS);
|
||||
|
@ -205,139 +158,82 @@ public class FennecNativeActions implements Actions {
|
|||
}
|
||||
|
||||
public void unregisterListener() {
|
||||
if (mRegistrationParams == null) {
|
||||
if (!mIsRegistered) {
|
||||
throw new IllegalStateException("listener not registered");
|
||||
}
|
||||
try {
|
||||
FennecNativeDriver.log(LogLevel.INFO, "EventExpecter: no longer listening for "+mGeckoEvent);
|
||||
mUnregisterEventListener.invoke(mRobocopApi, mRegistrationParams);
|
||||
mRegistrationParams = null;
|
||||
} catch (IllegalAccessException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
}
|
||||
|
||||
FennecNativeDriver.log(LogLevel.INFO,
|
||||
"EventExpecter: no longer listening for " + mGeckoEvent);
|
||||
|
||||
GeckoAppShell.unregisterEventListener(mGeckoEvent, mListener);
|
||||
mIsRegistered = false;
|
||||
}
|
||||
|
||||
public synchronized boolean eventReceived() {
|
||||
public boolean eventReceived() {
|
||||
return mEventEverReceived;
|
||||
}
|
||||
|
||||
void notifyOfEvent(Object[] args) {
|
||||
void notifyOfEvent(final JSONObject message) {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
|
||||
"received event " + mGeckoEvent);
|
||||
synchronized (this) {
|
||||
mEventEverReceived = true;
|
||||
}
|
||||
"received event " + mGeckoEvent);
|
||||
|
||||
mEventEverReceived = true;
|
||||
|
||||
try {
|
||||
mEventDataQueue.put(args[1].toString());
|
||||
mEventDataQueue.put(message.toString());
|
||||
} catch (InterruptedException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR,
|
||||
"EventExpecter dropped event: "+args[1].toString());
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
"EventExpecter dropped event: " + message.toString(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public RepeatedEventExpecter expectGeckoEvent(String geckoEvent) {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
|
||||
"waiting for "+geckoEvent);
|
||||
try {
|
||||
Object[] finalParams = new Object[2];
|
||||
finalParams[0] = geckoEvent;
|
||||
GeckoEventExpecter expecter = new GeckoEventExpecter(geckoEvent, finalParams);
|
||||
wakeInvocationHandler wIH = new wakeInvocationHandler(expecter);
|
||||
Object proxy = Proxy.newProxyInstance(mClassLoader, new Class[] { mEventListenerClass }, wIH);
|
||||
finalParams[1] = proxy;
|
||||
|
||||
mRegisterEventListener.invoke(mRobocopApi, finalParams);
|
||||
return expecter;
|
||||
} catch (IllegalAccessException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
}
|
||||
return null;
|
||||
public RepeatedEventExpecter expectGeckoEvent(final String geckoEvent) {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG, "waiting for " + geckoEvent);
|
||||
return new GeckoEventExpecter(geckoEvent);
|
||||
}
|
||||
|
||||
public void sendGeckoEvent(String geckoEvent, String data) {
|
||||
try {
|
||||
mBroadcastEvent.invoke(mRobocopApi, geckoEvent, data);
|
||||
} catch (IllegalAccessException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPreferencesEvent(Method method, int requestId, String[] prefNames) {
|
||||
try {
|
||||
method.invoke(mRobocopApi, requestId, prefNames);
|
||||
} catch (IllegalAccessException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
}
|
||||
public void sendGeckoEvent(final String geckoEvent, final String data) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(geckoEvent, data));
|
||||
}
|
||||
|
||||
public void sendPreferencesGetEvent(int requestId, String[] prefNames) {
|
||||
sendPreferencesEvent(mPreferencesGetEvent, requestId, prefNames);
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesGetEvent(requestId, prefNames));
|
||||
}
|
||||
|
||||
public void sendPreferencesObserveEvent(int requestId, String[] prefNames) {
|
||||
sendPreferencesEvent(mPreferencesObserveEvent, requestId, prefNames);
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesObserveEvent(requestId, prefNames));
|
||||
}
|
||||
|
||||
public void sendPreferencesRemoveObserversEvent(int requestId) {
|
||||
try {
|
||||
mPreferencesRemoveObserversEvent.invoke(mRobocopApi, requestId);
|
||||
} catch (IllegalAccessException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
class DrawListenerProxy implements InvocationHandler {
|
||||
private final PaintExpecter mPaintExpecter;
|
||||
|
||||
DrawListenerProxy(PaintExpecter paintExpecter) {
|
||||
mPaintExpecter = paintExpecter;
|
||||
}
|
||||
|
||||
public Object invoke(Object proxy, Method method, Object[] args) {
|
||||
String methodName = method.getName();
|
||||
if ("drawFinished".equals(methodName)) {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
|
||||
"Received drawFinished notification");
|
||||
mPaintExpecter.notifyOfEvent(args);
|
||||
} else if ("toString".equals(methodName)) {
|
||||
return "DrawListenerProxy";
|
||||
} else if ("equals".equals(methodName)) {
|
||||
return false;
|
||||
} else if ("hashCode".equals(methodName)) {
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesRemoveObserversEvent(requestId));
|
||||
}
|
||||
|
||||
class PaintExpecter implements RepeatedEventExpecter {
|
||||
private boolean mPaintDone;
|
||||
private boolean mListening;
|
||||
private static final int MAX_WAIT_MS = 90000;
|
||||
|
||||
PaintExpecter() throws IllegalAccessException, InvocationTargetException {
|
||||
Object proxy = Proxy.newProxyInstance(mClassLoader, new Class[] { mDrawListenerClass }, new DrawListenerProxy(this));
|
||||
mSetDrawListener.invoke(mRobocopApi, proxy);
|
||||
private boolean mPaintDone;
|
||||
private boolean mListening;
|
||||
|
||||
private final GeckoLayerClient mLayerClient;
|
||||
|
||||
PaintExpecter() {
|
||||
final PaintExpecter expecter = this;
|
||||
mLayerClient = GeckoAppShell.getLayerView().getLayerClient();
|
||||
mLayerClient.setDrawListener(new DrawListener() {
|
||||
@Override
|
||||
public void drawFinished() {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.DEBUG,
|
||||
"Received drawFinished notification");
|
||||
expecter.notifyOfEvent();
|
||||
}
|
||||
});
|
||||
mListening = true;
|
||||
}
|
||||
|
||||
void notifyOfEvent(Object[] args) {
|
||||
synchronized (this) {
|
||||
mPaintDone = true;
|
||||
this.notifyAll();
|
||||
}
|
||||
private synchronized void notifyOfEvent() {
|
||||
mPaintDone = true;
|
||||
this.notifyAll();
|
||||
}
|
||||
|
||||
private synchronized void blockForEvent(long millis, boolean failOnTimeout) {
|
||||
|
@ -431,23 +327,16 @@ public class FennecNativeActions implements Actions {
|
|||
if (!mListening) {
|
||||
throw new IllegalStateException("listener not registered");
|
||||
}
|
||||
try {
|
||||
FennecNativeDriver.log(LogLevel.INFO, "PaintExpecter: no longer listening for events");
|
||||
mListening = false;
|
||||
mSetDrawListener.invoke(mRobocopApi, (Object)null);
|
||||
} catch (Exception e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
}
|
||||
|
||||
FennecNativeDriver.log(LogLevel.INFO,
|
||||
"PaintExpecter: no longer listening for events");
|
||||
mLayerClient.setDrawListener(null);
|
||||
mListening = false;
|
||||
}
|
||||
}
|
||||
|
||||
public RepeatedEventExpecter expectPaint() {
|
||||
try {
|
||||
return new PaintExpecter();
|
||||
} catch (Exception e) {
|
||||
FennecNativeDriver.log(LogLevel.ERROR, e);
|
||||
return null;
|
||||
}
|
||||
return new PaintExpecter();
|
||||
}
|
||||
|
||||
public void sendSpecialKey(SpecialKey button) {
|
||||
|
@ -495,14 +384,7 @@ public class FennecNativeActions implements Actions {
|
|||
mSolo.drag(startingX, endingX, startingY, endingY, 10);
|
||||
}
|
||||
|
||||
public Cursor querySql(String dbPath, String sql) {
|
||||
try {
|
||||
return (Cursor)mQuerySql.invoke(mRobocopApi, dbPath, sql);
|
||||
} catch(InvocationTargetException ex) {
|
||||
Log.e(LOGTAG, "Error invoking method", ex);
|
||||
} catch(IllegalAccessException ex) {
|
||||
Log.e(LOGTAG, "Error using field", ex);
|
||||
}
|
||||
return null;
|
||||
public Cursor querySql(final String dbPath, final String sql) {
|
||||
return new SQLiteBridge(dbPath).rawQuery(sql, null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import org.mozilla.gecko.gfx.LayerView;
|
||||
import org.mozilla.gecko.gfx.PanningPerfAPI;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.DataOutputStream;
|
||||
|
@ -20,11 +24,6 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.opengl.GLSurfaceView;
|
||||
import android.view.View;
|
||||
|
@ -46,18 +45,6 @@ public class FennecNativeDriver implements Driver {
|
|||
private static String mLogFile = null;
|
||||
private static LogLevel mLogLevel = LogLevel.INFO;
|
||||
|
||||
// Objects for reflexive access of fennec classes.
|
||||
private ClassLoader mClassLoader;
|
||||
private Class mApiClass;
|
||||
private Class mEventListenerClass;
|
||||
private Method mRegisterEventListener;
|
||||
private Method mGetPixels;
|
||||
private Method mStartFrameRecording;
|
||||
private Method mStopFrameRecording;
|
||||
private Method mStartCheckerboardRecording;
|
||||
private Method mStopCheckerboardRecording;
|
||||
private Object mRobocopApi;
|
||||
|
||||
public enum LogLevel {
|
||||
DEBUG(1),
|
||||
INFO(2),
|
||||
|
@ -83,25 +70,6 @@ public class FennecNativeDriver implements Driver {
|
|||
|
||||
// Set up table of fennec_ids.
|
||||
mLocators = convertTextToTable(getFile(mRootPath + "/fennec_ids.txt"));
|
||||
|
||||
// Set up reflexive access of java classes and methods.
|
||||
try {
|
||||
mClassLoader = activity.getClassLoader();
|
||||
|
||||
mApiClass = mClassLoader.loadClass("org.mozilla.gecko.RobocopAPI");
|
||||
mEventListenerClass = mClassLoader.loadClass("org.mozilla.gecko.util.GeckoEventListener");
|
||||
|
||||
mRegisterEventListener = mApiClass.getMethod("registerEventListener", String.class, mEventListenerClass);
|
||||
mGetPixels = mApiClass.getMethod("getViewPixels", View.class);
|
||||
mStartFrameRecording = mApiClass.getDeclaredMethod("startFrameTimeRecording");
|
||||
mStopFrameRecording = mApiClass.getDeclaredMethod("stopFrameTimeRecording");
|
||||
mStartCheckerboardRecording = mApiClass.getDeclaredMethod("startCheckerboardRecording");
|
||||
mStopCheckerboardRecording = mApiClass.getDeclaredMethod("stopCheckerboardRecording");
|
||||
|
||||
mRobocopApi = mApiClass.getConstructor(Activity.class).newInstance(activity);
|
||||
} catch (Exception e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
}
|
||||
}
|
||||
|
||||
//Information on the location of the Gecko Frame.
|
||||
|
@ -172,99 +140,59 @@ public class FennecNativeDriver implements Driver {
|
|||
}
|
||||
|
||||
public void startFrameRecording() {
|
||||
try {
|
||||
mStartFrameRecording.invoke(null);
|
||||
} catch (IllegalAccessException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
}
|
||||
PanningPerfAPI.startFrameTimeRecording();
|
||||
}
|
||||
|
||||
public int stopFrameRecording() {
|
||||
try {
|
||||
List<Long> frames = (List<Long>)mStopFrameRecording.invoke(null);
|
||||
int badness = 0;
|
||||
for (int i = 1; i < frames.size(); i++) {
|
||||
long frameTime = frames.get(i) - frames.get(i - 1);
|
||||
int delay = (int)(frameTime - FRAME_TIME_THRESHOLD);
|
||||
// for each frame we miss, add the square of the delay. This
|
||||
// makes large delays much worse than small delays.
|
||||
if (delay > 0) {
|
||||
badness += delay * delay;
|
||||
}
|
||||
final List<Long> frames = PanningPerfAPI.stopFrameTimeRecording();
|
||||
int badness = 0;
|
||||
for (int i = 1; i < frames.size(); i++) {
|
||||
long frameTime = frames.get(i) - frames.get(i - 1);
|
||||
int delay = (int)(frameTime - FRAME_TIME_THRESHOLD);
|
||||
// for each frame we miss, add the square of the delay. This
|
||||
// makes large delays much worse than small delays.
|
||||
if (delay > 0) {
|
||||
badness += delay * delay;
|
||||
}
|
||||
// Don't do any averaging of the numbers because really we want to
|
||||
// know how bad the jank was at its worst
|
||||
return badness;
|
||||
} catch (IllegalAccessException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
}
|
||||
|
||||
// higher values are worse, and the test failing is the worst!
|
||||
return Integer.MAX_VALUE;
|
||||
// Don't do any averaging of the numbers because really we want to
|
||||
// know how bad the jank was at its worst
|
||||
return badness;
|
||||
}
|
||||
|
||||
public void startCheckerboardRecording() {
|
||||
try {
|
||||
mStartCheckerboardRecording.invoke(null);
|
||||
} catch (IllegalAccessException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
}
|
||||
PanningPerfAPI.startCheckerboardRecording();
|
||||
}
|
||||
|
||||
public float stopCheckerboardRecording() {
|
||||
try {
|
||||
List<Float> checkerboard = (List<Float>)mStopCheckerboardRecording.invoke(null);
|
||||
float total = 0;
|
||||
for (float val : checkerboard) {
|
||||
total += val;
|
||||
}
|
||||
return total * 100.0f;
|
||||
} catch (IllegalAccessException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
final List<Float> checkerboard = PanningPerfAPI.stopCheckerboardRecording();
|
||||
float total = 0;
|
||||
for (float val : checkerboard) {
|
||||
total += val;
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
return total * 100.0f;
|
||||
}
|
||||
|
||||
private View getSurfaceView() {
|
||||
ArrayList<View> views = mSolo.getCurrentViews();
|
||||
try {
|
||||
Class c = Class.forName("org.mozilla.gecko.gfx.LayerView");
|
||||
for (View v : views) {
|
||||
if (c.isInstance(v)) {
|
||||
return v;
|
||||
}
|
||||
private LayerView getSurfaceView() {
|
||||
final LayerView layerView = mSolo.getView(LayerView.class, 0);
|
||||
|
||||
if (layerView == null) {
|
||||
log(LogLevel.WARN, "getSurfaceView could not find LayerView");
|
||||
for (final View v : mSolo.getViews()) {
|
||||
log(LogLevel.WARN, " View: " + v);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
}
|
||||
log(LogLevel.WARN, "getSurfaceView could not find LayerView");
|
||||
for (View v : views) {
|
||||
log(LogLevel.WARN, v.toString());
|
||||
}
|
||||
return null;
|
||||
return layerView;
|
||||
}
|
||||
|
||||
public PaintedSurface getPaintedSurface() {
|
||||
View view = getSurfaceView();
|
||||
final LayerView view = getSurfaceView();
|
||||
if (view == null) {
|
||||
return null;
|
||||
}
|
||||
IntBuffer pixelBuffer;
|
||||
try {
|
||||
pixelBuffer = (IntBuffer)mGetPixels.invoke(mRobocopApi, view);
|
||||
} catch (Exception e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
return null;
|
||||
}
|
||||
|
||||
final IntBuffer pixelBuffer = view.getPixels();
|
||||
|
||||
// now we need to (1) flip the image, because GL likes to do things up-side-down,
|
||||
// and (2) rearrange the bits from AGBR-8888 to ARGB-8888.
|
||||
|
@ -312,27 +240,6 @@ public class FennecNativeDriver implements Driver {
|
|||
public int mScrollHeight=0;
|
||||
public int mPageHeight=10;
|
||||
|
||||
class scrollHandler implements InvocationHandler {
|
||||
public scrollHandler(){};
|
||||
public Object invoke(Object proxy, Method method, Object[] args) {
|
||||
try {
|
||||
// Disect the JSON object into the appropriate variables
|
||||
JSONObject jo = ((JSONObject)args[1]);
|
||||
mScrollHeight = jo.getInt("y");
|
||||
mHeight = jo.getInt("cheight");
|
||||
// We don't want a height of 0. That means it's a bad response.
|
||||
if (mHeight > 0) {
|
||||
mPageHeight = jo.getInt("height");
|
||||
}
|
||||
|
||||
} catch( Throwable e) {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
|
||||
"WARNING: ScrollReceived, but read wrong!");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getScrollHeight() {
|
||||
return mScrollHeight;
|
||||
}
|
||||
|
@ -344,20 +251,23 @@ public class FennecNativeDriver implements Driver {
|
|||
}
|
||||
|
||||
public void setupScrollHandling() {
|
||||
//Setup scrollHandler to catch "robocop:scroll" events.
|
||||
try {
|
||||
Class [] interfaces = new Class[1];
|
||||
interfaces[0] = mEventListenerClass;
|
||||
Object[] finalParams = new Object[2];
|
||||
finalParams[0] = "robocop:scroll";
|
||||
finalParams[1] = Proxy.newProxyInstance(mClassLoader, interfaces, new scrollHandler());
|
||||
mRegisterEventListener.invoke(mRobocopApi, finalParams);
|
||||
} catch (IllegalAccessException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
} catch (InvocationTargetException e) {
|
||||
log(LogLevel.ERROR, e);
|
||||
}
|
||||
|
||||
GeckoAppShell.registerEventListener("robocop:scroll", new GeckoEventListener() {
|
||||
@Override
|
||||
public void handleMessage(final String event, final JSONObject message) {
|
||||
try {
|
||||
mScrollHeight = message.getInt("y");
|
||||
mHeight = message.getInt("cheight");
|
||||
// We don't want a height of 0. That means it's a bad response.
|
||||
if (mHeight > 0) {
|
||||
mPageHeight = message.getInt("height");
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
FennecNativeDriver.log(FennecNativeDriver.LogLevel.WARN,
|
||||
"WARNING: ScrollReceived, but message does not contain " +
|
||||
"expected fields: " + e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -325,6 +325,7 @@ public class GeckoAppShell
|
|||
sLayerView = lv;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static LayerView getLayerView() {
|
||||
return sLayerView;
|
||||
}
|
||||
|
@ -392,6 +393,7 @@ public class GeckoAppShell
|
|||
} catch (NoSuchElementException e) {}
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static void sendEventToGecko(GeckoEvent e) {
|
||||
if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
|
||||
notifyGeckoOfEvent(e);
|
||||
|
@ -2290,6 +2292,7 @@ public class GeckoAppShell
|
|||
* any added listeners will not be invoked on the event currently being processed, but
|
||||
* will be invoked on future events of that type.
|
||||
*/
|
||||
@RobocopTarget
|
||||
public static void registerEventListener(String event, GeckoEventListener listener) {
|
||||
sEventDispatcher.registerEventListener(event, listener);
|
||||
}
|
||||
|
@ -2305,6 +2308,7 @@ public class GeckoAppShell
|
|||
* any removed listeners will still be invoked on the event currently being processed, but
|
||||
* will not be invoked on future events of that type.
|
||||
*/
|
||||
@RobocopTarget
|
||||
public static void unregisterEventListener(String event, GeckoEventListener listener) {
|
||||
sEventDispatcher.unregisterEventListener(event, listener);
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
|
|||
import org.mozilla.gecko.mozglue.JNITarget;
|
||||
import org.mozilla.gecko.mozglue.generatorannotations.GeneratorOptions;
|
||||
import org.mozilla.gecko.mozglue.generatorannotations.WrapEntireClassForJNI;
|
||||
import org.mozilla.gecko.mozglue.RobocopTarget;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Point;
|
||||
|
@ -612,6 +613,7 @@ public class GeckoEvent {
|
|||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GeckoEvent createBroadcastEvent(String subject, String data) {
|
||||
GeckoEvent event = new GeckoEvent(NativeGeckoEvent.BROADCAST);
|
||||
event.mCharacters = subject;
|
||||
|
@ -702,6 +704,7 @@ public class GeckoEvent {
|
|||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GeckoEvent createPreferencesObserveEvent(int requestId, String[] prefNames) {
|
||||
GeckoEvent event = new GeckoEvent(NativeGeckoEvent.PREFERENCES_OBSERVE);
|
||||
event.mCount = requestId;
|
||||
|
@ -709,6 +712,7 @@ public class GeckoEvent {
|
|||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GeckoEvent createPreferencesGetEvent(int requestId, String[] prefNames) {
|
||||
GeckoEvent event = new GeckoEvent(NativeGeckoEvent.PREFERENCES_GET);
|
||||
event.mCount = requestId;
|
||||
|
@ -716,6 +720,7 @@ public class GeckoEvent {
|
|||
return event;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static GeckoEvent createPreferencesRemoveObserversEvent(int requestId) {
|
||||
GeckoEvent event = new GeckoEvent(NativeGeckoEvent.PREFERENCES_REMOVE_OBSERVERS);
|
||||
event.mCount = requestId;
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
package org.mozilla.gecko;
|
||||
|
||||
import org.mozilla.gecko.gfx.GeckoLayerClient;
|
||||
import org.mozilla.gecko.gfx.LayerView;
|
||||
import org.mozilla.gecko.gfx.PanningPerfAPI;
|
||||
import org.mozilla.gecko.mozglue.GeckoLoader;
|
||||
import org.mozilla.gecko.mozglue.RobocopTarget;
|
||||
import org.mozilla.gecko.sqlite.SQLiteBridge;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.database.Cursor;
|
||||
import android.view.View;
|
||||
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Class to provide wrapper methods around methods wanted by Robocop.
|
||||
*
|
||||
* This class provides fixed entry points into code that is liable to be optimised by Proguard without
|
||||
* needing to prevent Proguard from optimising the wrapped methods.
|
||||
* Wrapping in this way still slightly hinders Proguard's ability to optimise.
|
||||
*
|
||||
* If you find yourself wanting to add a method to this class - proceed with caution. If you're writing
|
||||
* a test that's not about manipulating the UI, you might be better off using JUnit (Or similar)
|
||||
* instead of Robocop.
|
||||
*
|
||||
* Alternatively, you might be able to get what you want by reflecting on a method annotated for the
|
||||
* benefit of the C++ wrapper generator - these methods are sure to not disappear at compile-time.
|
||||
*
|
||||
* Finally, you might be able to get what you want via Reflection on Android's libraries. Those are
|
||||
* also not prone to vanishing at compile-time, but doing this might substantially complicate your
|
||||
* work, ultimately not proving worth the extra effort to avoid making a slight mess here.
|
||||
*/
|
||||
@RobocopTarget
|
||||
public class RobocopAPI {
|
||||
private final GeckoApp mGeckoApp;
|
||||
|
||||
public RobocopAPI(Activity activity) {
|
||||
mGeckoApp = (GeckoApp)activity;
|
||||
}
|
||||
|
||||
public void registerEventListener(String event, GeckoEventListener listener) {
|
||||
GeckoAppShell.registerEventListener(event, listener);
|
||||
}
|
||||
|
||||
public void unregisterEventListener(String event, GeckoEventListener listener) {
|
||||
GeckoAppShell.unregisterEventListener(event, listener);
|
||||
}
|
||||
|
||||
public void broadcastEvent(String subject, String data) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(subject, data));
|
||||
}
|
||||
|
||||
public void preferencesGetEvent(int requestId, String[] prefNames) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesGetEvent(requestId, prefNames));
|
||||
}
|
||||
|
||||
public void preferencesObserveEvent(int requestId, String[] prefNames) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesObserveEvent(requestId, prefNames));
|
||||
}
|
||||
|
||||
public void preferencesRemoveObserversEvent(int requestId) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createPreferencesRemoveObserversEvent(requestId));
|
||||
}
|
||||
|
||||
public void setDrawListener(GeckoLayerClient.DrawListener listener) {
|
||||
GeckoAppShell.getLayerView().getLayerClient().setDrawListener(listener);
|
||||
}
|
||||
|
||||
public Cursor querySql(String dbPath, String query) {
|
||||
GeckoLoader.loadSQLiteLibs(mGeckoApp, mGeckoApp.getApplication().getPackageResourcePath());
|
||||
return new SQLiteBridge(dbPath).rawQuery(query, null);
|
||||
}
|
||||
|
||||
public IntBuffer getViewPixels(View view) {
|
||||
return ((LayerView)view).getPixels();
|
||||
}
|
||||
|
||||
// PanningPerfAPI.
|
||||
public static void startFrameTimeRecording() {
|
||||
PanningPerfAPI.startFrameTimeRecording();
|
||||
}
|
||||
|
||||
public static List<Long> stopFrameTimeRecording() {
|
||||
return PanningPerfAPI.stopFrameTimeRecording();
|
||||
}
|
||||
|
||||
public static void startCheckerboardRecording() {
|
||||
PanningPerfAPI.startCheckerboardRecording();
|
||||
}
|
||||
|
||||
public static List<Float> stopCheckerboardRecording() {
|
||||
return PanningPerfAPI.stopCheckerboardRecording();
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import org.mozilla.gecko.Tabs;
|
|||
import org.mozilla.gecko.TouchEventInterceptor;
|
||||
import org.mozilla.gecko.ZoomConstraints;
|
||||
import org.mozilla.gecko.mozglue.generatorannotations.WrapElementForJNI;
|
||||
import org.mozilla.gecko.mozglue.RobocopTarget;
|
||||
import org.mozilla.gecko.util.EventDispatcher;
|
||||
|
||||
import android.content.Context;
|
||||
|
@ -324,6 +325,7 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener
|
|||
}
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public GeckoLayerClient getLayerClient() { return mLayerClient; }
|
||||
public PanZoomController getPanZoomController() { return mPanZoomController; }
|
||||
public LayerMarginsAnimator getLayerMarginsAnimator() { return mMarginsAnimator; }
|
||||
|
@ -456,6 +458,7 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener
|
|||
}
|
||||
|
||||
/** Used by robocop for testing purposes. Not for production use! */
|
||||
@RobocopTarget
|
||||
public IntBuffer getPixels() {
|
||||
return mRenderer.getPixels();
|
||||
}
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
|
||||
package org.mozilla.gecko.gfx;
|
||||
|
||||
import org.mozilla.gecko.mozglue.RobocopTarget;
|
||||
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
|
@ -40,6 +42,7 @@ public class PanningPerfAPI {
|
|||
}
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static void startFrameTimeRecording() {
|
||||
if (mRecordingFrames || mRecordingCheckerboard) {
|
||||
Log.e(LOGTAG, "Error: startFrameTimeRecording() called while already recording!");
|
||||
|
@ -50,6 +53,7 @@ public class PanningPerfAPI {
|
|||
mFrameStartTime = SystemClock.uptimeMillis();
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static List<Long> stopFrameTimeRecording() {
|
||||
if (!mRecordingFrames) {
|
||||
Log.e(LOGTAG, "Error: stopFrameTimeRecording() called when not recording!");
|
||||
|
@ -70,6 +74,7 @@ public class PanningPerfAPI {
|
|||
return mRecordingCheckerboard;
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static void startCheckerboardRecording() {
|
||||
if (mRecordingCheckerboard || mRecordingFrames) {
|
||||
Log.e(LOGTAG, "Error: startCheckerboardRecording() called while already recording!");
|
||||
|
@ -80,6 +85,7 @@ public class PanningPerfAPI {
|
|||
mCheckerboardStartTime = SystemClock.uptimeMillis();
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static List<Float> stopCheckerboardRecording() {
|
||||
if (!mRecordingCheckerboard) {
|
||||
Log.e(LOGTAG, "Error: stopCheckerboardRecording() called when not recording!");
|
||||
|
|
|
@ -325,19 +325,67 @@ public class BrowserSearch extends HomeFragment
|
|||
}
|
||||
|
||||
private void handleAutocomplete(String searchTerm, Cursor c) {
|
||||
if (TextUtils.isEmpty(mSearchTerm) || c == null || mAutocompleteHandler == null) {
|
||||
if (c == null ||
|
||||
mAutocompleteHandler == null ||
|
||||
TextUtils.isEmpty(searchTerm)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid searching the path if we don't have to. Currently just
|
||||
// decided by if there is a '/' character in the string.
|
||||
final boolean searchPath = (searchTerm.indexOf("/") > 0);
|
||||
// decided by whether there is a '/' character in the string.
|
||||
final boolean searchPath = searchTerm.indexOf('/') > 0;
|
||||
final String autocompletion = findAutocompletion(searchTerm, c, searchPath);
|
||||
|
||||
if (autocompletion != null && mAutocompleteHandler != null) {
|
||||
mAutocompleteHandler.onAutocomplete(autocompletion);
|
||||
mAutocompleteHandler = null;
|
||||
if (autocompletion == null || mAutocompleteHandler == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefetch auto-completed domain since it's a likely target
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", "http://" + autocompletion));
|
||||
|
||||
mAutocompleteHandler.onAutocomplete(autocompletion);
|
||||
mAutocompleteHandler = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the substring of a provided URI, starting at the given offset,
|
||||
* and extending up to the end of the path segment in which the provided
|
||||
* index is found.
|
||||
*
|
||||
* For example, given
|
||||
*
|
||||
* "www.reddit.com/r/boop/abcdef", 0, ?
|
||||
*
|
||||
* this method returns
|
||||
*
|
||||
* ?=2: "www.reddit.com/"
|
||||
* ?=17: "www.reddit.com/r/boop/"
|
||||
* ?=21: "www.reddit.com/r/boop/"
|
||||
* ?=22: "www.reddit.com/r/boop/abcdef"
|
||||
*
|
||||
*/
|
||||
private static String uriSubstringUpToMatchedPath(final String url, final int offset, final int begin) {
|
||||
final int afterEnd = url.length();
|
||||
|
||||
// We want to include the trailing slash, but not other characters.
|
||||
int chop = url.indexOf('/', begin);
|
||||
if (chop != -1) {
|
||||
++chop;
|
||||
if (chop < offset) {
|
||||
// This isn't supposed to happen. Fall back to returning the whole damn thing.
|
||||
return url;
|
||||
}
|
||||
} else {
|
||||
chop = url.indexOf('?', begin);
|
||||
if (chop == -1) {
|
||||
chop = url.indexOf('#', begin);
|
||||
}
|
||||
if (chop == -1) {
|
||||
chop = afterEnd;
|
||||
}
|
||||
}
|
||||
|
||||
return url.substring(offset, chop);
|
||||
}
|
||||
|
||||
private String findAutocompletion(String searchTerm, Cursor c, boolean searchPath) {
|
||||
|
@ -345,36 +393,62 @@ public class BrowserSearch extends HomeFragment
|
|||
return null;
|
||||
}
|
||||
|
||||
final int searchLength = searchTerm.length();
|
||||
final int urlIndex = c.getColumnIndexOrThrow(URLColumns.URL);
|
||||
int searchCount = 0;
|
||||
|
||||
do {
|
||||
final Uri url = Uri.parse(c.getString(urlIndex));
|
||||
final String host = StringUtils.stripCommonSubdomains(url.getHost());
|
||||
final String url = c.getString(urlIndex);
|
||||
|
||||
// Host may be null for about pages
|
||||
if (searchCount == 0) {
|
||||
// Prefetch the first item in the list since it's weighted the highest
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Session:Prefetch", url.toString()));
|
||||
}
|
||||
|
||||
// Does the completion match against the whole URL? This will match
|
||||
// about: pages, as well as user input including "http://...".
|
||||
if (url.startsWith(searchTerm)) {
|
||||
return uriSubstringUpToMatchedPath(url, 0, searchLength);
|
||||
}
|
||||
|
||||
final Uri uri = Uri.parse(url);
|
||||
final String host = uri.getHost();
|
||||
|
||||
// Host may be null for about pages.
|
||||
if (host == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final StringBuilder hostBuilder = new StringBuilder(host);
|
||||
if (hostBuilder.indexOf(searchTerm) == 0) {
|
||||
return hostBuilder.append("/").toString();
|
||||
if (host.startsWith(searchTerm)) {
|
||||
return host + "/";
|
||||
}
|
||||
|
||||
if (searchPath) {
|
||||
final List<String> path = url.getPathSegments();
|
||||
|
||||
for (String s : path) {
|
||||
hostBuilder.append("/").append(s);
|
||||
|
||||
if (hostBuilder.indexOf(searchTerm) == 0) {
|
||||
return hostBuilder.append("/").toString();
|
||||
}
|
||||
}
|
||||
final String strippedHost = StringUtils.stripCommonSubdomains(host);
|
||||
if (strippedHost.startsWith(searchTerm)) {
|
||||
return strippedHost + "/";
|
||||
}
|
||||
|
||||
searchCount++;
|
||||
++searchCount;
|
||||
|
||||
if (!searchPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, if we're matching paths, let's compare against the string itself.
|
||||
final int hostOffset = url.indexOf(strippedHost);
|
||||
if (hostOffset == -1) {
|
||||
// This was a URL string that parsed to a different host (normalized?).
|
||||
// Give up.
|
||||
continue;
|
||||
}
|
||||
|
||||
// We already matched the non-stripped host, so now we're
|
||||
// substring-searching in the part of the URL without the common
|
||||
// subdomains.
|
||||
if (url.startsWith(searchTerm, hostOffset)) {
|
||||
// Great! Return including the rest of the path segment.
|
||||
return uriSubstringUpToMatchedPath(url, hostOffset, hostOffset + searchLength);
|
||||
}
|
||||
} while (searchCount < MAX_AUTOCOMPLETE_SEARCH && c.moveToNext());
|
||||
|
||||
return null;
|
||||
|
@ -787,7 +861,7 @@ public class BrowserSearch extends HomeFragment
|
|||
mAdapter.swapCursor(c);
|
||||
|
||||
// We should handle autocompletion based on the search term
|
||||
// associated with the currently loader that has just provided
|
||||
// associated with the loader that has just provided
|
||||
// the results.
|
||||
SearchCursorLoader searchLoader = (SearchCursorLoader) loader;
|
||||
handleAutocomplete(searchLoader.getSearchTerm(), c);
|
||||
|
|
|
@ -237,6 +237,8 @@ size. -->
|
|||
<!ENTITY pref_titlebar_mode_title "Show page title">
|
||||
<!ENTITY pref_titlebar_mode_url "Show page address">
|
||||
|
||||
<!ENTITY pref_dynamic_toolbar "Hide title bar when scrolling">
|
||||
|
||||
<!ENTITY history_removed "Page removed">
|
||||
|
||||
<!ENTITY bookmark_edit_title "Edit Bookmark">
|
||||
|
|
|
@ -273,7 +273,6 @@ gbjar.sources += [
|
|||
'ReferrerReceiver.java',
|
||||
'RemoteTabs.java',
|
||||
'Restarter.java',
|
||||
'RobocopAPI.java',
|
||||
'ScrollAnimator.java',
|
||||
'ServiceNotificationClient.java',
|
||||
'SessionParser.java',
|
||||
|
|
|
@ -217,6 +217,7 @@ public final class GeckoLoader {
|
|||
#endif
|
||||
}
|
||||
|
||||
@RobocopTarget
|
||||
public static void loadSQLiteLibs(Context context, String apkName) {
|
||||
synchronized (sLibLoadingLock) {
|
||||
if (sSQLiteLibsLoaded) {
|
||||
|
|
|
@ -21,6 +21,9 @@
|
|||
android:entryValues="@array/pref_titlebar_mode_values"
|
||||
android:persistent="false" />
|
||||
|
||||
<CheckBoxPreference android:key="browser.chrome.dynamictoolbar"
|
||||
android:title="@string/pref_dynamic_toolbar" />
|
||||
|
||||
<PreferenceCategory android:title="@string/pref_category_advanced">
|
||||
|
||||
<CheckBoxPreference
|
||||
|
|
|
@ -51,6 +51,7 @@ public class SQLiteBridge {
|
|||
private static native void closeDatabase(long aDb);
|
||||
|
||||
// Takes the path to the database we want to access.
|
||||
@RobocopTarget
|
||||
public SQLiteBridge(String aDb) throws SQLiteBridgeException {
|
||||
mDb = aDb;
|
||||
}
|
||||
|
|
|
@ -234,6 +234,8 @@
|
|||
<string name="pref_titlebar_mode_title">&pref_titlebar_mode_title;</string>
|
||||
<string name="pref_titlebar_mode_url">&pref_titlebar_mode_url;</string>
|
||||
|
||||
<string name="pref_dynamic_toolbar">&pref_dynamic_toolbar;</string>
|
||||
|
||||
<string name="history_removed">&history_removed;</string>
|
||||
|
||||
<string name="bookmark_edit_title">&bookmark_edit_title;</string>
|
||||
|
|
|
@ -21,25 +21,27 @@ public final class testInputUrlBar extends BaseTest {
|
|||
startEditingMode();
|
||||
assertUrlBarText("about:home");
|
||||
|
||||
mActions.sendKeys("ab");
|
||||
assertUrlBarText("ab");
|
||||
// Avoid any auto domain completion by using a prefix that matches
|
||||
// nothing, including about: pages
|
||||
mActions.sendKeys("zy");
|
||||
assertUrlBarText("zy");
|
||||
|
||||
mActions.sendKeys("cd");
|
||||
assertUrlBarText("abcd");
|
||||
assertUrlBarText("zycd");
|
||||
|
||||
mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
|
||||
mActions.sendSpecialKey(Actions.SpecialKey.LEFT);
|
||||
|
||||
// Inserting "" should not do anything.
|
||||
mActions.sendKeys("");
|
||||
assertUrlBarText("abcd");
|
||||
assertUrlBarText("zycd");
|
||||
|
||||
mActions.sendKeys("ef");
|
||||
assertUrlBarText("abefcd");
|
||||
assertUrlBarText("zyefcd");
|
||||
|
||||
mActions.sendSpecialKey(Actions.SpecialKey.RIGHT);
|
||||
mActions.sendKeys("gh");
|
||||
assertUrlBarText("abefcghd");
|
||||
assertUrlBarText("zyefcghd");
|
||||
|
||||
final EditText editText = mUrlBarEditView;
|
||||
runOnUiThreadSync(new Runnable() {
|
||||
|
@ -49,7 +51,7 @@ public final class testInputUrlBar extends BaseTest {
|
|||
}
|
||||
});
|
||||
mActions.sendKeys("op");
|
||||
assertUrlBarText("abopefcghd");
|
||||
assertUrlBarText("zyopefcghd");
|
||||
|
||||
runOnUiThreadSync(new Runnable() {
|
||||
public void run() {
|
||||
|
@ -58,7 +60,7 @@ public final class testInputUrlBar extends BaseTest {
|
|||
}
|
||||
});
|
||||
mActions.sendKeys("qr");
|
||||
assertUrlBarText("abopefqrhd");
|
||||
assertUrlBarText("zyopefqrhd");
|
||||
|
||||
runOnUiThreadSync(new Runnable() {
|
||||
public void run() {
|
||||
|
@ -67,7 +69,7 @@ public final class testInputUrlBar extends BaseTest {
|
|||
}
|
||||
});
|
||||
mActions.sendKeys("st");
|
||||
assertUrlBarText("abstefqrhd");
|
||||
assertUrlBarText("zystefqrhd");
|
||||
|
||||
runOnUiThreadSync(new Runnable() {
|
||||
public void run() {
|
||||
|
|
|
@ -389,7 +389,8 @@ var SelectionHandler = {
|
|||
SelectionHandler.shareSelection();
|
||||
},
|
||||
showAsAction: function(aElement) {
|
||||
return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
|
||||
return !((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
|
||||
(aElement instanceof HTMLTextAreaElement));
|
||||
},
|
||||
selector: ClipboardHelper.shareContext,
|
||||
},
|
||||
|
@ -401,7 +402,8 @@ var SelectionHandler = {
|
|||
id: "search_action",
|
||||
icon: "drawable://ic_url_bar_search",
|
||||
showAsAction: function(aElement) {
|
||||
return !(aElement instanceof HTMLInputElement && aElement.mozIsTextField(false))
|
||||
return !((aElement instanceof HTMLInputElement && aElement.mozIsTextField(false)) ||
|
||||
(aElement instanceof HTMLTextAreaElement));
|
||||
},
|
||||
action: function() {
|
||||
SelectionHandler.searchSelection();
|
||||
|
|
|
@ -4283,7 +4283,9 @@ var BrowserEventHandler = {
|
|||
if (closest) {
|
||||
let uri = this._getLinkURI(closest);
|
||||
if (uri) {
|
||||
Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
|
||||
try {
|
||||
Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
|
||||
} catch (e) {}
|
||||
}
|
||||
this._doTapHighlight(closest);
|
||||
}
|
||||
|
@ -4393,7 +4395,7 @@ var BrowserEventHandler = {
|
|||
}
|
||||
|
||||
// Was the element already focused before it was clicked?
|
||||
let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser, true));
|
||||
let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser));
|
||||
|
||||
this._sendMouseEvent("mousemove", element, x, y);
|
||||
this._sendMouseEvent("mousedown", element, x, y);
|
||||
|
@ -6707,7 +6709,9 @@ var SearchEngines = {
|
|||
let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
|
||||
let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
|
||||
connector.speculativeConnect(searchURI, callbacks);
|
||||
try {
|
||||
connector.speculativeConnect(searchURI, callbacks);
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
_handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) {
|
||||
|
@ -8109,34 +8113,63 @@ var Distribution = {
|
|||
};
|
||||
|
||||
var Tabs = {
|
||||
// This object provides functions to manage a most-recently-used list
|
||||
// of tabs. Each tab has a timestamp associated with it that indicates when
|
||||
// it was last touched.
|
||||
|
||||
_enableTabExpiration: false,
|
||||
_domains: new Set(),
|
||||
|
||||
init: function() {
|
||||
// on low-memory platforms, always allow tab expiration. on high-mem
|
||||
// platforms, allow it to be turned on once we hit a low-mem situation
|
||||
// On low-memory platforms, always allow tab expiration. On high-mem
|
||||
// platforms, allow it to be turned on once we hit a low-mem situation.
|
||||
if (BrowserApp.isOnLowMemoryPlatform) {
|
||||
this._enableTabExpiration = true;
|
||||
} else {
|
||||
Services.obs.addObserver(this, "memory-pressure", false);
|
||||
}
|
||||
|
||||
Services.obs.addObserver(this, "Session:Prefetch", false);
|
||||
|
||||
BrowserApp.deck.addEventListener("pageshow", this, false);
|
||||
},
|
||||
|
||||
uninit: function() {
|
||||
if (!this._enableTabExpiration) {
|
||||
// if _enableTabExpiration is true then we won't have this
|
||||
// If _enableTabExpiration is true then we won't have this
|
||||
// observer registered any more.
|
||||
Services.obs.removeObserver(this, "memory-pressure");
|
||||
}
|
||||
|
||||
Services.obs.removeObserver(this, "Session:Prefetch");
|
||||
|
||||
BrowserApp.deck.removeEventListener("pageshow", this);
|
||||
},
|
||||
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
if (aTopic == "memory-pressure" && aData != "heap-minimize") {
|
||||
this._enableTabExpiration = true;
|
||||
Services.obs.removeObserver(this, "memory-pressure");
|
||||
switch (aTopic) {
|
||||
case "memory-pressure":
|
||||
if (aData != "heap-minimize") {
|
||||
this._enableTabExpiration = true;
|
||||
Services.obs.removeObserver(this, "memory-pressure");
|
||||
}
|
||||
break;
|
||||
case "Session:Prefetch":
|
||||
if (aData) {
|
||||
let uri = Services.io.newURI(aData, null, null);
|
||||
if (uri && !this._domains.has(uri.host)) {
|
||||
try {
|
||||
Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null);
|
||||
this._domains.add(uri.host);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
handleEvent: function(aEvent) {
|
||||
switch (aEvent.type) {
|
||||
case "pageshow":
|
||||
// Clear the domain cache whenever a page get loaded into any browser.
|
||||
this._domains.clear();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -8144,30 +8177,32 @@ var Tabs = {
|
|||
aTab.lastTouchedAt = Date.now();
|
||||
},
|
||||
|
||||
// Manage the most-recently-used list of tabs. Each tab has a timestamp
|
||||
// associated with it that indicates when it was last touched.
|
||||
expireLruTab: function() {
|
||||
if (!this._enableTabExpiration) {
|
||||
return false;
|
||||
}
|
||||
let expireTimeMs = Services.prefs.getIntPref("browser.tabs.expireTime") * 1000;
|
||||
if (expireTimeMs < 0) {
|
||||
// this behaviour is disabled
|
||||
// This behaviour is disabled.
|
||||
return false;
|
||||
}
|
||||
let tabs = BrowserApp.tabs;
|
||||
let selected = BrowserApp.selectedTab;
|
||||
let lruTab = null;
|
||||
// find the least recently used non-zombie tab
|
||||
// Find the least recently used non-zombie tab.
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
if (tabs[i] == selected || tabs[i].browser.__SS_restore) {
|
||||
// this tab is selected or already a zombie, skip it
|
||||
// This tab is selected or already a zombie, skip it.
|
||||
continue;
|
||||
}
|
||||
if (lruTab == null || tabs[i].lastTouchedAt < lruTab.lastTouchedAt) {
|
||||
lruTab = tabs[i];
|
||||
}
|
||||
}
|
||||
// if the tab was last touched more than browser.tabs.expireTime seconds ago,
|
||||
// zombify it
|
||||
// If the tab was last touched more than browser.tabs.expireTime seconds ago,
|
||||
// zombify it.
|
||||
if (lruTab) {
|
||||
let tabAgeMs = Date.now() - lruTab.lastTouchedAt;
|
||||
if (tabAgeMs > expireTimeMs) {
|
||||
|
@ -8179,7 +8214,7 @@ var Tabs = {
|
|||
return false;
|
||||
},
|
||||
|
||||
// for debugging
|
||||
// For debugging
|
||||
dump: function(aPrefix) {
|
||||
let tabs = BrowserApp.tabs;
|
||||
for (let i = 0; i < tabs.length; i++) {
|
||||
|
|
|
@ -0,0 +1,639 @@
|
|||
/* 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/. */
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["fxAccounts", "FxAccounts"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Timer.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "jwcrypto",
|
||||
"resource://gre/modules/identity/jwcrypto.jsm");
|
||||
|
||||
const DATA_FORMAT_VERSION = 1;
|
||||
const DEFAULT_STORAGE_FILENAME = "signedInUser.json";
|
||||
const ASSERTION_LIFETIME = 1000 * 60 * 5; // 5 minutes
|
||||
const KEY_LIFETIME = 1000 * 3600 * 12; // 12 hours
|
||||
const CERT_LIFETIME = 1000 * 3600 * 6; // 6 hours
|
||||
const POLL_SESSION = 1000 * 60 * 5; // 5 minutes
|
||||
const POLL_STEP = 1000 * 3; // 3 seconds
|
||||
|
||||
// loglevel preference should be one of: "FATAL", "ERROR", "WARN", "INFO",
|
||||
// "CONFIG", "DEBUG", "TRACE" or "ALL". We will be logging error messages by
|
||||
// default.
|
||||
const PREF_LOG_LEVEL = "identity.fxaccounts.loglevel";
|
||||
try {
|
||||
this.LOG_LEVEL =
|
||||
Services.prefs.getPrefType(PREF_LOG_LEVEL) == Ci.nsIPrefBranch.PREF_STRING
|
||||
&& Services.prefs.getCharPref(PREF_LOG_LEVEL);
|
||||
} catch (e) {
|
||||
this.LOG_LEVEL = Log.Level.Error;
|
||||
}
|
||||
|
||||
let log = Log.repository.getLogger("Services.FxAccounts");
|
||||
log.level = LOG_LEVEL;
|
||||
log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
|
||||
|
||||
InternalMethods = function(mock) {
|
||||
this.cert = null;
|
||||
this.keyPair = null;
|
||||
this.signedInUser = null;
|
||||
this.version = DATA_FORMAT_VERSION;
|
||||
|
||||
// Make a local copy of these constants so we can mock it in testing
|
||||
this.POLL_STEP = POLL_STEP;
|
||||
this.POLL_SESSION = POLL_SESSION;
|
||||
// We will create this.pollTimeRemaining below; it will initially be
|
||||
// set to the value of POLL_SESSION.
|
||||
|
||||
// We interact with the Firefox Accounts auth server in order to confirm that
|
||||
// a user's email has been verified and also to fetch the user's keys from
|
||||
// the server. We manage these processes in possibly long-lived promises
|
||||
// that are internal to this object (never exposed to callers). Because
|
||||
// Firefox Accounts allows for only one logged-in user, and because it's
|
||||
// conceivable that while we are waiting to verify one identity, a caller
|
||||
// could start verification on a second, different identity, we need to be
|
||||
// able to abort all work on the first sign-in process. The currentTimer and
|
||||
// generationCount are used for this purpose.
|
||||
this.whenVerifiedPromise = null;
|
||||
this.whenKeysReadyPromise = null;
|
||||
this.currentTimer = null;
|
||||
this.generationCount = 0;
|
||||
|
||||
this.fxAccountsClient = new FxAccountsClient();
|
||||
|
||||
if (mock) { // Testing.
|
||||
Object.keys(mock).forEach((prop) => {
|
||||
log.debug("InternalMethods: mocking: " + prop);
|
||||
this[prop] = mock[prop];
|
||||
});
|
||||
}
|
||||
if (!this.signedInUserStorage) {
|
||||
// Normal (i.e., non-testing) initialization.
|
||||
// We don't reference |profileDir| in the top-level module scope
|
||||
// as we may be imported before we know where it is.
|
||||
this.signedInUserStorage = new JSONStorage({
|
||||
filename: DEFAULT_STORAGE_FILENAME,
|
||||
baseDir: OS.Constants.Path.profileDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
InternalMethods.prototype = {
|
||||
|
||||
/**
|
||||
* Ask the server whether the user's email has been verified
|
||||
*/
|
||||
checkEmailStatus: function checkEmailStatus(sessionToken) {
|
||||
return this.fxAccountsClient.recoveryEmailStatus(sessionToken);
|
||||
},
|
||||
|
||||
/**
|
||||
* Once the user's email is verified, we can request the keys
|
||||
*/
|
||||
fetchKeys: function fetchKeys(keyFetchToken) {
|
||||
log.debug("fetchKeys: " + keyFetchToken);
|
||||
return this.fxAccountsClient.accountKeys(keyFetchToken);
|
||||
},
|
||||
|
||||
/*
|
||||
* Reset state such that any previous flow is canceled.
|
||||
*/
|
||||
abortExistingFlow: function abortExistingFlow() {
|
||||
if (this.currentTimer) {
|
||||
log.debug("Polling aborted; Another user signing in");
|
||||
clearTimeout(this.currentTimer);
|
||||
this.currentTimer = 0;
|
||||
}
|
||||
this.generationCount++;
|
||||
log.debug("generationCount: " + this.generationCount);
|
||||
|
||||
if (this.whenVerifiedPromise) {
|
||||
this.whenVerifiedPromise.reject(
|
||||
new Error("Verification aborted; Another user signing in"));
|
||||
this.whenVerifiedPromise = null;
|
||||
}
|
||||
|
||||
if (this.whenKeysReadyPromise) {
|
||||
this.whenKeysReadyPromise.reject(
|
||||
new Error("KeyFetch aborted; Another user signing in"));
|
||||
this.whenKeysReadyPromise = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch encryption keys for the signed-in-user from the FxA API server.
|
||||
*
|
||||
* Not for user consumption. Exists to cause the keys to be fetch.
|
||||
*
|
||||
* Returns user data so that it can be chained with other methods.
|
||||
*
|
||||
* @return Promise
|
||||
* The promise resolves to the credentials object of the signed-in user:
|
||||
* {
|
||||
* email: The user's email address
|
||||
* uid: The user's unique id
|
||||
* sessionToken: Session for the FxA server
|
||||
* kA: An encryption key from the FxA server
|
||||
* kB: An encryption key derived from the user's FxA password
|
||||
* isVerified: email verification status
|
||||
* }
|
||||
* or null if no user is signed in
|
||||
*/
|
||||
getKeys: function() {
|
||||
return this.getUserAccountData().then((data) => {
|
||||
if (!data) {
|
||||
throw new Error("Can't get keys; User is not signed in");
|
||||
}
|
||||
if (data.kA && data.kB) {
|
||||
return data;
|
||||
}
|
||||
if (!this.whenKeysReadyPromise) {
|
||||
this.whenKeysReadyPromise = Promise.defer();
|
||||
this.fetchAndUnwrapKeys(data.keyFetchToken)
|
||||
.then((data) => {
|
||||
if (this.whenKeysReadyPromise) {
|
||||
this.whenKeysReadyPromise.resolve(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
return this.whenKeysReadyPromise.promise;
|
||||
});
|
||||
},
|
||||
|
||||
fetchAndUnwrapKeys: function(keyFetchToken) {
|
||||
log.debug("fetchAndUnwrapKeys: token: " + keyFetchToken);
|
||||
return Task.spawn(function* task() {
|
||||
// Sign out if we don't have a key fetch token.
|
||||
if (!keyFetchToken) {
|
||||
yield internal.signOut();
|
||||
return null;
|
||||
}
|
||||
let myGenerationCount = internal.generationCount;
|
||||
|
||||
let {kA, wrapKB} = yield internal.fetchKeys(keyFetchToken);
|
||||
|
||||
let data = yield internal.getUserAccountData();
|
||||
|
||||
// Sanity check that the user hasn't changed out from under us
|
||||
if (data.keyFetchToken !== keyFetchToken) {
|
||||
throw new Error("Signed in user changed while fetching keys!");
|
||||
}
|
||||
|
||||
// Next statements must be synchronous until we setUserAccountData
|
||||
// so that we don't risk getting into a weird state.
|
||||
let kB_hex = CryptoUtils.xor(CommonUtils.hexToBytes(data.unwrapBKey),
|
||||
wrapKB);
|
||||
|
||||
log.debug("kB_hex: " + kB_hex);
|
||||
data.kA = CommonUtils.bytesAsHex(kA);
|
||||
data.kB = CommonUtils.bytesAsHex(kB_hex);
|
||||
|
||||
delete data.keyFetchToken;
|
||||
|
||||
log.debug("Keys Obtained: kA=" + data.kA + ", kB=" + data.kB);
|
||||
|
||||
// Before writing any data, ensure that a new flow hasn't been
|
||||
// started behind our backs.
|
||||
if (internal.generationCount !== myGenerationCount) {
|
||||
return null;
|
||||
}
|
||||
|
||||
yield internal.setUserAccountData(data);
|
||||
|
||||
// We are now ready for business. This should only be invoked once
|
||||
// per setSignedInUser(), regardless of whether we've rebooted since
|
||||
// setSignedInUser() was called.
|
||||
internal.notifyObservers("fxaccounts:onlogin");
|
||||
return data;
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
getAssertionFromCert: function(data, keyPair, cert, audience) {
|
||||
log.debug("getAssertionFromCert");
|
||||
let payload = {};
|
||||
let d = Promise.defer();
|
||||
// "audience" should look like "http://123done.org".
|
||||
// The generated assertion will expire in two minutes.
|
||||
jwcrypto.generateAssertion(cert, keyPair, audience, function(err, signed) {
|
||||
if (err) {
|
||||
log.error("getAssertionFromCert: " + err);
|
||||
d.reject(err);
|
||||
} else {
|
||||
log.debug("getAssertionFromCert returning signed: " + signed);
|
||||
d.resolve(signed);
|
||||
}
|
||||
});
|
||||
return d.promise;
|
||||
},
|
||||
|
||||
getCertificate: function(data, keyPair, mustBeValidUntil) {
|
||||
log.debug("getCertificate" + JSON.stringify(this.signedInUserStorage));
|
||||
// TODO: get the lifetime from the cert's .exp field
|
||||
if (this.cert && this.cert.validUntil > mustBeValidUntil) {
|
||||
log.debug(" getCertificate already had one");
|
||||
return Promise.resolve(this.cert.cert);
|
||||
}
|
||||
// else get our cert signed
|
||||
let willBeValidUntil = this.now() + CERT_LIFETIME;
|
||||
return this.getCertificateSigned(data.sessionToken,
|
||||
keyPair.serializedPublicKey,
|
||||
CERT_LIFETIME)
|
||||
.then((cert) => {
|
||||
this.cert = {
|
||||
cert: cert,
|
||||
validUntil: willBeValidUntil
|
||||
};
|
||||
return cert;
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
getCertificateSigned: function(sessionToken, serializedPublicKey, lifetime) {
|
||||
log.debug("getCertificateSigned: " + sessionToken + " " + serializedPublicKey);
|
||||
return this.fxAccountsClient.signCertificate(sessionToken,
|
||||
JSON.parse(serializedPublicKey),
|
||||
lifetime);
|
||||
},
|
||||
|
||||
getKeyPair: function(mustBeValidUntil) {
|
||||
if (this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
|
||||
log.debug("getKeyPair: already have a keyPair");
|
||||
return Promise.resolve(this.keyPair.keyPair);
|
||||
}
|
||||
// Otherwse, create a keypair and set validity limit.
|
||||
let willBeValidUntil = this.now() + KEY_LIFETIME;
|
||||
let d = Promise.defer();
|
||||
jwcrypto.generateKeyPair("DS160", (err, kp) => {
|
||||
if (err) {
|
||||
d.reject(err);
|
||||
} else {
|
||||
this.keyPair = {
|
||||
keyPair: kp,
|
||||
validUntil: willBeValidUntil
|
||||
};
|
||||
log.debug("got keyPair");
|
||||
delete this.cert;
|
||||
d.resolve(this.keyPair.keyPair);
|
||||
}
|
||||
});
|
||||
return d.promise;
|
||||
},
|
||||
|
||||
getUserAccountData: function() {
|
||||
// Skip disk if user is cached.
|
||||
if (this.signedInUser) {
|
||||
return Promise.resolve(this.signedInUser.accountData);
|
||||
}
|
||||
|
||||
let deferred = Promise.defer();
|
||||
this.signedInUserStorage.get()
|
||||
.then((user) => {
|
||||
log.debug("getUserAccountData -> " + JSON.stringify(user));
|
||||
if (user && user.version == this.version) {
|
||||
log.debug("setting signed in user");
|
||||
this.signedInUser = user;
|
||||
}
|
||||
deferred.resolve(user ? user.accountData : null);
|
||||
},
|
||||
(err) => {
|
||||
if (err instanceof OS.File.Error && err.becauseNoSuchFile) {
|
||||
// File hasn't been created yet. That will be done
|
||||
// on the first call to getSignedInUser
|
||||
deferred.resolve(null);
|
||||
} else {
|
||||
deferred.reject(err);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
isUserEmailVerified: function isUserEmailVerified(data) {
|
||||
return !!(data && data.isVerified);
|
||||
},
|
||||
|
||||
/**
|
||||
* Setup for and if necessary do email verification polling.
|
||||
*/
|
||||
loadAndPoll: function() {
|
||||
return this.getUserAccountData()
|
||||
.then(data => {
|
||||
if (data && !this.isUserEmailVerified(data)) {
|
||||
this.pollEmailStatus(data.sessionToken, "start");
|
||||
}
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
startVerifiedCheck: function(data) {
|
||||
log.debug("startVerifiedCheck " + JSON.stringify(data));
|
||||
// Get us to the verified state, then get the keys. This returns a promise
|
||||
// that will fire when we are completely ready.
|
||||
//
|
||||
// Login is truly complete once keys have been fetched, so once getKeys()
|
||||
// obtains and stores kA and kB, it will fire the onlogin observer
|
||||
// notification.
|
||||
return this.whenVerified(data)
|
||||
.then((data) => this.getKeys(data));
|
||||
},
|
||||
|
||||
whenVerified: function(data) {
|
||||
if (data.isVerified) {
|
||||
log.debug("already verified");
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
if (!this.whenVerifiedPromise) {
|
||||
this.whenVerifiedPromise = Promise.defer();
|
||||
log.debug("whenVerified promise starts polling for verified email");
|
||||
this.pollEmailStatus(data.sessionToken, "start");
|
||||
}
|
||||
return this.whenVerifiedPromise.promise;
|
||||
},
|
||||
|
||||
notifyObservers: function(topic) {
|
||||
log.debug("Notifying observers of user login");
|
||||
Services.obs.notifyObservers(null, topic, null);
|
||||
},
|
||||
|
||||
/**
|
||||
* Give xpcshell tests an override point for duration testing. This is
|
||||
* necessary because the tests need to manipulate the date in order to
|
||||
* simulate certificate expiration.
|
||||
*/
|
||||
now: function() {
|
||||
return Date.now();
|
||||
},
|
||||
|
||||
pollEmailStatus: function pollEmailStatus(sessionToken, why) {
|
||||
let myGenerationCount = this.generationCount;
|
||||
log.debug("entering pollEmailStatus: " + why + " " + myGenerationCount);
|
||||
if (why == "start") {
|
||||
if (this.currentTimer) {
|
||||
// safety check - this case should have been caught on
|
||||
// entry with setSignedInUser
|
||||
throw new Error("Already polling for email status");
|
||||
}
|
||||
this.pollTimeRemaining = this.POLL_SESSION;
|
||||
}
|
||||
|
||||
this.checkEmailStatus(sessionToken)
|
||||
.then((response) => {
|
||||
log.debug("checkEmailStatus -> " + JSON.stringify(response));
|
||||
// Check to see if we're still current.
|
||||
// If for some ghastly reason we are not, stop processing.
|
||||
if (this.generationCount !== myGenerationCount) {
|
||||
log.debug("generation count differs from " + this.generationCount + " - aborting");
|
||||
log.debug("sessionToken on abort is " + sessionToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (response && response.verified) {
|
||||
// Bug 947056 - Server should be able to tell FxAccounts.jsm to back
|
||||
// off or stop polling altogether
|
||||
this.getUserAccountData()
|
||||
.then((data) => {
|
||||
data.isVerified = true;
|
||||
return this.setUserAccountData(data);
|
||||
})
|
||||
.then((data) => {
|
||||
// Now that the user is verified, we can proceed to fetch keys
|
||||
if (this.whenVerifiedPromise) {
|
||||
this.whenVerifiedPromise.resolve(data);
|
||||
delete this.whenVerifiedPromise;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
log.debug("polling with step = " + this.POLL_STEP);
|
||||
this.pollTimeRemaining -= this.POLL_STEP;
|
||||
log.debug("time remaining: " + this.pollTimeRemaining);
|
||||
if (this.pollTimeRemaining > 0) {
|
||||
this.currentTimer = setTimeout(() => {
|
||||
this.pollEmailStatus(sessionToken, "timer")}, this.POLL_STEP);
|
||||
log.debug("started timer " + this.currentTimer);
|
||||
} else {
|
||||
if (this.whenVerifiedPromise) {
|
||||
this.whenVerifiedPromise.reject(
|
||||
new Error("User email verification timed out.")
|
||||
);
|
||||
delete this.whenVerifiedPromise;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setUserAccountData: function(accountData) {
|
||||
return this.signedInUserStorage.get().then((record) => {
|
||||
record.accountData = accountData;
|
||||
this.signedInUser = record;
|
||||
return this.signedInUserStorage.set(record)
|
||||
.then(() => accountData);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
let internal = null;
|
||||
|
||||
/**
|
||||
* FxAccounts delegates private methods to an instance of InternalMethods,
|
||||
* which is not exported. The xpcshell tests need two overrides:
|
||||
* 1) Access to the real internal.signedInUserStorage.
|
||||
* 2) The ability to mock InternalMethods.
|
||||
* If mockInternal is undefined, we are live.
|
||||
* If mockInternal.onlySetInternal is present, we are executing the first
|
||||
* case by binding internal to the FxAccounts instance.
|
||||
* Otherwise if we have a mock instance, we are executing the second case.
|
||||
*/
|
||||
this.FxAccounts = function(mockInternal) {
|
||||
let mocks = mockInternal;
|
||||
if (mocks && mocks.onlySetInternal) {
|
||||
mocks = null;
|
||||
}
|
||||
internal = new InternalMethods(mocks);
|
||||
if (mockInternal) {
|
||||
// Exposes the internal object for testing only.
|
||||
this.internal = internal;
|
||||
}
|
||||
}
|
||||
this.FxAccounts.prototype = Object.freeze({
|
||||
version: DATA_FORMAT_VERSION,
|
||||
|
||||
// set() makes sure that polling is happening, if necessary.
|
||||
// get() does not wait for verification, and returns an object even if
|
||||
// unverified. The caller of get() must check .isVerified .
|
||||
// The "fxaccounts:onlogin" event will fire only when the verified state
|
||||
// goes from false to true, so callers must register their observer
|
||||
// and then call get(). In particular, it will not fire when the account
|
||||
// was found to be verified in a previous boot: if our stored state says
|
||||
// the account is verified, the event will never fire. So callers must do:
|
||||
// register notification observer (go)
|
||||
// userdata = get()
|
||||
// if (userdata.isVerified()) {go()}
|
||||
|
||||
/**
|
||||
* Set the current user signed in to Firefox Accounts.
|
||||
*
|
||||
* @param credentials
|
||||
* The credentials object obtained by logging in or creating
|
||||
* an account on the FxA server:
|
||||
* {
|
||||
* email: The users email address
|
||||
* uid: The user's unique id
|
||||
* sessionToken: Session for the FxA server
|
||||
* keyFetchToken: an unused keyFetchToken
|
||||
* isVerified: true/false
|
||||
* }
|
||||
* @return Promise
|
||||
* The promise resolves to null when the data is saved
|
||||
* successfully and is rejected on error.
|
||||
*/
|
||||
setSignedInUser: function setSignedInUser(credentials) {
|
||||
log.debug("setSignedInUser - aborting any existing flows");
|
||||
internal.abortExistingFlow();
|
||||
|
||||
let record = {version: this.version, accountData: credentials };
|
||||
// Cache a clone of the credentials object.
|
||||
internal.signedInUser = JSON.parse(JSON.stringify(record));
|
||||
|
||||
// This promise waits for storage, but not for verification.
|
||||
// We're telling the caller that this is durable now.
|
||||
return internal.signedInUserStorage.set(record)
|
||||
.then(() => {
|
||||
if (!internal.isUserEmailVerified(credentials)) {
|
||||
internal.startVerifiedCheck(credentials);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the user currently signed in to Firefox Accounts.
|
||||
*
|
||||
* @return Promise
|
||||
* The promise resolves to the credentials object of the signed-in user:
|
||||
* {
|
||||
* email: The user's email address
|
||||
* uid: The user's unique id
|
||||
* sessionToken: Session for the FxA server
|
||||
* kA: An encryption key from the FxA server
|
||||
* kB: An encryption key derived from the user's FxA password
|
||||
* isVerified: email verification status
|
||||
* }
|
||||
* or null if no user is signed in.
|
||||
*/
|
||||
getSignedInUser: function getSignedInUser() {
|
||||
return internal.getUserAccountData()
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
if (!internal.isUserEmailVerified(data)) {
|
||||
// If the email is not verified, start polling for verification,
|
||||
// but return null right away. We don't want to return a promise
|
||||
// that might not be fulfilled for a long time.
|
||||
internal.startVerifiedCheck(credentials);
|
||||
}
|
||||
return data;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* returns a promise that fires with the assertion. If there is no verified
|
||||
* signed-in user, fires with null.
|
||||
*/
|
||||
getAssertion: function getAssertion(audience) {
|
||||
log.debug("enter getAssertion()");
|
||||
let mustBeValidUntil = internal.now() + ASSERTION_LIFETIME;
|
||||
return internal.getUserAccountData()
|
||||
.then((data) => {
|
||||
if (!data) {
|
||||
// No signed-in user
|
||||
return null;
|
||||
}
|
||||
if (!internal.isUserEmailVerified(data)) {
|
||||
// Signed-in user has not verified email
|
||||
return null;
|
||||
}
|
||||
return internal.getKeyPair(mustBeValidUntil)
|
||||
.then((keyPair) => {
|
||||
return internal.getCertificate(data, keyPair, mustBeValidUntil)
|
||||
.then((cert) => {
|
||||
return internal.getAssertionFromCert(data, keyPair,
|
||||
cert, audience)
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sign the current user out.
|
||||
*
|
||||
* @return Promise
|
||||
* The promise is rejected if a storage error occurs.
|
||||
*/
|
||||
signOut: function signOut() {
|
||||
internal.abortExistingFlow();
|
||||
internal.signedInUser = null; // clear in-memory cache
|
||||
return internal.signedInUserStorage.set(null).then(() => {
|
||||
internal.notifyObservers("fxaccounts:onlogout");
|
||||
});
|
||||
},
|
||||
|
||||
// Return the URI of the remote UI flows.
|
||||
getAccountsURI: function() {
|
||||
let url = Services.urlFormatter.formatURLPref("firefox.accounts.remoteUrl");
|
||||
if (!/^https:/.test(url)) { // Comment to un-break emacs js-mode highlighting
|
||||
throw new Error("Firefox Accounts server must use HTTPS");
|
||||
}
|
||||
return url;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* JSONStorage constructor that creates instances that may set/get
|
||||
* to a specified file, in a directory that will be created if it
|
||||
* doesn't exist.
|
||||
*
|
||||
* @param options {
|
||||
* filename: of the file to write to
|
||||
* baseDir: directory where the file resides
|
||||
* }
|
||||
* @return instance
|
||||
*/
|
||||
function JSONStorage(options) {
|
||||
this.baseDir = options.baseDir;
|
||||
this.path = OS.Path.join(options.baseDir, options.filename);
|
||||
};
|
||||
|
||||
JSONStorage.prototype = {
|
||||
set: function(contents) {
|
||||
return OS.File.makeDir(this.baseDir, {ignoreExisting: true})
|
||||
.then(CommonUtils.writeJSON.bind(null, contents, this.path));
|
||||
},
|
||||
|
||||
get: function() {
|
||||
return CommonUtils.readJSON(this.path);
|
||||
}
|
||||
};
|
||||
|
||||
// A getter for the instance to export
|
||||
XPCOMUtils.defineLazyGetter(this, "fxAccounts", function() {
|
||||
let a = new FxAccounts();
|
||||
|
||||
// XXX Bug 947061 - We need a strategy for resuming email verification after
|
||||
// browser restart
|
||||
internal.loadAndPoll();
|
||||
|
||||
return a;
|
||||
});
|
||||
|
|
@ -5,4 +5,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
TEST_DIRS += ['tests']
|
||||
EXTRA_JS_MODULES += ['FxAccountsClient.jsm']
|
||||
EXTRA_JS_MODULES += [
|
||||
'FxAccounts.jsm',
|
||||
'FxAccountsClient.jsm'
|
||||
]
|
||||
|
|
|
@ -0,0 +1,493 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
// XXX until bug 937114 is fixed
|
||||
Cu.importGlobalProperties(['atob']);
|
||||
|
||||
let log = Log.repository.getLogger("Services.FxAccounts.test");
|
||||
log.level = Log.Level.Debug;
|
||||
|
||||
// See verbose logging from FxAccounts.jsm
|
||||
Services.prefs.setCharPref("identity.fxaccounts.loglevel", "DEBUG");
|
||||
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
/*
|
||||
* The FxAccountsClient communicates with the remote Firefox
|
||||
* Accounts auth server. Mock the server calls, with a little
|
||||
* lag time to simulate some latency.
|
||||
*
|
||||
* We add the _verified attribute to mock the change in verification
|
||||
* state on the FXA server.
|
||||
*/
|
||||
function MockFxAccountsClient() {
|
||||
this._email = "nobody@example.com";
|
||||
this._verified = false;
|
||||
|
||||
// mock calls up to the auth server to determine whether the
|
||||
// user account has been verified
|
||||
this.recoveryEmailStatus = function (sessionToken) {
|
||||
// simulate a call to /recovery_email/status
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let response = {
|
||||
email: this._email,
|
||||
verified: this._verified
|
||||
};
|
||||
deferred.resolve(response);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
this.accountKeys = function (keyFetchToken) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
do_timeout(50, () => {
|
||||
let response = {
|
||||
kA: expandBytes("11"),
|
||||
wrapKB: expandBytes("22")
|
||||
};
|
||||
deferred.resolve(response);
|
||||
});
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
this.signCertificate = function() { throw "no" };
|
||||
|
||||
FxAccountsClient.apply(this);
|
||||
}
|
||||
MockFxAccountsClient.prototype = {
|
||||
__proto__: FxAccountsClient.prototype
|
||||
}
|
||||
|
||||
let MockStorage = function() {
|
||||
this.data = null;
|
||||
};
|
||||
MockStorage.prototype = Object.freeze({
|
||||
set: function (contents) {
|
||||
this.data = contents;
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
get: function () {
|
||||
return Promise.resolve(this.data);
|
||||
},
|
||||
});
|
||||
|
||||
/*
|
||||
* We need to mock the FxAccounts module's interfaces to external
|
||||
* services, such as storage and the FxAccounts client. We also
|
||||
* mock the now() method, so that we can simulate the passing of
|
||||
* time and verify that signatures expire correctly.
|
||||
*/
|
||||
let MockFxAccounts = function() {
|
||||
this._getCertificateSigned_calls = [];
|
||||
this._d_signCertificate = Promise.defer();
|
||||
this._now_is = new Date();
|
||||
|
||||
let mockInternal = {
|
||||
signedInUserStorage: new MockStorage(),
|
||||
now: () => {
|
||||
return this._now_is;
|
||||
},
|
||||
getCertificateSigned: (sessionToken, serializedPublicKey) => {
|
||||
_("mock getCerificateSigned\n");
|
||||
this._getCertificateSigned_calls.push([sessionToken, serializedPublicKey]);
|
||||
return this._d_signCertificate.promise;
|
||||
},
|
||||
fxAccountsClient: new MockFxAccountsClient()
|
||||
};
|
||||
FxAccounts.apply(this, [mockInternal]);
|
||||
};
|
||||
MockFxAccounts.prototype = {
|
||||
__proto__: FxAccounts.prototype,
|
||||
};
|
||||
|
||||
add_test(function test_non_https_remote_server_uri() {
|
||||
Services.prefs.setCharPref(
|
||||
"firefox.accounts.remoteUrl",
|
||||
"http://example.com/browser/browser/base/content/test/general/accounts_testRemoteCommands.html");
|
||||
do_check_throws_message(function () {
|
||||
fxAccounts.getAccountsURI();
|
||||
}, "Firefox Accounts server must use HTTPS");
|
||||
|
||||
Services.prefs.clearUserPref("firefox.accounts.remoteUrl");
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_task(function test_get_signed_in_user_initially_unset() {
|
||||
// This test, unlike the rest, uses an un-mocked FxAccounts instance.
|
||||
// However, we still need to pass an object to the constructor to
|
||||
// force it to expose "internal", so we can test the disk storage.
|
||||
let account = new FxAccounts({onlySetInternal: true})
|
||||
let credentials = {
|
||||
email: "foo@example.com",
|
||||
uid: "1234@lcip.org",
|
||||
assertion: "foobar",
|
||||
sessionToken: "dead",
|
||||
kA: "beef",
|
||||
kB: "cafe",
|
||||
isVerified: true
|
||||
};
|
||||
|
||||
let result = yield account.getSignedInUser();
|
||||
do_check_eq(result, null);
|
||||
|
||||
yield account.setSignedInUser(credentials);
|
||||
|
||||
let result = yield account.getSignedInUser();
|
||||
do_check_eq(result.email, credentials.email);
|
||||
do_check_eq(result.assertion, credentials.assertion);
|
||||
do_check_eq(result.kB, credentials.kB);
|
||||
|
||||
// Delete the memory cache and force the user
|
||||
// to be read and parsed from storage (e.g. disk via JSONStorage).
|
||||
delete account.internal.signedInUser;
|
||||
let result = yield account.getSignedInUser();
|
||||
do_check_eq(result.email, credentials.email);
|
||||
do_check_eq(result.assertion, credentials.assertion);
|
||||
do_check_eq(result.kB, credentials.kB);
|
||||
|
||||
// sign out
|
||||
yield account.signOut();
|
||||
|
||||
// user should be undefined after sign out
|
||||
let result = yield account.getSignedInUser();
|
||||
do_check_eq(result, null);
|
||||
});
|
||||
|
||||
/*
|
||||
* Sanity-check that our mocked client is working correctly
|
||||
*/
|
||||
add_test(function test_client_mock() {
|
||||
do_test_pending();
|
||||
|
||||
let fxa = new MockFxAccounts();
|
||||
let client = fxa.internal.fxAccountsClient;
|
||||
do_check_eq(client._verified, false);
|
||||
do_check_eq(typeof client.signIn, "function");
|
||||
|
||||
// The recoveryEmailStatus function eventually fulfills its promise
|
||||
client.recoveryEmailStatus()
|
||||
.then(response => {
|
||||
do_check_eq(response.verified, false);
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Sign in a user, and after a little while, verify the user's email.
|
||||
* Polling should detect that the email is verified, and eventually
|
||||
* 'onlogin' should be observed
|
||||
*/
|
||||
add_test(function test_verification_poll() {
|
||||
do_test_pending();
|
||||
|
||||
let fxa = new MockFxAccounts();
|
||||
let test_user = getTestUser("francine");
|
||||
|
||||
makeObserver("fxaccounts:onlogin", function() {
|
||||
log.debug("test_verification_poll observed onlogin");
|
||||
// Once email verification is complete, we will observe onlogin
|
||||
fxa.internal.getUserAccountData().then(user => {
|
||||
// And confirm that the user's state has changed
|
||||
do_check_eq(user.isVerified, true);
|
||||
do_check_eq(user.email, test_user.email);
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
fxa.setSignedInUser(test_user).then(() => {
|
||||
fxa.internal.getUserAccountData().then(user => {
|
||||
// The user is signing in, but email has not been verified yet
|
||||
do_check_eq(user.isVerified, false);
|
||||
do_timeout(200, function() {
|
||||
// Mock email verification ...
|
||||
fxa.internal.fxAccountsClient._email = test_user.email;
|
||||
fxa.internal.fxAccountsClient._verified = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Sign in the user, but never verify the email. The check-email
|
||||
* poll should time out. No login event should be observed, and the
|
||||
* internal whenVerified promise should be rejected
|
||||
*/
|
||||
add_test(function test_polling_timeout() {
|
||||
do_test_pending();
|
||||
|
||||
// This test could be better - the onlogin observer might fire on somebody
|
||||
// else's stack, and we're not making sure that we're not receiving such a
|
||||
// message. In other words, this tests either failure, or success, but not
|
||||
// both.
|
||||
|
||||
let fxa = new MockFxAccounts();
|
||||
let test_user = getTestUser("carol");
|
||||
|
||||
let removeObserver = makeObserver("fxaccounts:onlogin", function() {
|
||||
do_throw("We should not be getting a login event!");
|
||||
});
|
||||
|
||||
fxa.internal.POLL_SESSION = 1;
|
||||
fxa.internal.POLL_STEP = 2;
|
||||
|
||||
let p = fxa.internal.whenVerified({});
|
||||
|
||||
fxa.setSignedInUser(test_user).then(() => {
|
||||
p.then(
|
||||
(success) => {
|
||||
do_throw("this should not succeed");
|
||||
},
|
||||
(fail) => {
|
||||
removeObserver();
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function test_getKeys() {
|
||||
let fxa = new MockFxAccounts();
|
||||
let user = getTestUser("eusebius");
|
||||
|
||||
// Once email has been verified, we will be able to get keys
|
||||
user.isVerified = true;
|
||||
|
||||
fxa.setSignedInUser(user).then(() => {
|
||||
fxa.getSignedInUser().then((user) => {
|
||||
// Before getKeys, we have no keys
|
||||
do_check_eq(!!data.kA, false);
|
||||
do_check_eq(!!data.kB, false);
|
||||
// And we still have a key-fetch token to use
|
||||
do_check_eq(!!data.keyFetchToken, true);
|
||||
|
||||
fxa.internal.getKeys().then(() => {
|
||||
fxa.getSignedInUser().then((user) => {
|
||||
// Now we should have keys
|
||||
do_check_eq(fxa.internal.isUserEmailVerified(data), true);
|
||||
do_check_eq(!!data.isVerified, true);
|
||||
do_check_eq(data.kA, expandHex("11"));
|
||||
do_check_eq(data.kB, expandHex("66"));
|
||||
do_check_eq(data.keyFetchToken, undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Alice (User A) signs up but never verifies her email. Then Bob (User B)
|
||||
* signs in with a verified email. Ensure that no sign-in events are triggered
|
||||
* on Alice's behalf. In the end, Bob should be the signed-in user.
|
||||
*/
|
||||
add_test(function test_overlapping_signins() {
|
||||
do_test_pending();
|
||||
|
||||
let fxa = new MockFxAccounts();
|
||||
let alice = getTestUser("alice");
|
||||
let bob = getTestUser("bob");
|
||||
|
||||
makeObserver("fxaccounts:onlogin", function() {
|
||||
log.debug("test_overlapping_signins observed onlogin");
|
||||
// Once email verification is complete, we will observe onlogin
|
||||
fxa.internal.getUserAccountData().then(user => {
|
||||
do_check_eq(user.email, bob.email);
|
||||
do_check_eq(user.isVerified, true);
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
// Alice is the user signing in; her email is unverified.
|
||||
fxa.setSignedInUser(alice).then(() => {
|
||||
log.debug("Alice signing in ...");
|
||||
fxa.internal.getUserAccountData().then(user => {
|
||||
do_check_eq(user.email, alice.email);
|
||||
do_check_eq(user.isVerified, false);
|
||||
log.debug("Alice has not verified her email ...");
|
||||
|
||||
// Now Bob signs in instead and actually verifies his email
|
||||
log.debug("Bob signing in ...");
|
||||
fxa.setSignedInUser(bob).then(() => {
|
||||
do_timeout(200, function() {
|
||||
// Mock email verification ...
|
||||
log.debug("Bob verifying his email ...");
|
||||
fxa.internal.fxAccountsClient._verified = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_task(function test_getAssertion() {
|
||||
let fxa = new MockFxAccounts();
|
||||
|
||||
do_check_throws(function() {
|
||||
yield fxa.getAssertion("nonaudience");
|
||||
});
|
||||
|
||||
let creds = {
|
||||
sessionToken: "sessionToken",
|
||||
kA: expandHex("11"),
|
||||
kB: expandHex("66"),
|
||||
isVerified: true
|
||||
};
|
||||
// By putting kA/kB/isVerified in "creds", we skip ahead
|
||||
// to the "we're ready" stage.
|
||||
yield fxa.setSignedInUser(creds);
|
||||
|
||||
_("== ready to go\n");
|
||||
let now = 138000000*1000;
|
||||
let start = Date.now();
|
||||
fxa._now_is = now;
|
||||
let d = fxa.getAssertion("audience.example.com");
|
||||
// At this point, a thread has been spawned to generate the keys.
|
||||
_("-- back from fxa.getAssertion\n");
|
||||
fxa._d_signCertificate.resolve("cert1");
|
||||
let assertion = yield d;
|
||||
let finish = Date.now();
|
||||
do_check_eq(fxa._getCertificateSigned_calls.length, 1);
|
||||
do_check_eq(fxa._getCertificateSigned_calls[0][0], "sessionToken");
|
||||
do_check_neq(assertion, null);
|
||||
_("ASSERTION: "+assertion+"\n");
|
||||
let pieces = assertion.split("~");
|
||||
do_check_eq(pieces[0], "cert1");
|
||||
do_check_neq(fxa.internal.keyPair, undefined);
|
||||
_(fxa.internal.keyPair.validUntil+"\n");
|
||||
let p2 = pieces[1].split(".");
|
||||
let header = JSON.parse(atob(p2[0]));
|
||||
_("HEADER: "+JSON.stringify(header)+"\n");
|
||||
do_check_eq(header.alg, "DS128");
|
||||
let payload = JSON.parse(atob(p2[1]));
|
||||
_("PAYLOAD: "+JSON.stringify(payload)+"\n");
|
||||
do_check_eq(payload.aud, "audience.example.com");
|
||||
// FxAccounts KEY_LIFETIME
|
||||
do_check_eq(fxa.internal.keyPair.validUntil, now + (12*3600*1000));
|
||||
// FxAccounts CERT_LIFETIME
|
||||
do_check_eq(fxa.internal.cert.validUntil, now + (6*3600*1000));
|
||||
_("delta: "+(new Date(payload.exp) - now)+"\n");
|
||||
let exp = Number(payload.exp);
|
||||
// jwcrypto.jsm uses an unmocked Date.now()+2min to decide on the
|
||||
// expiration time, so we test that it's inside a specific timebox
|
||||
do_check_true(start + 2*60*1000 <= exp);
|
||||
do_check_true(exp <= finish + 2*60*1000);
|
||||
|
||||
// Reset for next call.
|
||||
fxa._d_signCertificate = Promise.defer();
|
||||
|
||||
// Getting a new assertion "soon" (i.e. w/o incrementing "now"), even for
|
||||
// a new audience, should not provoke key generation or a signing request.
|
||||
assertion = yield fxa.getAssertion("other.example.com");
|
||||
do_check_eq(fxa._getCertificateSigned_calls.length, 1);
|
||||
|
||||
// But "waiting" (i.e. incrementing "now") will need a new key+signature.
|
||||
fxa._now_is = now + 24*3600*1000;
|
||||
start = Date.now();
|
||||
d = fxa.getAssertion("third.example.com");
|
||||
fxa._d_signCertificate.resolve("cert2");
|
||||
assertion = yield d;
|
||||
finish = Date.now();
|
||||
do_check_eq(fxa._getCertificateSigned_calls.length, 2);
|
||||
do_check_eq(fxa._getCertificateSigned_calls[1][0], "sessionToken");
|
||||
pieces = assertion.split("~");
|
||||
do_check_eq(pieces[0], "cert2");
|
||||
p2 = pieces[1].split(".");
|
||||
header = JSON.parse(atob(p2[0]));
|
||||
payload = JSON.parse(atob(p2[1]));
|
||||
do_check_eq(payload.aud, "third.example.com");
|
||||
// 12*3600*1000 === FxAccounts KEY_LIFETIME
|
||||
do_check_eq(fxa.internal.keyPair.validUntil, now + 24*3600*1000 + (12*3600*1000));
|
||||
// 6*3600*1000 === FxAccounts CERT_LIFETIME
|
||||
do_check_eq(fxa.internal.cert.validUntil, now + 24*3600*1000 + (6*3600*1000));
|
||||
exp = Number(payload.exp);
|
||||
do_check_true(start + 2*60*1000 <= exp);
|
||||
do_check_true(exp <= finish + 2*60*1000);
|
||||
|
||||
_("----- DONE ----\n");
|
||||
});
|
||||
|
||||
/*
|
||||
* End of tests.
|
||||
* Utility functions follow.
|
||||
*/
|
||||
|
||||
function expandHex(two_hex) {
|
||||
// Return a 64-character hex string, encoding 32 identical bytes.
|
||||
let eight_hex = two_hex + two_hex + two_hex + two_hex;
|
||||
let thirtytwo_hex = eight_hex + eight_hex + eight_hex + eight_hex;
|
||||
return thirtytwo_hex + thirtytwo_hex;
|
||||
};
|
||||
|
||||
function expandBytes(two_hex) {
|
||||
return CommonUtils.hexToBytes(expandHex(two_hex));
|
||||
};
|
||||
|
||||
function getTestUser(name) {
|
||||
return {
|
||||
email: name + "@example.com",
|
||||
uid: "1ad7f502-4cc7-4ec1-a209-071fd2fae348",
|
||||
sessionToken: name + "'s session token",
|
||||
keyFetchToken: name + "'s keyfetch token",
|
||||
unwrapBKey: expandHex("44"),
|
||||
isVerified: false
|
||||
};
|
||||
}
|
||||
|
||||
function makeObserver(aObserveTopic, aObserveFunc) {
|
||||
let observer = {
|
||||
// nsISupports provides type management in C++
|
||||
// nsIObserver is to be an observer
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports, Ci.nsIObserver]),
|
||||
|
||||
observe: function (aSubject, aTopic, aData) {
|
||||
log.debug("observed " + aTopic + " " + aData);
|
||||
if (aTopic == aObserveTopic) {
|
||||
removeMe();
|
||||
aObserveFunc(aSubject, aTopic, aData);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function removeMe() {
|
||||
log.debug("removing observer for " + aObserveTopic);
|
||||
Services.obs.removeObserver(observer, aObserveTopic);
|
||||
}
|
||||
|
||||
Services.obs.addObserver(observer, aObserveTopic, false);
|
||||
return removeMe;
|
||||
}
|
||||
|
||||
function do_check_throws(func, result, stack)
|
||||
{
|
||||
if (!stack)
|
||||
stack = Components.stack.caller;
|
||||
|
||||
try {
|
||||
func();
|
||||
} catch (ex) {
|
||||
if (ex.name == result) {
|
||||
return;
|
||||
}
|
||||
do_throw("Expected result " + result + ", caught " + ex, stack);
|
||||
}
|
||||
|
||||
if (result) {
|
||||
do_throw("Expected result " + result + ", none thrown", stack);
|
||||
}
|
||||
}
|
|
@ -2,5 +2,6 @@
|
|||
head = head.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
|
||||
tail =
|
||||
|
||||
[test_accounts.js]
|
||||
[test_client.js]
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
this.EXPORTED_SYMBOLS = [
|
||||
"getAppInfo",
|
||||
"updateAppInfo",
|
||||
"makeFakeAppDir",
|
||||
"createFakeCrash",
|
||||
"InspectedHealthReporter",
|
||||
"getHealthReporter",
|
||||
|
@ -85,97 +84,6 @@ this.updateAppInfo = function (obj) {
|
|||
registrar.registerFactory(id, "XULAppInfo", cid, factory);
|
||||
};
|
||||
|
||||
// Reference needed in order for fake app dir provider to be active.
|
||||
let gFakeAppDirectoryProvider;
|
||||
|
||||
/**
|
||||
* Installs a fake UAppData directory.
|
||||
*
|
||||
* This is needed by tests because a UAppData directory typically isn't
|
||||
* present in the test environment.
|
||||
*
|
||||
* This function is suitable for use in different components. If we ever
|
||||
* establish a central location for convenient test helpers, this should
|
||||
* go there.
|
||||
*
|
||||
* We create the new UAppData directory under the profile's directory
|
||||
* because the profile directory is automatically cleaned as part of
|
||||
* test shutdown.
|
||||
*
|
||||
* This returns a promise that will be resolved once the new directory
|
||||
* is created and installed.
|
||||
*/
|
||||
this.makeFakeAppDir = function () {
|
||||
let dirMode = OS.Constants.libc.S_IRWXU;
|
||||
let dirService = Cc["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Ci.nsIProperties);
|
||||
let baseFile = dirService.get("ProfD", Ci.nsIFile);
|
||||
let appD = baseFile.clone();
|
||||
appD.append("UAppData");
|
||||
|
||||
if (gFakeAppDirectoryProvider) {
|
||||
return Promise.resolve(appD.path);
|
||||
}
|
||||
|
||||
function makeDir(f) {
|
||||
if (f.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dump("Creating directory: " + f.path + "\n");
|
||||
f.create(Ci.nsIFile.DIRECTORY_TYPE, dirMode);
|
||||
}
|
||||
|
||||
makeDir(appD);
|
||||
|
||||
let reportsD = appD.clone();
|
||||
reportsD.append("Crash Reports");
|
||||
|
||||
let pendingD = reportsD.clone();
|
||||
pendingD.append("pending");
|
||||
let submittedD = reportsD.clone();
|
||||
submittedD.append("submitted");
|
||||
|
||||
makeDir(reportsD);
|
||||
makeDir(pendingD);
|
||||
makeDir(submittedD);
|
||||
|
||||
let provider = {
|
||||
getFile: function (prop, persistent) {
|
||||
persistent.value = true;
|
||||
if (prop == "UAppData") {
|
||||
return appD.clone();
|
||||
}
|
||||
|
||||
throw Cr.NS_ERROR_FAILURE;
|
||||
},
|
||||
|
||||
QueryInterace: function (iid) {
|
||||
if (iid.equals(Ci.nsIDirectoryServiceProvider) ||
|
||||
iid.equals(Ci.nsISupports)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
};
|
||||
|
||||
// Register the new provider.
|
||||
dirService.QueryInterface(Ci.nsIDirectoryService)
|
||||
.registerProvider(provider);
|
||||
|
||||
// And undefine the old one.
|
||||
try {
|
||||
dirService.undefine("UAppData");
|
||||
} catch (ex) {};
|
||||
|
||||
gFakeAppDirectoryProvider = provider;
|
||||
|
||||
dump("Successfully installed fake UAppDir\n");
|
||||
return Promise.resolve(appD.path);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Creates a fake crash in the Crash Reports directory.
|
||||
*
|
||||
|
|
|
@ -20,6 +20,7 @@ Cu.import("resource://testing-common/httpd.js");
|
|||
Cu.import("resource://testing-common/services-common/bagheeraserver.js");
|
||||
Cu.import("resource://testing-common/services/metrics/mocks.jsm");
|
||||
Cu.import("resource://testing-common/services/healthreport/utils.jsm");
|
||||
Cu.import("resource://testing-common/AppData.jsm");
|
||||
|
||||
|
||||
const DUMMY_URI = "http://localhost:62013/";
|
||||
|
|
|
@ -9,6 +9,7 @@ const {utils: Cu} = Components;
|
|||
Cu.import("resource://gre/modules/Metrics.jsm");
|
||||
Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
|
||||
Cu.import("resource://testing-common/services/healthreport/utils.jsm");
|
||||
Cu.import("resource://testing-common/AppData.jsm");
|
||||
|
||||
|
||||
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"makeFakeAppDir",
|
||||
];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
// Reference needed in order for fake app dir provider to be active.
|
||||
let gFakeAppDirectoryProvider;
|
||||
|
||||
/**
|
||||
* Installs a fake UAppData directory.
|
||||
*
|
||||
* This is needed by tests because a UAppData directory typically isn't
|
||||
* present in the test environment.
|
||||
*
|
||||
* We create the new UAppData directory under the profile's directory
|
||||
* because the profile directory is automatically cleaned as part of
|
||||
* test shutdown.
|
||||
*
|
||||
* This returns a promise that will be resolved once the new directory
|
||||
* is created and installed.
|
||||
*/
|
||||
this.makeFakeAppDir = function () {
|
||||
let dirMode = OS.Constants.libc.S_IRWXU;
|
||||
let dirService = Cc["@mozilla.org/file/directory_service;1"]
|
||||
.getService(Ci.nsIProperties);
|
||||
let baseFile = dirService.get("ProfD", Ci.nsIFile);
|
||||
let appD = baseFile.clone();
|
||||
appD.append("UAppData");
|
||||
|
||||
if (gFakeAppDirectoryProvider) {
|
||||
return Promise.resolve(appD.path);
|
||||
}
|
||||
|
||||
function makeDir(f) {
|
||||
if (f.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
dump("Creating directory: " + f.path + "\n");
|
||||
f.create(Ci.nsIFile.DIRECTORY_TYPE, dirMode);
|
||||
}
|
||||
|
||||
makeDir(appD);
|
||||
|
||||
let reportsD = appD.clone();
|
||||
reportsD.append("Crash Reports");
|
||||
|
||||
let pendingD = reportsD.clone();
|
||||
pendingD.append("pending");
|
||||
let submittedD = reportsD.clone();
|
||||
submittedD.append("submitted");
|
||||
|
||||
makeDir(reportsD);
|
||||
makeDir(pendingD);
|
||||
makeDir(submittedD);
|
||||
|
||||
let provider = {
|
||||
getFile: function (prop, persistent) {
|
||||
persistent.value = true;
|
||||
if (prop == "UAppData") {
|
||||
return appD.clone();
|
||||
}
|
||||
|
||||
throw Cr.NS_ERROR_FAILURE;
|
||||
},
|
||||
|
||||
QueryInterace: function (iid) {
|
||||
if (iid.equals(Ci.nsIDirectoryServiceProvider) ||
|
||||
iid.equals(Ci.nsISupports)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
throw Cr.NS_ERROR_NO_INTERFACE;
|
||||
},
|
||||
};
|
||||
|
||||
// Register the new provider.
|
||||
dirService.QueryInterface(Ci.nsIDirectoryService)
|
||||
.registerProvider(provider);
|
||||
|
||||
// And undefine the old one.
|
||||
try {
|
||||
dirService.undefine("UAppData");
|
||||
} catch (ex) {};
|
||||
|
||||
gFakeAppDirectoryProvider = provider;
|
||||
|
||||
dump("Successfully installed fake UAppDir\n");
|
||||
return Promise.resolve(appD.path);
|
||||
};
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
TESTING_JS_MODULES := \
|
||||
AppData.jsm \
|
||||
AppInfo.jsm \
|
||||
Assert.jsm \
|
||||
$(NULL)
|
||||
|
|
|
@ -381,32 +381,8 @@ RootActor.prototype = {
|
|||
windowUtils.resumeTimeouts();
|
||||
windowUtils.suppressEventHandling(false);
|
||||
}
|
||||
},
|
||||
|
||||
/* ChromeDebuggerActor hooks. */
|
||||
|
||||
/**
|
||||
* Add the specified actor to the default actor pool connection, in order to
|
||||
* keep it alive as long as the server is. This is used by breakpoints in the
|
||||
* thread and chrome debugger actors.
|
||||
*
|
||||
* @param actor aActor
|
||||
* The actor object.
|
||||
*/
|
||||
addToParentPool: function(aActor) {
|
||||
this.conn.addActor(aActor);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the specified actor from the default actor pool.
|
||||
*
|
||||
* @param BreakpointActor aActor
|
||||
* The actor object.
|
||||
*/
|
||||
removeFromParentPool: function(aActor) {
|
||||
this.conn.removeActor(aActor);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
RootActor.prototype.requestTypes = {
|
||||
"listTabs": RootActor.prototype.onListTabs,
|
||||
|
|
|
@ -403,9 +403,7 @@ EventLoop.prototype = {
|
|||
*
|
||||
* @param aHooks object
|
||||
* An object with preNest and postNest methods for calling when entering
|
||||
* and exiting a nested event loop, addToParentPool and
|
||||
* removeFromParentPool methods for handling the lifetime of actors that
|
||||
* will outlive the thread, like breakpoints.
|
||||
* and exiting a nested event loop.
|
||||
* @param aGlobal object [optional]
|
||||
* An optional (for content debugging only) reference to the content
|
||||
* window.
|
||||
|
@ -1368,7 +1366,7 @@ ThreadActor.prototype = {
|
|||
line: aLocation.line,
|
||||
column: aLocation.column
|
||||
});
|
||||
this._hooks.addToParentPool(actor);
|
||||
this.threadLifetimePool.addActor(actor);
|
||||
}
|
||||
|
||||
// Find all scripts matching the given location
|
||||
|
@ -3568,7 +3566,7 @@ BreakpointActor.prototype = {
|
|||
onDelete: function (aRequest) {
|
||||
// Remove from the breakpoint store.
|
||||
this.threadActor.breakpointStore.removeBreakpoint(this.location);
|
||||
this.threadActor._hooks.removeFromParentPool(this);
|
||||
this.threadActor.threadLifetimePool.removeActor(this);
|
||||
// Remove the actual breakpoint from the associated scripts.
|
||||
this.removeScripts();
|
||||
return { from: this.actorID };
|
||||
|
@ -3818,9 +3816,7 @@ Object.defineProperty(Debugger.Frame.prototype, "line", {
|
|||
*
|
||||
* @param aHooks object
|
||||
* An object with preNest and postNest methods for calling when entering
|
||||
* and exiting a nested event loop and also addToParentPool and
|
||||
* removeFromParentPool methods for handling the lifetime of actors that
|
||||
* will outlive the thread, like breakpoints.
|
||||
* and exiting a nested event loop.
|
||||
*/
|
||||
function ChromeDebuggerActor(aConnection, aHooks)
|
||||
{
|
||||
|
|
|
@ -503,28 +503,6 @@ BrowserTabActor.prototype = {
|
|||
|
||||
_pendingNavigation: null,
|
||||
|
||||
/**
|
||||
* Add the specified actor to the default actor pool connection, in order to
|
||||
* keep it alive as long as the server is. This is used by breakpoints in the
|
||||
* thread actor.
|
||||
*
|
||||
* @param actor aActor
|
||||
* The actor object.
|
||||
*/
|
||||
addToParentPool: function BTA_addToParentPool(aActor) {
|
||||
this.conn.addActor(aActor);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the specified actor from the default actor pool.
|
||||
*
|
||||
* @param BreakpointActor aActor
|
||||
* The actor object.
|
||||
*/
|
||||
removeFromParentPool: function BTA_removeFromParentPool(aActor) {
|
||||
this.conn.removeActor(aActor);
|
||||
},
|
||||
|
||||
// A constant prefix that will be used to form the actor ID by the server.
|
||||
actorPrefix: "tab",
|
||||
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче