This commit is contained in:
Ryan VanderMeulen 2013-12-06 16:03:56 -05:00
Родитель e837de97ec 4b4fd0c228
Коммит 33a583eacc
102 изменённых файлов: 2673 добавлений и 1437 удалений

Просмотреть файл

@ -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;

101
testing/modules/AppData.jsm Normal file
Просмотреть файл

@ -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",

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше