зеркало из https://github.com/mozilla/gecko-dev.git
Merge fx-team to m-c. a=merge
This commit is contained in:
Коммит
28c2fca375
|
@ -255,6 +255,7 @@ pref("lightweightThemes.recommendedThemes", "[{\"id\":\"recommended-1\",\"homepa
|
|||
|
||||
// UI tour experience.
|
||||
pref("browser.uitour.enabled", true);
|
||||
pref("browser.uitour.loglevel", "Error");
|
||||
pref("browser.uitour.requireSecure", true);
|
||||
pref("browser.uitour.themeOrigin", "https://addons.mozilla.org/%LOCALE%/firefox/themes/");
|
||||
pref("browser.uitour.pinnedTabUrl", "https://support.mozilla.org/%LOCALE%/kb/pinned-tabs-keep-favorite-websites-open");
|
||||
|
|
|
@ -74,6 +74,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel
|
|||
* temporarily shown until the next state change.
|
||||
*/
|
||||
updateToolbarState: function(aReason = null) {
|
||||
if (!this.toolbarButton.node) {
|
||||
return;
|
||||
}
|
||||
let state = "";
|
||||
if (MozLoopService.errors.size) {
|
||||
state = "error";
|
||||
|
|
|
@ -474,7 +474,8 @@ var PlacesCommandHook = {
|
|||
*/
|
||||
showPlacesOrganizer: function PCH_showPlacesOrganizer(aLeftPaneRoot) {
|
||||
var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
|
||||
if (!organizer) {
|
||||
// Due to bug 528706, getMostRecentWindow can return closed windows.
|
||||
if (!organizer || organizer.closed) {
|
||||
// No currently open places window, so open one with the specified mode.
|
||||
openDialog("chrome://browser/content/places/places.xul",
|
||||
"", "chrome,toolbar=yes,dialog=no,resizable", aLeftPaneRoot);
|
||||
|
|
|
@ -70,8 +70,12 @@ function isBrowserAppTab(browser) {
|
|||
browser.messageManager.removeMessageListener("Test:IsAppTab", listener);
|
||||
resolve(data.isAppTab);
|
||||
}
|
||||
browser.messageManager.addMessageListener("Test:IsAppTab", listener);
|
||||
browser.messageManager.sendAsyncMessage("Test:GetIsAppTab");
|
||||
// It looks like same-process messages may be reordered by the message
|
||||
// manager, so we need to wait one tick before sending the message.
|
||||
executeSoon(function () {
|
||||
browser.messageManager.addMessageListener("Test:IsAppTab", listener);
|
||||
browser.messageManager.sendAsyncMessage("Test:GetIsAppTab");
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
let gOriginalEngine;
|
||||
let gEngine;
|
||||
let gUnifiedCompletePref = "browser.urlbar.unifiedcomplete";
|
||||
let gRestyleSearchesPref = "browser.urlbar.restyleSearches";
|
||||
|
||||
/**
|
||||
* Asynchronously adds visits to a page.
|
||||
|
@ -77,6 +78,7 @@ function* promiseAutocompleteResultPopup(inputText) {
|
|||
|
||||
registerCleanupFunction(() => {
|
||||
Services.prefs.clearUserPref(gUnifiedCompletePref);
|
||||
Services.prefs.clearUserPref(gRestyleSearchesPref);
|
||||
Services.search.currentEngine = gOriginalEngine;
|
||||
Services.search.removeEngine(gEngine);
|
||||
return promiseClearHistory();
|
||||
|
@ -84,6 +86,7 @@ registerCleanupFunction(() => {
|
|||
|
||||
add_task(function*() {
|
||||
Services.prefs.setBoolPref(gUnifiedCompletePref, true);
|
||||
Services.prefs.setBoolPref(gRestyleSearchesPref, true);
|
||||
});
|
||||
|
||||
add_task(function*() {
|
||||
|
|
|
@ -41,7 +41,9 @@ function test() {
|
|||
testVal("ftp://ftpx.mozilla.org/", "ftp://ftpx.mozilla.org");
|
||||
|
||||
testVal("https://user:pass@mozilla.org/", "https://user:pass@mozilla.org");
|
||||
testVal("https://user@mozilla.org/", "https://user@mozilla.org");
|
||||
testVal("http://user:pass@mozilla.org/", "http://user:pass@mozilla.org");
|
||||
testVal("http://user@mozilla.org/", "user@mozilla.org");
|
||||
testVal("http://sub.mozilla.org:666/", "sub.mozilla.org:666");
|
||||
|
||||
testVal("https://[fe80::222:19ff:fe11:8c76]/file.ext");
|
||||
|
@ -55,7 +57,16 @@ function test() {
|
|||
testVal("jar:http://mozilla.org/example.jar!/");
|
||||
testVal("view-source:http://mozilla.org/");
|
||||
|
||||
// Behaviour for hosts with no dots depends on the whitelist:
|
||||
let fixupWhitelistPref = "browser.fixup.domainwhitelist.localhost";
|
||||
Services.prefs.setBoolPref(fixupWhitelistPref, false);
|
||||
testVal("http://localhost");
|
||||
Services.prefs.setBoolPref(fixupWhitelistPref, true);
|
||||
testVal("http://localhost", "localhost");
|
||||
Services.prefs.clearUserPref(fixupWhitelistPref);
|
||||
|
||||
testVal("http:// invalid url");
|
||||
|
||||
testVal("http://someotherhostwithnodots");
|
||||
testVal("http://localhost/ foo bar baz");
|
||||
testVal("http://localhost.localdomain/ foo bar baz", "localhost.localdomain/ foo bar baz");
|
||||
|
|
|
@ -714,15 +714,25 @@ function trimURL(aURL) {
|
|||
// nsIURIFixup::createFixupURI with the result will produce a different URI.
|
||||
|
||||
// remove single trailing slash for http/https/ftp URLs
|
||||
let rv = aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
|
||||
let url = aURL.replace(/^((?:http|https|ftp):\/\/[^/]+)\/$/, "$1");
|
||||
|
||||
// Strip the leading http:// only if the host has at least one '.' or
|
||||
// looks like an ipv6 ip:
|
||||
let hostMatch = rv.match(/^http:\/\/([^\/]*)/);
|
||||
let ipv6Regex = /\[[\da-f:]*\]/;
|
||||
if (hostMatch && (hostMatch[1].contains(".") || ipv6Regex.test(hostMatch[1]))) {
|
||||
/* remove http:// unless the host starts with "ftp\d*\." or contains "@" */
|
||||
rv = rv.replace(/^http:\/\/((?!ftp\d*\.)[^\/@]+(?:\/|$))/, "$1");
|
||||
// remove http://
|
||||
if (!url.startsWith("http://")) {
|
||||
return url;
|
||||
}
|
||||
return rv;
|
||||
let urlWithoutProtocol = url.substring(7);
|
||||
|
||||
let flags = Services.uriFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP |
|
||||
Services.uriFixup.FIXUP_FLAG_FIX_SCHEME_TYPOS;
|
||||
let fixedUpURL = Services.uriFixup.createFixupURI(urlWithoutProtocol, flags);
|
||||
let expectedURLSpec;
|
||||
try {
|
||||
expectedURLSpec = makeURI(aURL).spec;
|
||||
} catch (ex) {
|
||||
return url;
|
||||
}
|
||||
if (fixedUpURL.spec == expectedURLSpec) {
|
||||
return urlWithoutProtocol;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
|
|
@ -131,7 +131,9 @@ let LoopRoomsInternal = {
|
|||
}
|
||||
|
||||
Task.spawn(function* () {
|
||||
yield MozLoopService.promiseRegisteredWithServers();
|
||||
let deferredInitialization = Promise.defer();
|
||||
MozLoopService.delayedInitialize(deferredInitialization);
|
||||
yield deferredInitialization.promise;
|
||||
|
||||
if (!gDirty) {
|
||||
callback(null, [...this.rooms.values()]);
|
||||
|
|
|
@ -318,12 +318,17 @@ let MozLoopServiceInternal = {
|
|||
|
||||
/**
|
||||
* Get endpoints with the push server and register for notifications.
|
||||
* For now we register as both a Guest and FxA user and all must succeed.
|
||||
* This should only be called from promiseRegisteredWithServers to prevent reentrancy.
|
||||
*
|
||||
* @param {LOOP_SESSION_TYPE} sessionType
|
||||
* @return {Promise} resolves with all push endpoints
|
||||
* rejects if any of the push registrations failed
|
||||
*/
|
||||
promiseRegisteredWithPushServer: function() {
|
||||
promiseRegisteredWithPushServer: function(sessionType) {
|
||||
if (!this.deferredRegistrations.has(sessionType)) {
|
||||
return Promise.reject("promiseRegisteredWithPushServer must be called while there is a " +
|
||||
"deferred in deferredRegistrations in order to prevent reentrancy");
|
||||
}
|
||||
// Wrap push notification registration call-back in a Promise.
|
||||
function registerForNotification(channelID, onNotification) {
|
||||
log.debug("registerForNotification", channelID);
|
||||
|
@ -352,19 +357,23 @@ let MozLoopServiceInternal = {
|
|||
let options = this.mocks.webSocket ? { mockWebSocket: this.mocks.webSocket } : {};
|
||||
this.pushHandler.initialize(options);
|
||||
|
||||
let callsRegGuest = registerForNotification(MozLoopService.channelIDs.callsGuest,
|
||||
if (sessionType == LOOP_SESSION_TYPE.GUEST) {
|
||||
let callsRegGuest = registerForNotification(MozLoopService.channelIDs.callsGuest,
|
||||
LoopCalls.onNotification);
|
||||
|
||||
let roomsRegGuest = registerForNotification(MozLoopService.channelIDs.roomsGuest,
|
||||
roomsPushNotification);
|
||||
return Promise.all([callsRegGuest, roomsRegGuest]);
|
||||
} else if (sessionType == LOOP_SESSION_TYPE.FXA) {
|
||||
let callsRegFxA = registerForNotification(MozLoopService.channelIDs.callsFxA,
|
||||
LoopCalls.onNotification);
|
||||
|
||||
let roomsRegGuest = registerForNotification(MozLoopService.channelIDs.roomsGuest,
|
||||
let roomsRegFxA = registerForNotification(MozLoopService.channelIDs.roomsFxA,
|
||||
roomsPushNotification);
|
||||
return Promise.all([callsRegFxA, roomsRegFxA]);
|
||||
}
|
||||
|
||||
let callsRegFxA = registerForNotification(MozLoopService.channelIDs.callsFxA,
|
||||
LoopCalls.onNotification);
|
||||
|
||||
let roomsRegFxA = registerForNotification(MozLoopService.channelIDs.roomsFxA,
|
||||
roomsPushNotification);
|
||||
|
||||
return Promise.all([callsRegGuest, roomsRegGuest, callsRegFxA, roomsRegFxA]);
|
||||
return Promise.reject("promiseRegisteredWithPushServer: Invalid sessionType");
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -389,7 +398,7 @@ let MozLoopServiceInternal = {
|
|||
// We grab the promise early in case one of the callers below delete it from the map.
|
||||
result = deferred.promise;
|
||||
|
||||
this.promiseRegisteredWithPushServer().then(() => {
|
||||
this.promiseRegisteredWithPushServer(sessionType).then(() => {
|
||||
return this.registerWithLoopServer(sessionType);
|
||||
}).then(() => {
|
||||
deferred.resolve("registered to status:" + sessionType);
|
||||
|
|
|
@ -232,7 +232,10 @@ loop.roomViews = (function(mozL10n) {
|
|||
});
|
||||
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.FAILED: {
|
||||
case ROOM_STATES.FAILED:
|
||||
case ROOM_STATES.FULL: {
|
||||
// Note: While rooms are set to hold a maximum of 2 participants, the
|
||||
// FULL case should never happen on desktop.
|
||||
return loop.conversation.GenericFailureView({
|
||||
cancelCall: this.closeWindow}
|
||||
);
|
||||
|
|
|
@ -232,7 +232,10 @@ loop.roomViews = (function(mozL10n) {
|
|||
});
|
||||
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.FAILED: {
|
||||
case ROOM_STATES.FAILED:
|
||||
case ROOM_STATES.FULL: {
|
||||
// Note: While rooms are set to hold a maximum of 2 participants, the
|
||||
// FULL case should never happen on desktop.
|
||||
return <loop.conversation.GenericFailureView
|
||||
cancelCall={this.closeWindow}
|
||||
/>;
|
||||
|
|
|
@ -753,6 +753,7 @@ html, .fx-embedded, #main,
|
|||
width: 50%;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.standalone .room-inner-info-area button {
|
||||
|
@ -762,6 +763,12 @@ html, .fx-embedded, #main,
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.standalone .room-inner-info-area a.btn {
|
||||
padding: .5em 3em .3em 3em;
|
||||
border-radius: 3px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.standalone .room-conversation h2.room-name {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
|
|
|
@ -25,7 +25,9 @@ loop.store.ActiveRoomStore = (function() {
|
|||
// There are participants in the room.
|
||||
HAS_PARTICIPANTS: "room-has-participants",
|
||||
// There was an issue with the room
|
||||
FAILED: "room-failed"
|
||||
FAILED: "room-failed",
|
||||
// The room is full
|
||||
FULL: "room-full"
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -105,8 +107,7 @@ loop.store.ActiveRoomStore = (function() {
|
|||
},
|
||||
|
||||
/**
|
||||
* Handles a room failure. Currently this prints the error to the console
|
||||
* and sets the roomState to failed.
|
||||
* Handles a room failure.
|
||||
*
|
||||
* @param {sharedActions.RoomFailure} actionData
|
||||
*/
|
||||
|
@ -116,7 +117,8 @@ loop.store.ActiveRoomStore = (function() {
|
|||
|
||||
this.setStoreState({
|
||||
error: actionData.error,
|
||||
roomState: ROOM_STATES.FAILED
|
||||
roomState: actionData.error.errno === 202 ? ROOM_STATES.FULL
|
||||
: ROOM_STATES.FAILED
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -362,6 +364,10 @@ loop.store.ActiveRoomStore = (function() {
|
|||
* Switches to READY if undefined.
|
||||
*/
|
||||
_leaveRoom: function(nextState) {
|
||||
if (loop.standaloneMedia) {
|
||||
loop.standaloneMedia.multiplexGum.reset();
|
||||
}
|
||||
|
||||
this._sdkDriver.disconnectSession();
|
||||
|
||||
if (this._timeout) {
|
||||
|
|
|
@ -177,8 +177,6 @@ loop.shared.models = (function(l10n) {
|
|||
this._connectionDestroyed);
|
||||
this.listenTo(this.session, "sessionDisconnected",
|
||||
this._sessionDisconnected);
|
||||
this.listenTo(this.session, "networkDisconnected",
|
||||
this._networkDisconnected);
|
||||
this.session.connect(this.get("apiKey"), this.get("sessionToken"),
|
||||
this._onConnectCompletion.bind(this));
|
||||
},
|
||||
|
@ -323,9 +321,17 @@ loop.shared.models = (function(l10n) {
|
|||
* @param {SessionDisconnectEvent} event
|
||||
*/
|
||||
_sessionDisconnected: function(event) {
|
||||
if(event.reason === "networkDisconnected") {
|
||||
this._signalEnd("session:network-disconnected", event);
|
||||
} else {
|
||||
this._signalEnd("session:ended", event);
|
||||
}
|
||||
},
|
||||
|
||||
_signalEnd: function(eventName, event) {
|
||||
this.set("connected", false)
|
||||
.set("ongoing", false)
|
||||
.trigger("session:ended");
|
||||
.trigger(eventName, event);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -335,24 +341,11 @@ loop.shared.models = (function(l10n) {
|
|||
* @param {ConnectionEvent} event
|
||||
*/
|
||||
_connectionDestroyed: function(event) {
|
||||
this.set("connected", false)
|
||||
.set("ongoing", false)
|
||||
.trigger("session:peer-hungup", {
|
||||
connectionId: event.connection.connectionId
|
||||
});
|
||||
this.endSession();
|
||||
},
|
||||
|
||||
/**
|
||||
* Network was disconnected.
|
||||
* http://tokbox.com/opentok/libraries/client/js/reference/ConnectionEvent.html
|
||||
*
|
||||
* @param {ConnectionEvent} event
|
||||
*/
|
||||
_networkDisconnected: function(event) {
|
||||
this.set("connected", false)
|
||||
.set("ongoing", false)
|
||||
.trigger("session:network-disconnected");
|
||||
if (event.reason === "networkDisconnected") {
|
||||
this._signalEnd("session:network-disconnected", event);
|
||||
} else {
|
||||
this._signalEnd("session:peer-hungup", event);
|
||||
}
|
||||
this.endSession();
|
||||
},
|
||||
});
|
||||
|
|
|
@ -18,6 +18,7 @@ LOOP_FEEDBACK_PRODUCT_NAME := $(shell echo $${LOOP_FEEDBACK_PRODUCT_NAME-Loop})
|
|||
LOOP_BRAND_WEBSITE_URL := $(shell echo $${LOOP_BRAND_WEBSITE_URL-"https://www.mozilla.org/firefox/"})
|
||||
LOOP_PRIVACY_WEBSITE_URL := $(shell echo $${LOOP_PRIVACY_WEBSITE_URL-"https://www.mozilla.org/privacy"})
|
||||
LOOP_LEGAL_WEBSITE_URL := $(shell echo $${LOOP_LEGAL_WEBSITE_URL-"/legal/terms"})
|
||||
LOOP_PRODUCT_HOMEPAGE_URL := $(shell echo $${LOOP_PRODUCT_HOMEPAGE_URL-"https://www.firefox.com/hello/"})
|
||||
|
||||
NODE_LOCAL_BIN=./node_modules/.bin
|
||||
|
||||
|
@ -79,6 +80,7 @@ config:
|
|||
@echo "loop.config.brandWebsiteUrl = '`echo $(LOOP_BRAND_WEBSITE_URL)`';" >> content/config.js
|
||||
@echo "loop.config.privacyWebsiteUrl = '`echo $(LOOP_PRIVACY_WEBSITE_URL)`';" >> content/config.js
|
||||
@echo "loop.config.legalWebsiteUrl = '`echo $(LOOP_LEGAL_WEBSITE_URL)`';" >> content/config.js
|
||||
@echo "loop.config.learnMoreUrl = '`echo $(LOOP_PRODUCT_HOMEPAGE_URL)`';" >> content/config.js
|
||||
@echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js
|
||||
@echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js
|
||||
@echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js
|
||||
|
|
|
@ -47,7 +47,7 @@ loop.StandaloneMozLoop = (function(mozL10n) {
|
|||
var message = "HTTP " + jqXHR.status + " " + errorThrown;
|
||||
|
||||
// Create an error with server error `errno` code attached as a property
|
||||
var err = new Error(message);
|
||||
var err = new Error(message + (jsonErr.error ? "; " + jsonErr.error : ""));
|
||||
err.errno = jsonErr.errno;
|
||||
|
||||
callback(err);
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true, React */
|
||||
/* jshint newcap:false, maxlen:false */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.standaloneRoomViews = (function(mozL10n) {
|
||||
|
@ -14,6 +15,72 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
var sharedActions = loop.shared.actions;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
var StandaloneRoomInfoArea = React.createClass({displayName: 'StandaloneRoomInfoArea',
|
||||
propTypes: {
|
||||
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
|
||||
},
|
||||
|
||||
_renderCallToActionLink: function() {
|
||||
if (this.props.helper.isFirefox(navigator.userAgent)) {
|
||||
return (
|
||||
React.DOM.a({href: loop.config.learnMoreUrl, className: "btn btn-info"},
|
||||
mozL10n.get("rooms_room_full_call_to_action_label", {
|
||||
clientShortname: mozL10n.get("clientShortname2")
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
React.DOM.a({href: loop.config.brandWebsiteUrl, className: "btn btn-info"},
|
||||
mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
|
||||
brandShortname: mozL10n.get("brandShortname")
|
||||
})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_renderContent: function() {
|
||||
switch(this.props.roomState) {
|
||||
case ROOM_STATES.INIT:
|
||||
case ROOM_STATES.READY: {
|
||||
return (
|
||||
React.DOM.button({className: "btn btn-join btn-info",
|
||||
onClick: this.props.joinRoom},
|
||||
mozL10n.get("rooms_room_join_label")
|
||||
)
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.JOINED:
|
||||
case ROOM_STATES.SESSION_CONNECTED: {
|
||||
return (
|
||||
React.DOM.p({className: "empty-room-message"},
|
||||
mozL10n.get("rooms_only_occupant_label")
|
||||
)
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.FULL:
|
||||
return (
|
||||
React.DOM.div(null,
|
||||
React.DOM.p({className: "full-room-message"},
|
||||
mozL10n.get("rooms_room_full_label")
|
||||
),
|
||||
React.DOM.p(null, this._renderCallToActionLink())
|
||||
)
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div({className: "room-inner-info-area"},
|
||||
this._renderContent()
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView',
|
||||
mixins: [Backbone.Events],
|
||||
|
||||
|
@ -21,6 +88,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
activeRoomStore:
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -129,35 +197,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
|
||||
},
|
||||
|
||||
_renderContextualRoomInfo: function() {
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.INIT:
|
||||
case ROOM_STATES.READY: {
|
||||
// Join button
|
||||
return (
|
||||
React.DOM.div({className: "room-inner-info-area"},
|
||||
React.DOM.button({className: "btn btn-join btn-info", onClick: this.joinRoom},
|
||||
mozL10n.get("rooms_room_join_label")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.JOINED:
|
||||
case ROOM_STATES.SESSION_CONNECTED: {
|
||||
// Empty room message
|
||||
return (
|
||||
React.DOM.div({className: "room-inner-info-area"},
|
||||
React.DOM.p({className: "empty-room-message"},
|
||||
mozL10n.get("rooms_only_occupant_label")
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
// XXX Render "Start your own" button when room is over capacity (see
|
||||
// bug 1074709)
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
hide: !this._roomIsActive(),
|
||||
|
@ -168,7 +207,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
return (
|
||||
React.DOM.div({className: "room-conversation-wrapper"},
|
||||
this._renderContextualRoomInfo(),
|
||||
StandaloneRoomInfoArea({roomState: this.state.roomState,
|
||||
joinRoom: this.joinRoom,
|
||||
helper: this.props.helper}),
|
||||
React.DOM.div({className: "video-layout-wrapper"},
|
||||
React.DOM.div({className: "conversation room-conversation"},
|
||||
React.DOM.h2({className: "room-name"}, this.state.roomName),
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* global loop:true, React */
|
||||
/* jshint newcap:false, maxlen:false */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.standaloneRoomViews = (function(mozL10n) {
|
||||
|
@ -14,6 +15,72 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
var sharedActions = loop.shared.actions;
|
||||
var sharedViews = loop.shared.views;
|
||||
|
||||
var StandaloneRoomInfoArea = React.createClass({
|
||||
propTypes: {
|
||||
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
|
||||
},
|
||||
|
||||
_renderCallToActionLink: function() {
|
||||
if (this.props.helper.isFirefox(navigator.userAgent)) {
|
||||
return (
|
||||
<a href={loop.config.learnMoreUrl} className="btn btn-info">
|
||||
{mozL10n.get("rooms_room_full_call_to_action_label", {
|
||||
clientShortname: mozL10n.get("clientShortname2")
|
||||
})}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<a href={loop.config.brandWebsiteUrl} className="btn btn-info">
|
||||
{mozL10n.get("rooms_room_full_call_to_action_nonFx_label", {
|
||||
brandShortname: mozL10n.get("brandShortname")
|
||||
})}
|
||||
</a>
|
||||
);
|
||||
},
|
||||
|
||||
_renderContent: function() {
|
||||
switch(this.props.roomState) {
|
||||
case ROOM_STATES.INIT:
|
||||
case ROOM_STATES.READY: {
|
||||
return (
|
||||
<button className="btn btn-join btn-info"
|
||||
onClick={this.props.joinRoom}>
|
||||
{mozL10n.get("rooms_room_join_label")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.JOINED:
|
||||
case ROOM_STATES.SESSION_CONNECTED: {
|
||||
return (
|
||||
<p className="empty-room-message">
|
||||
{mozL10n.get("rooms_only_occupant_label")}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.FULL:
|
||||
return (
|
||||
<div>
|
||||
<p className="full-room-message">
|
||||
{mozL10n.get("rooms_room_full_label")}
|
||||
</p>
|
||||
<p>{this._renderCallToActionLink()}</p>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
{this._renderContent()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var StandaloneRoomView = React.createClass({
|
||||
mixins: [Backbone.Events],
|
||||
|
||||
|
@ -21,6 +88,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
activeRoomStore:
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
helper: React.PropTypes.instanceOf(loop.shared.utils.Helper).isRequired
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
|
@ -129,35 +197,6 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
|
||||
},
|
||||
|
||||
_renderContextualRoomInfo: function() {
|
||||
switch(this.state.roomState) {
|
||||
case ROOM_STATES.INIT:
|
||||
case ROOM_STATES.READY: {
|
||||
// Join button
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
<button className="btn btn-join btn-info" onClick={this.joinRoom}>
|
||||
{mozL10n.get("rooms_room_join_label")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case ROOM_STATES.JOINED:
|
||||
case ROOM_STATES.SESSION_CONNECTED: {
|
||||
// Empty room message
|
||||
return (
|
||||
<div className="room-inner-info-area">
|
||||
<p className="empty-room-message">
|
||||
{mozL10n.get("rooms_only_occupant_label")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
// XXX Render "Start your own" button when room is over capacity (see
|
||||
// bug 1074709)
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
hide: !this._roomIsActive(),
|
||||
|
@ -168,7 +207,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
|||
|
||||
return (
|
||||
<div className="room-conversation-wrapper">
|
||||
{this._renderContextualRoomInfo()}
|
||||
<StandaloneRoomInfoArea roomState={this.state.roomState}
|
||||
joinRoom={this.joinRoom}
|
||||
helper={this.props.helper} />
|
||||
<div className="video-layout-wrapper">
|
||||
<div className="conversation room-conversation">
|
||||
<h2 className="room-name">{this.state.roomName}</h2>
|
||||
|
|
|
@ -939,7 +939,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
|||
return (
|
||||
loop.standaloneRoomViews.StandaloneRoomView({
|
||||
activeRoomStore: this.props.activeRoomStore,
|
||||
dispatcher: this.props.dispatcher}
|
||||
dispatcher: this.props.dispatcher,
|
||||
helper: this.props.helper}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -940,6 +940,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
|||
<loop.standaloneRoomViews.StandaloneRoomView
|
||||
activeRoomStore={this.props.activeRoomStore}
|
||||
dispatcher={this.props.dispatcher}
|
||||
helper={this.props.helper}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ function getConfigFile(req, res) {
|
|||
"loop.config.marketplaceUrl = 'http://fake-market.herokuapp.com/iframe-install.html'",
|
||||
"loop.config.brandWebsiteUrl = 'https://www.mozilla.org/firefox/';",
|
||||
"loop.config.privacyWebsiteUrl = 'https://www.mozilla.org/privacy';",
|
||||
"loop.config.learnMoreUrl = 'https://www.mozilla.org/hello/';",
|
||||
"loop.config.legalWebsiteUrl = '/legal/terms';",
|
||||
"loop.config.fxosApp = loop.config.fxosApp || {};",
|
||||
"loop.config.fxosApp.name = 'Loop';",
|
||||
|
|
|
@ -246,6 +246,16 @@ describe("loop.roomViews", function () {
|
|||
loop.conversation.GenericFailureView);
|
||||
});
|
||||
|
||||
it("should render the GenericFailureView if the roomState is `FULL`",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.conversation.GenericFailureView);
|
||||
});
|
||||
|
||||
it("should render the DesktopRoomInvitationView if roomState is `JOINED`",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.JOINED});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global chai */
|
||||
/* global chai, loop */
|
||||
|
||||
var expect = chai.expect;
|
||||
var sharedActions = loop.shared.actions;
|
||||
|
@ -8,6 +8,7 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
|
||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||
var sandbox, dispatcher, store, fakeMozLoop, fakeSdkDriver;
|
||||
var fakeMultiplexGum;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox = sinon.sandbox.create();
|
||||
|
@ -30,6 +31,14 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
disconnectSession: sandbox.stub()
|
||||
};
|
||||
|
||||
fakeMultiplexGum = {
|
||||
reset: sandbox.spy()
|
||||
};
|
||||
|
||||
loop.standaloneMedia = {
|
||||
multiplexGum: fakeMultiplexGum
|
||||
};
|
||||
|
||||
store = new loop.store.ActiveRoomStore({
|
||||
dispatcher: dispatcher,
|
||||
mozLoop: fakeMozLoop,
|
||||
|
@ -82,7 +91,15 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
sinon.match(ROOM_STATES.READY), fakeError);
|
||||
});
|
||||
|
||||
it("should set the state to `FAILED`", function() {
|
||||
it("should set the state to `FULL` on server errno 202", function() {
|
||||
fakeError.errno = 202;
|
||||
|
||||
store.roomFailure({error: fakeError});
|
||||
|
||||
expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
|
||||
});
|
||||
|
||||
it("should set the state to `FAILED` on generic error", function() {
|
||||
store.roomFailure({error: fakeError});
|
||||
|
||||
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
|
||||
|
@ -371,6 +388,12 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
});
|
||||
});
|
||||
|
||||
it("should reset the multiplexGum", function() {
|
||||
store.leaveRoom();
|
||||
|
||||
sinon.assert.calledOnce(fakeMultiplexGum.reset);
|
||||
});
|
||||
|
||||
it("should disconnect from the servers via the sdk", function() {
|
||||
store.connectionFailure();
|
||||
|
||||
|
@ -450,6 +473,12 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
});
|
||||
});
|
||||
|
||||
it("should reset the multiplexGum", function() {
|
||||
store.leaveRoom();
|
||||
|
||||
sinon.assert.calledOnce(fakeMultiplexGum.reset);
|
||||
});
|
||||
|
||||
it("should disconnect from the servers via the sdk", function() {
|
||||
store.windowUnload();
|
||||
|
||||
|
@ -489,6 +518,12 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
});
|
||||
});
|
||||
|
||||
it("should reset the multiplexGum", function() {
|
||||
store.leaveRoom();
|
||||
|
||||
sinon.assert.calledOnce(fakeMultiplexGum.reset);
|
||||
});
|
||||
|
||||
it("should disconnect from the servers via the sdk", function() {
|
||||
store.leaveRoom();
|
||||
|
||||
|
|
|
@ -253,6 +253,20 @@ describe("loop.shared.models", function() {
|
|||
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
|
||||
});
|
||||
|
||||
it("should trigger network-disconnected on networkDisconnect reason",
|
||||
function(done) {
|
||||
model.once("session:network-disconnected", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
var fakeEvent = {
|
||||
connectionId: 42,
|
||||
reason: "networkDisconnected"
|
||||
};
|
||||
|
||||
fakeSession.trigger("sessionDisconnected", fakeEvent);
|
||||
});
|
||||
|
||||
it("should set the connected attribute to false on sessionDisconnected",
|
||||
function() {
|
||||
fakeSession.trigger("sessionDisconnected", {reason: "ko"});
|
||||
|
@ -273,7 +287,7 @@ describe("loop.shared.models", function() {
|
|||
it("should trigger a session:peer-hungup model event",
|
||||
function(done) {
|
||||
model.once("session:peer-hungup", function(event) {
|
||||
expect(event.connectionId).eql(42);
|
||||
expect(event.connection.connectionId).eql(42);
|
||||
done();
|
||||
});
|
||||
|
||||
|
@ -288,25 +302,6 @@ describe("loop.shared.models", function() {
|
|||
sinon.assert.calledOnce(model.endSession);
|
||||
});
|
||||
});
|
||||
|
||||
describe("networkDisconnected event received", function() {
|
||||
it("should trigger a session:network-disconnected event",
|
||||
function(done) {
|
||||
model.once("session:network-disconnected", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
fakeSession.trigger("networkDisconnected");
|
||||
});
|
||||
|
||||
it("should terminate the session", function() {
|
||||
sandbox.stub(model, "endSession");
|
||||
|
||||
fakeSession.trigger("networkDisconnected", {reason: "ko"});
|
||||
|
||||
sinon.assert.calledOnce(model.endSession);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -34,7 +34,8 @@ describe("loop.standaloneRoomViews", function() {
|
|||
return TestUtils.renderIntoDocument(
|
||||
loop.standaloneRoomViews.StandaloneRoomView({
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore
|
||||
activeRoomStore: activeRoomStore,
|
||||
helper: new loop.shared.utils.Helper()
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -128,6 +129,16 @@ describe("loop.standaloneRoomViews", function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Full room message", function() {
|
||||
it("should display a full room message on FULL",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".full-room-message"))
|
||||
.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Join button", function() {
|
||||
function getJoinButton(view) {
|
||||
return view.getDOMNode().querySelector(".btn-join");
|
||||
|
@ -175,6 +186,13 @@ describe("loop.standaloneRoomViews", function() {
|
|||
expect(getLeaveButton(view).disabled).eql(true);
|
||||
});
|
||||
|
||||
it("should disable the Leave button when the room state is FULL",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.FULL});
|
||||
|
||||
expect(getLeaveButton(view).disabled).eql(true);
|
||||
});
|
||||
|
||||
it("should enable the Leave button when the room state is SESSION_CONNECTED",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
|
||||
|
|
|
@ -7,6 +7,7 @@ const { LoopCallsInternal } = Cu.import("resource:///modules/loop/LoopCalls.jsm"
|
|||
XPCOMUtils.defineLazyModuleGetter(this, "Chat",
|
||||
"resource:///modules/Chat.jsm");
|
||||
|
||||
let actionReceived = false;
|
||||
let openChatOrig = Chat.open;
|
||||
|
||||
const firstCallId = 4444333221;
|
||||
|
@ -21,107 +22,101 @@ let msgHandler = function(msg) {
|
|||
}
|
||||
};
|
||||
|
||||
add_test(function test_busy_2guest_calls() {
|
||||
add_task(function* test_busy_2guest_calls() {
|
||||
actionReceived = false;
|
||||
|
||||
mockPushHandler.registrationPushURL = kEndPointUrl;
|
||||
|
||||
MozLoopService.promiseRegisteredWithServers().then(() => {
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
|
||||
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
|
||||
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
run_next_test();
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
|
||||
|
||||
yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_busy_1fxa_1guest_calls() {
|
||||
add_task(function* test_busy_1fxa_1guest_calls() {
|
||||
actionReceived = false;
|
||||
|
||||
MozLoopService.promiseRegisteredWithServers().then(() => {
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
|
||||
yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA);
|
||||
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
|
||||
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
run_next_test();
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
|
||||
|
||||
yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_busy_2fxa_calls() {
|
||||
add_task(function* test_busy_2fxa_calls() {
|
||||
actionReceived = false;
|
||||
|
||||
MozLoopService.promiseRegisteredWithServers().then(() => {
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA);
|
||||
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
|
||||
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
run_next_test();
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
|
||||
|
||||
yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_busy_1guest_1fxa_calls() {
|
||||
add_task(function* test_busy_1guest_1fxa_calls() {
|
||||
actionReceived = false;
|
||||
|
||||
MozLoopService.promiseRegisteredWithServers().then(() => {
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.GUEST);
|
||||
yield MozLoopService.promiseRegisteredWithServers(LOOP_SESSION_TYPE.FXA);
|
||||
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
|
||||
let opened = 0;
|
||||
let windowId;
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
opened++;
|
||||
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
|
||||
};
|
||||
|
||||
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
run_next_test();
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
|
||||
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
|
||||
|
||||
yield waitForCondition(() => { return actionReceived && opened > 0; }).then(() => {
|
||||
do_check_true(opened === 1, "should open only one chat window");
|
||||
do_check_true(actionReceived, "should respond with busy/reject to second call");
|
||||
LoopCalls.clearCallInProgress(windowId);
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window for first call and rejected second call");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -565,7 +565,8 @@
|
|||
StandaloneRoomView({
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.READY})
|
||||
roomState: ROOM_STATES.READY,
|
||||
helper: {isFirefox: returnTrue}})
|
||||
)
|
||||
),
|
||||
|
||||
|
@ -574,7 +575,8 @@
|
|||
StandaloneRoomView({
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.JOINED})
|
||||
roomState: ROOM_STATES.JOINED,
|
||||
helper: {isFirefox: returnTrue}})
|
||||
)
|
||||
),
|
||||
|
||||
|
@ -583,7 +585,28 @@
|
|||
StandaloneRoomView({
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS})
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
helper: {isFirefox: returnTrue}})
|
||||
)
|
||||
),
|
||||
|
||||
Example({summary: "Standalone room conversation (full - FFx user)"},
|
||||
React.DOM.div({className: "standalone"},
|
||||
StandaloneRoomView({
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.FULL,
|
||||
helper: {isFirefox: returnTrue}})
|
||||
)
|
||||
),
|
||||
|
||||
Example({summary: "Standalone room conversation (full - non FFx user)"},
|
||||
React.DOM.div({className: "standalone"},
|
||||
StandaloneRoomView({
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
roomState: ROOM_STATES.FULL,
|
||||
helper: {isFirefox: returnFalse}})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
|
|
@ -565,7 +565,8 @@
|
|||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.READY} />
|
||||
roomState={ROOM_STATES.READY}
|
||||
helper={{isFirefox: returnTrue}} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
|
@ -574,7 +575,8 @@
|
|||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.JOINED} />
|
||||
roomState={ROOM_STATES.JOINED}
|
||||
helper={{isFirefox: returnTrue}} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
|
@ -583,7 +585,28 @@
|
|||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS}
|
||||
helper={{isFirefox: returnTrue}} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (full - FFx user)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.FULL}
|
||||
helper={{isFirefox: returnTrue}} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (full - non FFx user)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.FULL}
|
||||
helper={{isFirefox: returnFalse}} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
|
|
@ -22,6 +22,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
|||
"resource://gre/modules/NetUtil.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
|
||||
"resource:///modules/RecentWindow.jsm");
|
||||
|
||||
// PlacesUtils exposes multiple symbols, so we can't use defineLazyModuleGetter.
|
||||
Cu.import("resource://gre/modules/PlacesUtils.jsm");
|
||||
|
@ -473,7 +475,7 @@ this.PlacesUIUtils = {
|
|||
},
|
||||
|
||||
_getTopBrowserWin: function PUIU__getTopBrowserWin() {
|
||||
return Services.wm.getMostRecentWindow("navigator:browser");
|
||||
return RecentWindow.getMostRecentBrowserWindow();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -619,6 +621,10 @@ this.PlacesUIUtils = {
|
|||
return !PlacesUtils.nodeIsFolder(parentNode);
|
||||
}
|
||||
|
||||
// Generally it's always possible to remove children of a query.
|
||||
if (PlacesUtils.nodeIsQuery(parentNode))
|
||||
return true;
|
||||
|
||||
// Otherwise it has to be a child of an editable folder.
|
||||
return !this.isContentsReadOnly(parentNode);
|
||||
},
|
||||
|
|
|
@ -149,19 +149,20 @@ PlacesController.prototype = {
|
|||
return PlacesTransactions.topRedoEntry != null;
|
||||
case "cmd_cut":
|
||||
case "placesCmd_cut":
|
||||
var nodes = this._view.selectedNodes;
|
||||
// If selection includes history nodes there's no reason to allow cut.
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].itemId == -1)
|
||||
case "placesCmd_moveBookmarks":
|
||||
for (let node of this._view.selectedNodes) {
|
||||
// If selection includes history nodes or tags-as-bookmark, disallow
|
||||
// cutting.
|
||||
if (node.itemId == -1 ||
|
||||
(node.parent && PlacesUtils.nodeIsTagQuery(node.parent))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Otherwise fallback to cmd_delete check.
|
||||
// Otherwise fall through the cmd_delete check.
|
||||
case "cmd_delete":
|
||||
case "placesCmd_delete":
|
||||
case "placesCmd_deleteDataHost":
|
||||
return this._hasRemovableSelection(false);
|
||||
case "placesCmd_moveBookmarks":
|
||||
return this._hasRemovableSelection(true);
|
||||
return this._hasRemovableSelection();
|
||||
case "cmd_copy":
|
||||
case "placesCmd_copy":
|
||||
return this._view.hasSelection;
|
||||
|
@ -310,13 +311,11 @@ PlacesController.prototype = {
|
|||
* are non-removable. We don't need to worry about recursion here since it
|
||||
* is a policy decision that a removable item not be placed inside a non-
|
||||
* removable item.
|
||||
* @param aIsMoveCommand
|
||||
* True if the command for which this method is called only moves the
|
||||
* selected items to another container, false otherwise.
|
||||
*
|
||||
* @return true if all nodes in the selection can be removed,
|
||||
* false otherwise.
|
||||
*/
|
||||
_hasRemovableSelection: function PC__hasRemovableSelection(aIsMoveCommand) {
|
||||
_hasRemovableSelection() {
|
||||
var ranges = this._view.removableSelectionRanges;
|
||||
if (!ranges.length)
|
||||
return false;
|
||||
|
@ -1024,7 +1023,7 @@ PlacesController.prototype = {
|
|||
* as part of another operation.
|
||||
*/
|
||||
remove: Task.async(function* (aTxnName) {
|
||||
if (!this._hasRemovableSelection(false))
|
||||
if (!this._hasRemovableSelection())
|
||||
return;
|
||||
|
||||
NS_ASSERT(aTxnName !== undefined, "Must supply Transaction Name");
|
||||
|
@ -1545,7 +1544,7 @@ let PlacesControllerDragHelper = {
|
|||
canMoveUnwrappedNode: function (aUnwrappedNode) {
|
||||
return aUnwrappedNode.id > 0 &&
|
||||
!PlacesUtils.isRootItem(aUnwrappedNode.id) &&
|
||||
!PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent) ||
|
||||
(!aUnwrappedNode.parent || !PlacesUIUtils.isContentsReadOnly(aUnwrappedNode.parent)) &&
|
||||
aUnwrappedNode.parent != PlacesUtils.tagsFolderId &&
|
||||
aUnwrappedNode.grandParentId != PlacesUtils.tagsFolderId;
|
||||
},
|
||||
|
|
|
@ -31,7 +31,7 @@ skip-if = e10s
|
|||
# disabled for very frequent oranges - bug 551540
|
||||
skip-if = true
|
||||
|
||||
[browser_library_left_pane_commands.js]
|
||||
[browser_library_commands.js]
|
||||
[browser_drag_bookmarks_on_toolbar.js]
|
||||
skip-if = e10s # Bug ?????? - test fails - "Number of dragged items should be the same. - Got 0, expected 1"
|
||||
[browser_library_middleclick.js]
|
||||
|
|
|
@ -142,6 +142,7 @@ function test() {
|
|||
// tag a uri
|
||||
this.uri = makeURI("http://foo.com");
|
||||
PlacesUtils.tagging.tagURI(this.uri, ["bar"]);
|
||||
registerCleanupFunction(() => PlacesUtils.tagging.untagURI(this.uri, ["bar"]));
|
||||
},
|
||||
validate: function() {
|
||||
// get tag root
|
||||
|
|
|
@ -34,9 +34,8 @@ const TEST_URL = "http://www.example.com/";
|
|||
const DIALOG_URL = "chrome://browser/content/places/bookmarkProperties.xul";
|
||||
const DIALOG_URL_MINIMAL_UI = "chrome://browser/content/places/bookmarkProperties2.xul";
|
||||
|
||||
var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
|
||||
getService(Ci.nsIWindowMediator);
|
||||
var win = wm.getMostRecentWindow("navigator:browser");
|
||||
Cu.import("resource:///modules/RecentWindow.jsm");
|
||||
let win = RecentWindow.getMostRecentBrowserWindow();
|
||||
var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
|
||||
getService(Ci.nsIWindowWatcher);
|
||||
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim:set ts=2 sw=2 sts=2 et: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Test enabled commands in the left pane folder of the Library.
|
||||
*/
|
||||
|
||||
const TEST_URI = NetUtil.newURI("http://www.mozilla.org/");
|
||||
|
||||
registerCleanupFunction(function* () {
|
||||
yield PlacesUtils.bookmarks.eraseEverything();
|
||||
yield promiseClearHistory();
|
||||
});
|
||||
|
||||
add_task(function* test_date_container() {
|
||||
let library = yield promiseLibrary();
|
||||
info("Ensure date containers under History cannot be cut but can be deleted");
|
||||
|
||||
yield promiseAddVisits(TEST_URI);
|
||||
|
||||
// Select and open the left pane "History" query.
|
||||
let PO = library.PlacesOrganizer;
|
||||
|
||||
PO.selectLeftPaneQuery('History');
|
||||
isnot(PO._places.selectedNode, null, "We correctly selected History");
|
||||
|
||||
// Check that both delete and cut commands are disabled, cause this is
|
||||
// a child of the left pane folder.
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is disabled");
|
||||
let historyNode = PlacesUtils.asContainer(PO._places.selectedNode);
|
||||
historyNode.containerOpen = true;
|
||||
|
||||
// Check that we have a child container. It is "Today" container.
|
||||
is(historyNode.childCount, 1, "History node has one child");
|
||||
let todayNode = historyNode.getChild(0);
|
||||
let todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
|
||||
is(todayNode.title, todayNodeExpectedTitle,
|
||||
"History child is the expected container");
|
||||
|
||||
// Select "Today" container.
|
||||
PO._places.selectNode(todayNode);
|
||||
is(PO._places.selectedNode, todayNode,
|
||||
"We correctly selected Today container");
|
||||
// Check that delete command is enabled but cut command is disabled, cause
|
||||
// this is an history item.
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is enabled");
|
||||
|
||||
// Execute the delete command and check visit has been removed.
|
||||
let promiseURIRemoved = promiseHistoryNotification("onDeleteURI",
|
||||
() => TEST_URI.equals(arguments[0]));
|
||||
PO._places.controller.doCommand("cmd_delete");
|
||||
yield promiseURIRemoved;
|
||||
|
||||
// Test live update of "History" query.
|
||||
is(historyNode.childCount, 0, "History node has no more children");
|
||||
|
||||
historyNode.containerOpen = false;
|
||||
|
||||
ok(!(yield promiseIsURIVisited(TEST_URI)), "Visit has been removed");
|
||||
|
||||
library.close();
|
||||
});
|
||||
|
||||
add_task(function* test_query_on_toolbar() {
|
||||
let library = yield promiseLibrary();
|
||||
info("Ensure queries can be cut or deleted");
|
||||
|
||||
// Select and open the left pane "Bookmarks Toolbar" folder.
|
||||
let PO = library.PlacesOrganizer;
|
||||
|
||||
PO.selectLeftPaneQuery('BookmarksToolbar');
|
||||
isnot(PO._places.selectedNode, null, "We have a valid selection");
|
||||
is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
|
||||
PlacesUtils.toolbarFolderId,
|
||||
"We have correctly selected bookmarks toolbar node.");
|
||||
|
||||
// Check that both cut and delete commands are disabled, cause this is a child
|
||||
// of AllBookmarksFolderId.
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is disabled");
|
||||
|
||||
let toolbarNode = PlacesUtils.asContainer(PO._places.selectedNode);
|
||||
toolbarNode.containerOpen = true;
|
||||
|
||||
// Add an History query to the toolbar.
|
||||
let query = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||
url: "place:sort=4",
|
||||
title: "special_query",
|
||||
parentGuid: PlacesUtils.bookmarks.toolbarGuid,
|
||||
index: 0 });
|
||||
|
||||
// Get first child and check it is the just inserted query.
|
||||
ok(toolbarNode.childCount > 0, "Toolbar node has children");
|
||||
let queryNode = toolbarNode.getChild(0);
|
||||
is(queryNode.title, "special_query", "Query node is correctly selected");
|
||||
|
||||
// Select query node.
|
||||
PO._places.selectNode(queryNode);
|
||||
is(PO._places.selectedNode, queryNode, "We correctly selected query node");
|
||||
|
||||
// Check that both cut and delete commands are enabled.
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is enabled");
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is enabled");
|
||||
|
||||
// Execute the delete command and check bookmark has been removed.
|
||||
let promiseItemRemoved = promiseBookmarksNotification("onItemRemoved",
|
||||
() => query.guid == arguments[5]);
|
||||
PO._places.controller.doCommand("cmd_delete");
|
||||
yield promiseItemRemoved;
|
||||
|
||||
is((yield PlacesUtils.bookmarks.fetch(query.guid)), null,
|
||||
"Query node bookmark has been correctly removed");
|
||||
|
||||
toolbarNode.containerOpen = false;
|
||||
|
||||
library.close();
|
||||
});
|
||||
|
||||
add_task(function* test_search_contents() {
|
||||
let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||
url: "http://example.com/",
|
||||
title: "example page",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
index: 0 });
|
||||
|
||||
let library = yield promiseLibrary();
|
||||
info("Ensure query contents can be cut or deleted");
|
||||
|
||||
// Select and open the left pane "Bookmarks Toolbar" folder.
|
||||
let PO = library.PlacesOrganizer;
|
||||
|
||||
PO.selectLeftPaneQuery('BookmarksToolbar');
|
||||
isnot(PO._places.selectedNode, null, "We have a valid selection");
|
||||
is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
|
||||
PlacesUtils.toolbarFolderId,
|
||||
"We have correctly selected bookmarks toolbar node.");
|
||||
|
||||
let searchBox = library.document.getElementById("searchFilter");
|
||||
searchBox.value = "example";
|
||||
library.PlacesSearchBox.search(searchBox.value);
|
||||
|
||||
let bookmarkNode = library.ContentTree.view.selectedNode;
|
||||
is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
|
||||
|
||||
// Check that both cut and delete commands are enabled.
|
||||
ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is enabled");
|
||||
ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is enabled");
|
||||
|
||||
library.close();
|
||||
});
|
||||
|
||||
add_task(function* test_tags() {
|
||||
let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||
url: "http://example.com/",
|
||||
title: "example page",
|
||||
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
index: 0 });
|
||||
PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/"), ["test"]);
|
||||
|
||||
let library = yield promiseLibrary();
|
||||
info("Ensure query contents can be cut or deleted");
|
||||
|
||||
// Select and open the left pane "Bookmarks Toolbar" folder.
|
||||
let PO = library.PlacesOrganizer;
|
||||
|
||||
PO.selectLeftPaneQuery('Tags');
|
||||
let tagsNode = PO._places.selectedNode;
|
||||
isnot(tagsNode, null, "We have a valid selection");
|
||||
let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
|
||||
is(tagsNode.title, tagsTitle,
|
||||
"Tags has been properly selected");
|
||||
|
||||
// Check that both cut and delete commands are disabled.
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is disabled");
|
||||
|
||||
// Now select the tag.
|
||||
PlacesUtils.asContainer(tagsNode).containerOpen = true;
|
||||
let tag = tagsNode.getChild(0);
|
||||
PO._places.selectNode(tag);
|
||||
is(PO._places.selectedNode.title, "test",
|
||||
"The created tag has been properly selected");
|
||||
|
||||
// Check that cut is disabled but delete is enabled.
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is enabled");
|
||||
|
||||
let bookmarkNode = library.ContentTree.view.selectedNode;
|
||||
is(bookmarkNode.uri, "http://example.com/", "Found the expected bookmark");
|
||||
|
||||
// Check that both cut and delete commands are enabled.
|
||||
ok(library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
|
||||
"Copy command is enabled");
|
||||
ok(!library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is enabled");
|
||||
|
||||
tagsNode.containerOpen = false;
|
||||
|
||||
library.close();
|
||||
});
|
|
@ -71,16 +71,24 @@ gTests.push({
|
|||
checkAddInfoFieldsCollapsed(PO);
|
||||
|
||||
// open recently bookmarked node
|
||||
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
|
||||
NetUtil.newURI("place:folder=BOOKMARKS_MENU" +
|
||||
"&folder=UNFILED_BOOKMARKS" +
|
||||
"&folder=TOOLBAR" +
|
||||
"&queryType=" + Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
|
||||
"&sort=" + Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
|
||||
"&maxResults=10" +
|
||||
"&excludeQueries=1"),
|
||||
0, "Recent Bookmarks");
|
||||
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
|
||||
NetUtil.newURI("http://mozilla.org/"),
|
||||
1, "Mozilla");
|
||||
var menuNode = PO._places.selectedNode.
|
||||
QueryInterface(Ci.nsINavHistoryContainerResultNode);
|
||||
menuNode.containerOpen = true;
|
||||
childNode = menuNode.getChild(0);
|
||||
isnot(childNode, null, "Bookmarks menu child node exists.");
|
||||
var recentlyBookmarkedTitle = PlacesUIUtils.
|
||||
getString("recentlyBookmarkedTitle");
|
||||
isnot(recentlyBookmarkedTitle, null,
|
||||
"Correctly got the recently bookmarked title locale string.");
|
||||
is(childNode.title, recentlyBookmarkedTitle,
|
||||
is(childNode.title, "Recent Bookmarks",
|
||||
"Correctly selected recently bookmarked node.");
|
||||
PO._places.selectNode(childNode);
|
||||
checkInfoBoxSelected(PO);
|
||||
|
@ -98,15 +106,6 @@ gTests.push({
|
|||
checkAddInfoFieldsNotCollapsed(PO);
|
||||
checkAddInfoFields(PO, "bookmark item");
|
||||
|
||||
// make sure additional fields are still hidden in second bookmark item
|
||||
ok(view.rowCount > 1, "Second bookmark item exists.");
|
||||
view.selection.select(1);
|
||||
checkInfoBoxSelected(PO);
|
||||
ok(!infoBoxExpanderWrapper.hidden,
|
||||
"Expander button is not hidden for second bookmark item.");
|
||||
checkAddInfoFieldsNotCollapsed(PO);
|
||||
checkAddInfoFields(PO, "second bookmark item");
|
||||
|
||||
menuNode.containerOpen = false;
|
||||
|
||||
waitForClearHistory(nextTest);
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim:set ts=2 sw=2 sts=2 et: */
|
||||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Test enabled commands in the left pane folder of the Library.
|
||||
*/
|
||||
|
||||
const TEST_URI = "http://www.mozilla.org/";
|
||||
|
||||
var gTests = [];
|
||||
var gLibrary;
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
gTests.push({
|
||||
desc: "Bug 489351 - Date containers under History in Library cannot be deleted/cut",
|
||||
run: function() {
|
||||
function addVisitsCallback() {
|
||||
// Select and open the left pane "History" query.
|
||||
var PO = gLibrary.PlacesOrganizer;
|
||||
PO.selectLeftPaneQuery('History');
|
||||
isnot(PO._places.selectedNode, null, "We correctly selected History");
|
||||
|
||||
// Check that both delete and cut commands are disabled.
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is disabled");
|
||||
var historyNode = PO._places.selectedNode
|
||||
.QueryInterface(Ci.nsINavHistoryContainerResultNode);
|
||||
historyNode.containerOpen = true;
|
||||
|
||||
// Check that we have a child container. It is "Today" container.
|
||||
is(historyNode.childCount, 1, "History node has one child");
|
||||
var todayNode = historyNode.getChild(0);
|
||||
var todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
|
||||
is(todayNode.title, todayNodeExpectedTitle,
|
||||
"History child is the expected container");
|
||||
|
||||
// Select "Today" container.
|
||||
PO._places.selectNode(todayNode);
|
||||
is(PO._places.selectedNode, todayNode,
|
||||
"We correctly selected Today container");
|
||||
// Check that delete command is enabled but cut command is disabled.
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is enabled");
|
||||
|
||||
// Execute the delete command and check visit has been removed.
|
||||
PO._places.controller.doCommand("cmd_delete");
|
||||
|
||||
// Test live update of "History" query.
|
||||
is(historyNode.childCount, 0, "History node has no more children");
|
||||
|
||||
historyNode.containerOpen = false;
|
||||
|
||||
let testURI = NetUtil.newURI(TEST_URI);
|
||||
PlacesUtils.asyncHistory.isURIVisited(testURI, function(aURI, aIsVisited) {
|
||||
ok(!aIsVisited, "Visit has been removed");
|
||||
nextTest();
|
||||
});
|
||||
}
|
||||
addVisits(
|
||||
{uri: NetUtil.newURI(TEST_URI), visitDate: Date.now() * 1000,
|
||||
transition: PlacesUtils.history.TRANSITION_TYPED},
|
||||
window,
|
||||
addVisitsCallback);
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
gTests.push({
|
||||
desc: "Bug 490156 - Can't delete smart bookmark containers",
|
||||
run: function() {
|
||||
// Select and open the left pane "Bookmarks Toolbar" folder.
|
||||
var PO = gLibrary.PlacesOrganizer;
|
||||
PO.selectLeftPaneQuery('BookmarksToolbar');
|
||||
isnot(PO._places.selectedNode, null, "We have a valid selection");
|
||||
is(PlacesUtils.getConcreteItemId(PO._places.selectedNode),
|
||||
PlacesUtils.toolbarFolderId,
|
||||
"We have correctly selected bookmarks toolbar node.");
|
||||
|
||||
// Check that both cut and delete commands are disabled.
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is disabled");
|
||||
ok(!PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is disabled");
|
||||
|
||||
var toolbarNode = PO._places.selectedNode
|
||||
.QueryInterface(Ci.nsINavHistoryContainerResultNode);
|
||||
toolbarNode.containerOpen = true;
|
||||
|
||||
// Add an History query to the toolbar.
|
||||
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
|
||||
NetUtil.newURI("place:sort=4"),
|
||||
0, // Insert at start.
|
||||
"special_query");
|
||||
// Get first child and check it is the "Most Visited" smart bookmark.
|
||||
ok(toolbarNode.childCount > 0, "Toolbar node has children");
|
||||
var queryNode = toolbarNode.getChild(0);
|
||||
is(queryNode.title, "special_query", "Query node is correctly selected");
|
||||
|
||||
// Select query node.
|
||||
PO._places.selectNode(queryNode);
|
||||
is(PO._places.selectedNode, queryNode, "We correctly selected query node");
|
||||
|
||||
// Check that both cut and delete commands are enabled.
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_cut"),
|
||||
"Cut command is enabled");
|
||||
ok(PO._places.controller.isCommandEnabled("cmd_delete"),
|
||||
"Delete command is enabled");
|
||||
|
||||
// Execute the delete command and check bookmark has been removed.
|
||||
PO._places.controller.doCommand("cmd_delete");
|
||||
try {
|
||||
PlacesUtils.bookmarks.getFolderIdForItem(queryNode.itemId);
|
||||
ok(false, "Unable to remove query node bookmark");
|
||||
} catch(ex) {
|
||||
ok(true, "Query node bookmark has been correctly removed");
|
||||
}
|
||||
|
||||
toolbarNode.containerOpen = false;
|
||||
nextTest();
|
||||
}
|
||||
});
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function nextTest() {
|
||||
if (gTests.length) {
|
||||
var test = gTests.shift();
|
||||
info("Start of test: " + test.desc);
|
||||
test.run();
|
||||
}
|
||||
else {
|
||||
// Close Library window.
|
||||
gLibrary.close();
|
||||
// No need to cleanup anything, we have a correct left pane now.
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
// Sanity checks.
|
||||
ok(PlacesUtils, "PlacesUtils is running in chrome context");
|
||||
ok(PlacesUIUtils, "PlacesUIUtils is running in chrome context");
|
||||
|
||||
// Open Library.
|
||||
gLibrary = openLibrary(nextTest);
|
||||
}
|
|
@ -43,7 +43,7 @@ function openLibrary(callback, aLeftPaneRoot) {
|
|||
function promiseLibrary(aLeftPaneRoot) {
|
||||
let deferred = Promise.defer();
|
||||
let library = Services.wm.getMostRecentWindow("Places:Organizer");
|
||||
if (library) {
|
||||
if (library && !library.closed) {
|
||||
if (aLeftPaneRoot)
|
||||
library.PlacesOrganizer.selectLeftPaneContainerByHierarchy(aLeftPaneRoot);
|
||||
deferred.resolve(library);
|
||||
|
@ -164,7 +164,7 @@ function addVisits(aPlaceInfo, aWindow, aCallback, aStack) {
|
|||
places[i].title = "test visit for " + places[i].uri.spec;
|
||||
}
|
||||
places[i].visits = [{
|
||||
transitionType: places[i].transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
|
||||
transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
|
||||
: places[i].transition,
|
||||
visitDate: places[i].visitDate || (now++) * 1000,
|
||||
referrerURI: places[i].referrer
|
||||
|
@ -203,3 +203,206 @@ function synthesizeClickOnSelectedTreeCell(aTree, aOptions) {
|
|||
EventUtils.synthesizeMouse(aTree.body, x, y, aOptions || {},
|
||||
aTree.ownerDocument.defaultView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously adds visits to a page.
|
||||
*
|
||||
* @param aPlaceInfo
|
||||
* Can be an nsIURI, in such a case a single LINK visit will be added.
|
||||
* Otherwise can be an object describing the visit to add, or an array
|
||||
* of these objects:
|
||||
* { uri: nsIURI of the page,
|
||||
* transition: one of the TRANSITION_* from nsINavHistoryService,
|
||||
* [optional] title: title of the page,
|
||||
* [optional] visitDate: visit date in microseconds from the epoch
|
||||
* [optional] referrer: nsIURI of the referrer for this visit
|
||||
* }
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When all visits have been added successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
function promiseAddVisits(aPlaceInfo)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let places = [];
|
||||
if (aPlaceInfo instanceof Ci.nsIURI) {
|
||||
places.push({ uri: aPlaceInfo });
|
||||
}
|
||||
else if (Array.isArray(aPlaceInfo)) {
|
||||
places = places.concat(aPlaceInfo);
|
||||
} else {
|
||||
places.push(aPlaceInfo)
|
||||
}
|
||||
|
||||
// Create mozIVisitInfo for each entry.
|
||||
let now = Date.now();
|
||||
for (let i = 0; i < places.length; i++) {
|
||||
if (!places[i].title) {
|
||||
places[i].title = "test visit for " + places[i].uri.spec;
|
||||
}
|
||||
places[i].visits = [{
|
||||
transitionType: places[i].transition === undefined ? PlacesUtils.history.TRANSITION_LINK
|
||||
: places[i].transition,
|
||||
visitDate: places[i].visitDate || (now++) * 1000,
|
||||
referrerURI: places[i].referrer
|
||||
}];
|
||||
}
|
||||
|
||||
PlacesUtils.asyncHistory.updatePlaces(
|
||||
places,
|
||||
{
|
||||
handleError: function AAV_handleError(aResultCode, aPlaceInfo) {
|
||||
let ex = new Components.Exception("Unexpected error in adding visits.",
|
||||
aResultCode);
|
||||
deferred.reject(ex);
|
||||
},
|
||||
handleResult: function () {},
|
||||
handleCompletion: function UP_handleCompletion() {
|
||||
deferred.resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously check a url is visited.
|
||||
*
|
||||
* @param aURI The URI.
|
||||
* @return {Promise}
|
||||
* @resolves When the check has been added successfully.
|
||||
* @rejects JavaScript exception.
|
||||
*/
|
||||
function promiseIsURIVisited(aURI) {
|
||||
let deferred = Promise.defer();
|
||||
|
||||
PlacesUtils.asyncHistory.isURIVisited(aURI, function(aURI, aIsVisited) {
|
||||
deferred.resolve(aIsVisited);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for all pending async statements on the default connection.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When all pending async statements finished.
|
||||
* @rejects Never.
|
||||
*
|
||||
* @note The result is achieved by asynchronously executing a query requiring
|
||||
* a write lock. Since all statements on the same connection are
|
||||
* serialized, the end of this write operation means that all writes are
|
||||
* complete. Note that WAL makes so that writers don't block readers, but
|
||||
* this is a problem only across different connections.
|
||||
*/
|
||||
function promiseAsyncUpdates()
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let db = DBConn();
|
||||
let begin = db.createAsyncStatement("BEGIN EXCLUSIVE");
|
||||
begin.executeAsync();
|
||||
begin.finalize();
|
||||
|
||||
let commit = db.createAsyncStatement("COMMIT");
|
||||
commit.executeAsync({
|
||||
handleResult: function () {},
|
||||
handleError: function () {},
|
||||
handleCompletion: function(aReason)
|
||||
{
|
||||
deferred.resolve();
|
||||
}
|
||||
});
|
||||
commit.finalize();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
function promiseBookmarksNotification(notification, conditionFn) {
|
||||
info(`Waiting for ${notification}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
let proxifiedObserver = new Proxy({}, {
|
||||
get: (target, name) => {
|
||||
if (name == "QueryInterface")
|
||||
return XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]);
|
||||
if (name == notification)
|
||||
return () => {
|
||||
if (conditionFn.apply(this, arguments)) {
|
||||
clearTimeout(timeout);
|
||||
PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
|
||||
executeSoon(resolve);
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
});
|
||||
PlacesUtils.bookmarks.addObserver(proxifiedObserver, false);
|
||||
let timeout = setTimeout(() => {
|
||||
PlacesUtils.bookmarks.removeObserver(proxifiedObserver, false);
|
||||
reject(new Error("Timed out while waiting for bookmarks notification"));
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function promiseHistoryNotification(notification, conditionFn) {
|
||||
info(`Waiting for ${notification}`);
|
||||
return new Promise((resolve, reject) => {
|
||||
let proxifiedObserver = new Proxy({}, {
|
||||
get: (target, name) => {
|
||||
if (name == "QueryInterface")
|
||||
return XPCOMUtils.generateQI([ Ci.nsINavHistoryObserver ]);
|
||||
if (name == notification)
|
||||
return () => {
|
||||
if (conditionFn.apply(this, arguments)) {
|
||||
clearTimeout(timeout);
|
||||
PlacesUtils.history.removeObserver(proxifiedObserver, false);
|
||||
executeSoon(resolve);
|
||||
}
|
||||
}
|
||||
return () => {};
|
||||
}
|
||||
});
|
||||
PlacesUtils.history.addObserver(proxifiedObserver, false);
|
||||
let timeout = setTimeout(() => {
|
||||
PlacesUtils.history.removeObserver(proxifiedObserver, false);
|
||||
reject(new Error("Timed out while waiting for history notification"));
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears history asynchronously.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves When history has been cleared.
|
||||
* @rejects Never.
|
||||
*/
|
||||
function promiseClearHistory() {
|
||||
let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
|
||||
PlacesUtils.bhistory.removeAllPages();
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows waiting for an observer notification once.
|
||||
*
|
||||
* @param topic
|
||||
* Notification topic to observe.
|
||||
*
|
||||
* @return {Promise}
|
||||
* @resolves The array [subject, data] from the observed notification.
|
||||
* @rejects Never.
|
||||
*/
|
||||
function promiseTopicObserved(topic)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
info("Waiting for observer topic " + topic);
|
||||
Services.obs.addObserver(function PTO_observe(subject, topic, data) {
|
||||
Services.obs.removeObserver(PTO_observe, topic);
|
||||
deferred.resolve([subject, data]);
|
||||
}, topic, false);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ function secondCall() {
|
|||
// This comment is useful: ☺
|
||||
eval("debugger;");
|
||||
function foo() {}
|
||||
if (true) {
|
||||
if (x) {
|
||||
foo();
|
||||
}
|
||||
}
|
||||
|
||||
var x = true;
|
||||
|
|
|
@ -24,7 +24,9 @@ support-files =
|
|||
[browser_graphs-07a.js]
|
||||
[browser_graphs-07b.js]
|
||||
[browser_graphs-08.js]
|
||||
[browser_graphs-09.js]
|
||||
[browser_graphs-09a.js]
|
||||
[browser_graphs-09b.js]
|
||||
[browser_graphs-09c.js]
|
||||
[browser_graphs-10a.js]
|
||||
[browser_graphs-10b.js]
|
||||
[browser_graphs-11a.js]
|
||||
|
|
|
@ -27,11 +27,27 @@ function* performTest() {
|
|||
}
|
||||
|
||||
function* testGraph(graph) {
|
||||
info("Should be able to set the grpah data before waiting for the ready event.");
|
||||
info("Should be able to set the graph data before waiting for the ready event.");
|
||||
|
||||
yield graph.setDataWhenReady(TEST_DATA);
|
||||
ok(graph.hasData(), "Data was set successfully.");
|
||||
|
||||
is(graph._gutter.hidden, false,
|
||||
"The gutter should not be hidden because the tooltips have arrows.");
|
||||
is(graph._maxTooltip.hidden, false,
|
||||
"The max tooltip should not be hidden.");
|
||||
is(graph._avgTooltip.hidden, false,
|
||||
"The avg tooltip should not be hidden.");
|
||||
is(graph._minTooltip.hidden, false,
|
||||
"The min tooltip should not be hidden.");
|
||||
|
||||
is(graph._maxTooltip.getAttribute("with-arrows"), "true",
|
||||
"The maximum tooltip has the correct 'with-arrows' attribute.");
|
||||
is(graph._avgTooltip.getAttribute("with-arrows"), "true",
|
||||
"The average tooltip has the correct 'with-arrows' attribute.");
|
||||
is(graph._minTooltip.getAttribute("with-arrows"), "true",
|
||||
"The minimum tooltip has the correct 'with-arrows' attribute.");
|
||||
|
||||
is(graph._maxTooltip.querySelector("[text=info]").textContent, "max",
|
||||
"The maximum tooltip displays the correct info.");
|
||||
is(graph._avgTooltip.querySelector("[text=info]").textContent, "avg",
|
||||
|
@ -41,7 +57,7 @@ function* testGraph(graph) {
|
|||
|
||||
is(graph._maxTooltip.querySelector("[text=value]").textContent, "60",
|
||||
"The maximum tooltip displays the correct value.");
|
||||
is(graph._avgTooltip.querySelector("[text=value]").textContent, "41",
|
||||
is(graph._avgTooltip.querySelector("[text=value]").textContent, "41.71",
|
||||
"The average tooltip displays the correct value.");
|
||||
is(graph._minTooltip.querySelector("[text=value]").textContent, "10",
|
||||
"The minimum tooltip displays the correct value.");
|
|
@ -0,0 +1,63 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that line graphs properly use the tooltips configuration properties.
|
||||
|
||||
const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }];
|
||||
let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
|
||||
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
|
||||
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
|
||||
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
let graph = new LineGraphWidget(doc.body, "fps");
|
||||
graph.withTooltipArrows = false;
|
||||
graph.withFixedTooltipPositions = true;
|
||||
|
||||
yield testGraph(graph);
|
||||
|
||||
graph.destroy();
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function* testGraph(graph) {
|
||||
yield graph.setDataWhenReady(TEST_DATA);
|
||||
|
||||
is(graph._gutter.hidden, true,
|
||||
"The gutter should be hidden because the tooltips don't have arrows.");
|
||||
is(graph._maxTooltip.hidden, false,
|
||||
"The max tooltip should not be hidden.");
|
||||
is(graph._avgTooltip.hidden, false,
|
||||
"The avg tooltip should not be hidden.");
|
||||
is(graph._minTooltip.hidden, false,
|
||||
"The min tooltip should not be hidden.");
|
||||
|
||||
is(graph._maxTooltip.getAttribute("with-arrows"), "false",
|
||||
"The maximum tooltip has the correct 'with-arrows' attribute.");
|
||||
is(graph._avgTooltip.getAttribute("with-arrows"), "false",
|
||||
"The average tooltip has the correct 'with-arrows' attribute.");
|
||||
is(graph._minTooltip.getAttribute("with-arrows"), "false",
|
||||
"The minimum tooltip has the correct 'with-arrows' attribute.");
|
||||
|
||||
is(parseInt(graph._maxTooltip.style.top), 8,
|
||||
"The maximum tooltip is positioned correctly.");
|
||||
is(parseInt(graph._avgTooltip.style.top), 8,
|
||||
"The average tooltip is positioned correctly.");
|
||||
is(parseInt(graph._minTooltip.style.top), 142,
|
||||
"The minimum tooltip is positioned correctly.");
|
||||
|
||||
is(parseInt(graph._maxGutterLine.style.top), 22,
|
||||
"The maximum gutter line is positioned correctly.");
|
||||
is(parseInt(graph._avgGutterLine.style.top), 61,
|
||||
"The average gutter line is positioned correctly.");
|
||||
is(parseInt(graph._minGutterLine.style.top), 128,
|
||||
"The minimum gutter line is positioned correctly.");
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Tests that line graphs hide the tooltips when there's no data available.
|
||||
|
||||
const TEST_DATA = [];
|
||||
let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {});
|
||||
let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {});
|
||||
let {Promise} = devtools.require("resource://gre/modules/Promise.jsm");
|
||||
let {Hosts} = devtools.require("devtools/framework/toolbox-hosts");
|
||||
|
||||
let test = Task.async(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
yield performTest();
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
let graph = new LineGraphWidget(doc.body, "fps");
|
||||
|
||||
yield testGraph(graph);
|
||||
|
||||
graph.destroy();
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function* testGraph(graph) {
|
||||
yield graph.setDataWhenReady(TEST_DATA);
|
||||
|
||||
is(graph._gutter.hidden, false,
|
||||
"The gutter should not be hidden.");
|
||||
is(graph._maxTooltip.hidden, true,
|
||||
"The max tooltip should be hidden.");
|
||||
is(graph._avgTooltip.hidden, true,
|
||||
"The avg tooltip should be hidden.");
|
||||
is(graph._minTooltip.hidden, true,
|
||||
"The min tooltip should be hidden.");
|
||||
}
|
|
@ -19,6 +19,7 @@ this.EXPORTED_SYMBOLS = [
|
|||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml";
|
||||
const L10N = new ViewHelpers.L10N();
|
||||
|
||||
// Generic constants.
|
||||
|
||||
|
@ -44,8 +45,9 @@ const GRAPH_STRIPE_PATTERN_LINE_SPACING = 4; // px
|
|||
|
||||
const LINE_GRAPH_DAMPEN_VALUES = 0.85;
|
||||
const LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS = 400; // 20 px
|
||||
const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 10; // px
|
||||
const LINE_GRAPH_TOOLTIP_SAFE_BOUNDS = 8; // px
|
||||
|
||||
const LINE_GRAPH_BACKGROUND_COLOR = "#0088cc";
|
||||
const LINE_GRAPH_STROKE_WIDTH = 1; // px
|
||||
const LINE_GRAPH_STROKE_COLOR = "rgba(255,255,255,0.9)";
|
||||
const LINE_GRAPH_HELPER_LINES_DASH = [5]; // px
|
||||
|
@ -487,7 +489,8 @@ AbstractCanvasGraph.prototype = {
|
|||
* @return boolean
|
||||
*/
|
||||
hasSelection: function() {
|
||||
return this._selection.start != null && this._selection.end != null;
|
||||
return this._selection &&
|
||||
this._selection.start != null && this._selection.end != null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -496,7 +499,8 @@ AbstractCanvasGraph.prototype = {
|
|||
* @return boolean
|
||||
*/
|
||||
hasSelectionInProgress: function() {
|
||||
return this._selection.start != null && this._selection.end == null;
|
||||
return this._selection &&
|
||||
this._selection.start != null && this._selection.end == null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -552,7 +556,7 @@ AbstractCanvasGraph.prototype = {
|
|||
* @return boolean
|
||||
*/
|
||||
hasCursor: function() {
|
||||
return this._cursor.x != null;
|
||||
return this._cursor && this._cursor.x != null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1176,6 +1180,14 @@ this.LineGraphWidget = function(parent, metric, ...args) {
|
|||
}
|
||||
|
||||
LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
||||
backgroundColor: LINE_GRAPH_BACKGROUND_COLOR,
|
||||
backgroundGradientStart: LINE_GRAPH_BACKGROUND_GRADIENT_START,
|
||||
backgroundGradientEnd: LINE_GRAPH_BACKGROUND_GRADIENT_END,
|
||||
strokeColor: LINE_GRAPH_STROKE_COLOR,
|
||||
strokeWidth: LINE_GRAPH_STROKE_WIDTH,
|
||||
maximumLineColor: LINE_GRAPH_MAXIMUM_LINE_COLOR,
|
||||
averageLineColor: LINE_GRAPH_AVERAGE_LINE_COLOR,
|
||||
minimumLineColor: LINE_GRAPH_MINIMUM_LINE_COLOR,
|
||||
clipheadLineColor: LINE_GRAPH_CLIPHEAD_LINE_COLOR,
|
||||
selectionLineColor: LINE_GRAPH_SELECTION_LINE_COLOR,
|
||||
selectionBackgroundColor: LINE_GRAPH_SELECTION_BACKGROUND_COLOR,
|
||||
|
@ -1188,12 +1200,29 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
*/
|
||||
dataOffsetX: 0,
|
||||
|
||||
/**
|
||||
* The scalar used to multiply the graph values to leave some headroom
|
||||
* on the top.
|
||||
*/
|
||||
dampenValuesFactor: LINE_GRAPH_DAMPEN_VALUES,
|
||||
|
||||
/**
|
||||
* Points that are too close too each other in the graph will not be rendered.
|
||||
* This scalar specifies the required minimum squared distance between points.
|
||||
*/
|
||||
minDistanceBetweenPoints: LINE_GRAPH_MIN_SQUARED_DISTANCE_BETWEEN_POINTS,
|
||||
|
||||
/**
|
||||
* Specifies if min/max/avg tooltips have arrow handlers on their sides.
|
||||
*/
|
||||
withTooltipArrows: true,
|
||||
|
||||
/**
|
||||
* Specifies if min/max/avg tooltips are positioned based on the actual
|
||||
* values, or just placed next to the graph corners.
|
||||
*/
|
||||
withFixedTooltipPositions: false,
|
||||
|
||||
/**
|
||||
* Renders the graph's data source.
|
||||
* @see AbstractCanvasGraph.prototype.buildGraphImage
|
||||
|
@ -1204,8 +1233,8 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
let height = this._height;
|
||||
|
||||
let totalTicks = this._data.length;
|
||||
let firstTick = this._data[0].delta;
|
||||
let lastTick = this._data[totalTicks - 1].delta;
|
||||
let firstTick = totalTicks ? this._data[0].delta : 0;
|
||||
let lastTick = totalTicks ? this._data[totalTicks - 1].delta : 0;
|
||||
let maxValue = Number.MIN_SAFE_INTEGER;
|
||||
let minValue = Number.MAX_SAFE_INTEGER;
|
||||
let sumValues = 0;
|
||||
|
@ -1217,7 +1246,7 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
}
|
||||
|
||||
let dataScaleX = this.dataScaleX = width / (lastTick - this.dataOffsetX);
|
||||
let dataScaleY = this.dataScaleY = height / maxValue * LINE_GRAPH_DAMPEN_VALUES;
|
||||
let dataScaleY = this.dataScaleY = height / maxValue * this.dampenValuesFactor;
|
||||
|
||||
/**
|
||||
* Calculates the squared distance between two 2D points.
|
||||
|
@ -1230,12 +1259,15 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
|
||||
// Draw the graph.
|
||||
|
||||
ctx.fillStyle = this.backgroundColor;
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
let gradient = ctx.createLinearGradient(0, height / 2, 0, height);
|
||||
gradient.addColorStop(0, LINE_GRAPH_BACKGROUND_GRADIENT_START);
|
||||
gradient.addColorStop(1, LINE_GRAPH_BACKGROUND_GRADIENT_END);
|
||||
gradient.addColorStop(0, this.backgroundGradientStart);
|
||||
gradient.addColorStop(1, this.backgroundGradientEnd);
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.strokeStyle = LINE_GRAPH_STROKE_COLOR;
|
||||
ctx.lineWidth = LINE_GRAPH_STROKE_WIDTH * this._pixelRatio;
|
||||
ctx.strokeStyle = this.strokeColor;
|
||||
ctx.lineWidth = this.strokeWidth * this._pixelRatio;
|
||||
ctx.beginPath();
|
||||
|
||||
let prevX = 0;
|
||||
|
@ -1268,43 +1300,46 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
|
||||
// Draw the maximum value horizontal line.
|
||||
|
||||
ctx.strokeStyle = LINE_GRAPH_MAXIMUM_LINE_COLOR;
|
||||
ctx.strokeStyle = this.maximumLineColor;
|
||||
ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
|
||||
ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
|
||||
ctx.beginPath();
|
||||
let maximumY = height - maxValue * dataScaleY - ctx.lineWidth;
|
||||
let maximumY = height - maxValue * dataScaleY;
|
||||
ctx.moveTo(0, maximumY);
|
||||
ctx.lineTo(width, maximumY);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw the average value horizontal line.
|
||||
|
||||
ctx.strokeStyle = LINE_GRAPH_AVERAGE_LINE_COLOR;
|
||||
ctx.strokeStyle = this.averageLineColor;
|
||||
ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
|
||||
ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
|
||||
ctx.beginPath();
|
||||
let avgValue = sumValues / totalTicks;
|
||||
let averageY = height - avgValue * dataScaleY - ctx.lineWidth;
|
||||
let avgValue = totalTicks ? sumValues / totalTicks : 0;
|
||||
let averageY = height - avgValue * dataScaleY;
|
||||
ctx.moveTo(0, averageY);
|
||||
ctx.lineTo(width, averageY);
|
||||
ctx.stroke();
|
||||
|
||||
// Draw the minimum value horizontal line.
|
||||
|
||||
ctx.strokeStyle = LINE_GRAPH_MINIMUM_LINE_COLOR;
|
||||
ctx.strokeStyle = this.minimumLineColor;
|
||||
ctx.lineWidth = LINE_GRAPH_HELPER_LINES_WIDTH;
|
||||
ctx.setLineDash(LINE_GRAPH_HELPER_LINES_DASH);
|
||||
ctx.beginPath();
|
||||
let minimumY = height - minValue * dataScaleY - ctx.lineWidth;
|
||||
let minimumY = height - minValue * dataScaleY;
|
||||
ctx.moveTo(0, minimumY);
|
||||
ctx.lineTo(width, minimumY);
|
||||
ctx.stroke();
|
||||
|
||||
// Update the tooltips text and gutter lines.
|
||||
|
||||
this._maxTooltip.querySelector("[text=value]").textContent = maxValue|0;
|
||||
this._avgTooltip.querySelector("[text=value]").textContent = avgValue|0;
|
||||
this._minTooltip.querySelector("[text=value]").textContent = minValue|0;
|
||||
this._maxTooltip.querySelector("[text=value]").textContent =
|
||||
L10N.numberWithDecimals(maxValue, 2);
|
||||
this._avgTooltip.querySelector("[text=value]").textContent =
|
||||
L10N.numberWithDecimals(avgValue, 2);
|
||||
this._minTooltip.querySelector("[text=value]").textContent =
|
||||
L10N.numberWithDecimals(minValue, 2);
|
||||
|
||||
/**
|
||||
* Constrains a value to a range.
|
||||
|
@ -1316,19 +1351,32 @@ LineGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
}
|
||||
|
||||
let bottom = height / this._pixelRatio;
|
||||
let maxPosY = map(maxValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
|
||||
let avgPosY = map(avgValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
|
||||
let minPosY = map(minValue * LINE_GRAPH_DAMPEN_VALUES, 0, maxValue, bottom, 0);
|
||||
let maxPosY = map(maxValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
|
||||
let avgPosY = map(avgValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
|
||||
let minPosY = map(minValue * this.dampenValuesFactor, 0, maxValue, bottom, 0);
|
||||
|
||||
let safeTop = LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
|
||||
let safeBottom = bottom - LINE_GRAPH_TOOLTIP_SAFE_BOUNDS;
|
||||
|
||||
this._maxTooltip.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
|
||||
this._avgTooltip.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
|
||||
this._minTooltip.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
|
||||
this._maxGutterLine.style.top = clamp(maxPosY, safeTop, safeBottom) + "px";
|
||||
this._avgGutterLine.style.top = clamp(avgPosY, safeTop, safeBottom) + "px";
|
||||
this._minGutterLine.style.top = clamp(minPosY, safeTop, safeBottom) + "px";
|
||||
this._maxTooltip.style.top = (this.withFixedTooltipPositions
|
||||
? safeTop : clamp(maxPosY, safeTop, safeBottom)) + "px";
|
||||
this._avgTooltip.style.top = (this.withFixedTooltipPositions
|
||||
? safeTop : clamp(avgPosY, safeTop, safeBottom)) + "px";
|
||||
this._minTooltip.style.top = (this.withFixedTooltipPositions
|
||||
? safeBottom : clamp(minPosY, safeTop, safeBottom)) + "px";
|
||||
|
||||
this._maxGutterLine.style.top = maxPosY + "px";
|
||||
this._avgGutterLine.style.top = avgPosY + "px";
|
||||
this._minGutterLine.style.top = minPosY + "px";
|
||||
|
||||
this._maxTooltip.setAttribute("with-arrows", this.withTooltipArrows);
|
||||
this._avgTooltip.setAttribute("with-arrows", this.withTooltipArrows);
|
||||
this._minTooltip.setAttribute("with-arrows", this.withTooltipArrows);
|
||||
|
||||
this._gutter.hidden = !this.withTooltipArrows;
|
||||
this._maxTooltip.hidden = !totalTicks;
|
||||
this._avgTooltip.hidden = !totalTicks;
|
||||
this._minTooltip.hidden = !totalTicks;
|
||||
|
||||
return canvas;
|
||||
},
|
||||
|
@ -1466,6 +1514,12 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
*/
|
||||
dataOffsetX: 0,
|
||||
|
||||
/**
|
||||
* The scalar used to multiply the graph values to leave some headroom
|
||||
* on the top.
|
||||
*/
|
||||
dampenValuesFactor: BAR_GRAPH_DAMPEN_VALUES,
|
||||
|
||||
/**
|
||||
* Bars that are too close too each other in the graph will be combined.
|
||||
* This scalar specifies the required minimum width of each bar.
|
||||
|
@ -1520,7 +1574,7 @@ BarGraphWidget.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
data: this._data,
|
||||
dataScaleX: dataScaleX,
|
||||
minBarsWidth: minBarsWidth
|
||||
}) * BAR_GRAPH_DAMPEN_VALUES;
|
||||
}) * this.dampenValuesFactor;
|
||||
|
||||
// Draw the graph.
|
||||
|
||||
|
@ -1923,6 +1977,13 @@ this.CanvasGraphUtils = {
|
|||
if (!graph1 || !graph2) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (graph1.hasSelection()) {
|
||||
graph2.setSelection(graph1.getSelection());
|
||||
} else {
|
||||
graph2.dropSelection();
|
||||
}
|
||||
|
||||
graph1.on("selecting", () => {
|
||||
graph2.setSelection(graph1.getSelection());
|
||||
});
|
||||
|
|
|
@ -6,7 +6,8 @@
|
|||
EXTRA_JS_MODULES.devtools.timeline += [
|
||||
'panel.js',
|
||||
'widgets/global.js',
|
||||
'widgets/overview.js',
|
||||
'widgets/markers-overview.js',
|
||||
'widgets/memory-overview.js',
|
||||
'widgets/waterfall.js'
|
||||
]
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ support-files =
|
|||
[browser_timeline_overview-initial-selection-02.js]
|
||||
[browser_timeline_overview-update.js]
|
||||
[browser_timeline_panels.js]
|
||||
[browser_timeline_recording-without-memory.js]
|
||||
[browser_timeline_recording.js]
|
||||
[browser_timeline_waterfall-background.js]
|
||||
[browser_timeline_waterfall-generic.js]
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
|
||||
let test = Task.async(function*() {
|
||||
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
|
||||
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
|
||||
let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
|
||||
let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
|
||||
|
||||
$("#memory-checkbox").checked = true;
|
||||
yield TimelineController.updateMemoryRecording();
|
||||
|
||||
yield TimelineController.toggleRecording();
|
||||
ok(true, "Recording has started.");
|
||||
|
||||
|
@ -21,19 +24,22 @@ let test = Task.async(function*() {
|
|||
"The overview graph was updated a bunch of times.");
|
||||
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
|
||||
"There are some markers available.");
|
||||
ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
|
||||
"There are some memory measurements available now.");
|
||||
|
||||
yield TimelineController.toggleRecording();
|
||||
ok(true, "Recording has ended.");
|
||||
|
||||
let interval = TimelineController.getInterval();
|
||||
let markers = TimelineController.getMarkers();
|
||||
let selection = TimelineView.overview.getSelection();
|
||||
let selection = TimelineView.markersOverview.getSelection();
|
||||
|
||||
is((selection.start) | 0,
|
||||
((markers[0].start - markers.startTime) * TimelineView.overview.dataScaleX) | 0,
|
||||
((markers[0].start - interval.startTime) * TimelineView.markersOverview.dataScaleX) | 0,
|
||||
"The initial selection start is correct.");
|
||||
|
||||
is((selection.end - selection.start) | 0,
|
||||
(selectionRatio * TimelineView.overview.width) | 0,
|
||||
(selectionRatio * TimelineView.markersOverview.width) | 0,
|
||||
"The initial selection end is correct.");
|
||||
|
||||
yield teardown(panel);
|
||||
|
|
|
@ -8,9 +8,12 @@
|
|||
|
||||
let test = Task.async(function*() {
|
||||
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
|
||||
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
|
||||
let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
|
||||
let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin;
|
||||
|
||||
$("#memory-checkbox").checked = true;
|
||||
yield TimelineController.updateMemoryRecording();
|
||||
|
||||
yield TimelineController.toggleRecording();
|
||||
ok(true, "Recording has started.");
|
||||
|
||||
|
@ -18,10 +21,13 @@ let test = Task.async(function*() {
|
|||
ok(true, "Recording has ended.");
|
||||
|
||||
let markers = TimelineController.getMarkers();
|
||||
let selection = TimelineView.overview.getSelection();
|
||||
let memory = TimelineController.getMemory();
|
||||
let selection = TimelineView.markersOverview.getSelection();
|
||||
|
||||
is(markers.length, 0,
|
||||
"There are no markers available.");
|
||||
is(memory.length, 0,
|
||||
"There are no memory measurements available.");
|
||||
is(selection.start, null,
|
||||
"The initial selection start is correct.");
|
||||
is(selection.end, null,
|
||||
|
|
|
@ -2,46 +2,72 @@
|
|||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests if the overview graph is continuously updated.
|
||||
* Tests if the markers and memory overviews are continuously updated.
|
||||
*/
|
||||
|
||||
let test = Task.async(function*() {
|
||||
let { target, panel } = yield initTimelinePanel("about:blank");
|
||||
let { EVENTS, TimelineView, TimelineController } = panel.panelWin;
|
||||
let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
|
||||
|
||||
$("#memory-checkbox").checked = true;
|
||||
yield TimelineController.updateMemoryRecording();
|
||||
|
||||
yield DevToolsUtils.waitForTime(1000);
|
||||
yield TimelineController.toggleRecording();
|
||||
ok(true, "Recording has started.");
|
||||
|
||||
ok("selectionEnabled" in TimelineView.overview,
|
||||
"The selection should not be enabled for the overview graph (1).");
|
||||
is(TimelineView.overview.selectionEnabled, false,
|
||||
"The selection should not be enabled for the overview graph (2).");
|
||||
is(TimelineView.overview.hasSelection(), false,
|
||||
"The overview graph shouldn't have a selection before recording.");
|
||||
ok("selectionEnabled" in TimelineView.markersOverview,
|
||||
"The selection should not be enabled for the markers overview (1).");
|
||||
is(TimelineView.markersOverview.selectionEnabled, false,
|
||||
"The selection should not be enabled for the markers overview (2).");
|
||||
is(TimelineView.markersOverview.hasSelection(), false,
|
||||
"The markers overview shouldn't have a selection before recording.");
|
||||
|
||||
ok("selectionEnabled" in TimelineView.memoryOverview,
|
||||
"The selection should not be enabled for the memory overview (1).");
|
||||
is(TimelineView.memoryOverview.selectionEnabled, false,
|
||||
"The selection should not be enabled for the memory overview (2).");
|
||||
is(TimelineView.memoryOverview.hasSelection(), false,
|
||||
"The memory overview shouldn't have a selection before recording.");
|
||||
|
||||
let updated = 0;
|
||||
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
|
||||
|
||||
ok((yield waitUntil(() => updated > 10)),
|
||||
"The overview graph was updated a bunch of times.");
|
||||
"The overviews were updated a bunch of times.");
|
||||
ok((yield waitUntil(() => TimelineController.getMemory().length > 10)),
|
||||
"There are some memory measurements available now.");
|
||||
|
||||
ok("selectionEnabled" in TimelineView.overview,
|
||||
"The selection should still not be enabled for the overview graph (3).");
|
||||
is(TimelineView.overview.selectionEnabled, false,
|
||||
"The selection should still not be enabled for the overview graph (4).");
|
||||
is(TimelineView.overview.hasSelection(), false,
|
||||
"The overview graph should not have a selection while recording.");
|
||||
ok("selectionEnabled" in TimelineView.markersOverview,
|
||||
"The selection should still not be enabled for the markers overview (3).");
|
||||
is(TimelineView.markersOverview.selectionEnabled, false,
|
||||
"The selection should still not be enabled for the markers overview (4).");
|
||||
is(TimelineView.markersOverview.hasSelection(), false,
|
||||
"The markers overview should not have a selection while recording.");
|
||||
|
||||
ok("selectionEnabled" in TimelineView.memoryOverview,
|
||||
"The selection should still not be enabled for the memory overview (3).");
|
||||
is(TimelineView.memoryOverview.selectionEnabled, false,
|
||||
"The selection should still not be enabled for the memory overview (4).");
|
||||
is(TimelineView.memoryOverview.hasSelection(), false,
|
||||
"The memory overview should not have a selection while recording.");
|
||||
|
||||
yield TimelineController.toggleRecording();
|
||||
ok(true, "Recording has ended.");
|
||||
|
||||
is(TimelineController.getMarkers().length, 0,
|
||||
"There are no markers available.");
|
||||
is(TimelineView.overview.selectionEnabled, true,
|
||||
"The selection should now be enabled for the overview graph.");
|
||||
is(TimelineView.overview.hasSelection(), false,
|
||||
"The overview graph should not have a selection after recording.");
|
||||
isnot(TimelineController.getMemory().length, 0,
|
||||
"There are some memory measurements available.");
|
||||
|
||||
is(TimelineView.markersOverview.selectionEnabled, true,
|
||||
"The selection should now be enabled for the markers overview.");
|
||||
is(TimelineView.markersOverview.hasSelection(), false,
|
||||
"The markers overview should not have a selection after recording.");
|
||||
|
||||
is(TimelineView.memoryOverview.selectionEnabled, true,
|
||||
"The selection should now be enabled for the memory overview.");
|
||||
is(TimelineView.memoryOverview.hasSelection(), false,
|
||||
"The memory overview should not have a selection after recording.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
/**
|
||||
* Tests if the timeline actor isn't unnecessarily asked to record memory.
|
||||
*/
|
||||
|
||||
let test = Task.async(function*() {
|
||||
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
|
||||
let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin;
|
||||
|
||||
yield TimelineController.toggleRecording();
|
||||
ok(true, "Recording has started.");
|
||||
|
||||
let updated = 0;
|
||||
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
|
||||
|
||||
ok((yield waitUntil(() => updated > 10)),
|
||||
"The overview graph was updated a bunch of times.");
|
||||
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
|
||||
"There are some markers available.");
|
||||
|
||||
yield TimelineController.toggleRecording();
|
||||
ok(true, "Recording has ended.");
|
||||
|
||||
let markers = TimelineController.getMarkers();
|
||||
let memory = TimelineController.getMemory();
|
||||
|
||||
isnot(markers.length, 0,
|
||||
"There are some markers available.");
|
||||
is(memory.length, 0,
|
||||
"There are no memory measurements available.");
|
||||
|
||||
yield teardown(panel);
|
||||
finish();
|
||||
});
|
|
@ -7,7 +7,10 @@
|
|||
|
||||
let test = Task.async(function*() {
|
||||
let { target, panel } = yield initTimelinePanel(SIMPLE_URL);
|
||||
let { gFront, TimelineController } = panel.panelWin;
|
||||
let { $, gFront, TimelineController } = panel.panelWin;
|
||||
|
||||
$("#memory-checkbox").checked = true;
|
||||
yield TimelineController.updateMemoryRecording();
|
||||
|
||||
is((yield gFront.isRecording()), false,
|
||||
"The timeline actor should not be recording when the tool starts.");
|
||||
|
@ -20,13 +23,16 @@ let test = Task.async(function*() {
|
|||
"The timeline actor should be recording now.");
|
||||
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
|
||||
"There are some markers available now.");
|
||||
ok((yield waitUntil(() => TimelineController.getMemory().length > 0)),
|
||||
"There are some memory measurements available now.");
|
||||
|
||||
ok("startTime" in TimelineController.getMarkers(),
|
||||
"A `startTime` field was set on the markers array.");
|
||||
ok("endTime" in TimelineController.getMarkers(),
|
||||
"An `endTime` field was set on the markers array.");
|
||||
ok(TimelineController.getMarkers().endTime >
|
||||
TimelineController.getMarkers().startTime,
|
||||
ok("startTime" in TimelineController.getInterval(),
|
||||
"A `startTime` field was set on the recording data.");
|
||||
ok("endTime" in TimelineController.getInterval(),
|
||||
"An `endTime` field was set on the recording data.");
|
||||
|
||||
ok(TimelineController.getInterval().endTime >
|
||||
TimelineController.getInterval().startTime,
|
||||
"Some time has passed since the recording started.");
|
||||
|
||||
yield teardown(panel);
|
||||
|
|
|
@ -17,7 +17,7 @@ let test = Task.async(function*() {
|
|||
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
|
||||
|
||||
ok((yield waitUntil(() => updated > 0)),
|
||||
"The overview graph was updated a bunch of times.");
|
||||
"The overview graphs were updated a bunch of times.");
|
||||
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
|
||||
"There are some markers available.");
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ let test = Task.async(function*() {
|
|||
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
|
||||
|
||||
ok((yield waitUntil(() => updated > 0)),
|
||||
"The overview graph was updated a bunch of times.");
|
||||
"The overview graphs were updated a bunch of times.");
|
||||
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
|
||||
"There are some markers available.");
|
||||
|
||||
|
@ -25,42 +25,42 @@ let test = Task.async(function*() {
|
|||
|
||||
// Test the header container.
|
||||
|
||||
ok($(".timeline-header-container"),
|
||||
ok($(".waterfall-header-container"),
|
||||
"A header container should have been created.");
|
||||
|
||||
// Test the header sidebar (left).
|
||||
|
||||
ok($(".timeline-header-sidebar"),
|
||||
ok($(".waterfall-header-container > .waterfall-sidebar"),
|
||||
"A header sidebar node should have been created.");
|
||||
ok($(".timeline-header-sidebar > .timeline-header-name"),
|
||||
ok($(".waterfall-header-container > .waterfall-sidebar > .waterfall-header-name"),
|
||||
"A header name label should have been created inside the sidebar.");
|
||||
|
||||
// Test the header ticks (right).
|
||||
|
||||
ok($(".timeline-header-ticks"),
|
||||
ok($(".waterfall-header-ticks"),
|
||||
"A header ticks node should have been created.");
|
||||
ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0,
|
||||
ok($$(".waterfall-header-ticks > .waterfall-header-tick").length > 0,
|
||||
"Some header tick labels should have been created inside the tick node.");
|
||||
|
||||
// Test the markers container.
|
||||
|
||||
ok($(".timeline-marker-container"),
|
||||
ok($(".waterfall-marker-container"),
|
||||
"A marker container should have been created.");
|
||||
|
||||
// Test the markers sidebar (left).
|
||||
|
||||
ok($$(".timeline-marker-sidebar").length,
|
||||
ok($$(".waterfall-marker-container > .waterfall-sidebar").length,
|
||||
"Some marker sidebar nodes should have been created.");
|
||||
ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-bullet").length,
|
||||
ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-bullet").length,
|
||||
"Some marker color bullets should have been created inside the sidebar.");
|
||||
ok($$(".timeline-marker-sidebar:not(spacer) > .timeline-marker-name").length,
|
||||
ok($$(".waterfall-marker-container > .waterfall-sidebar:not(spacer) > .waterfall-marker-name").length,
|
||||
"Some marker name labels should have been created inside the sidebar.");
|
||||
|
||||
// Test the markers waterfall (right).
|
||||
|
||||
ok($$(".timeline-marker-waterfall").length,
|
||||
ok($$(".waterfall-marker-item").length,
|
||||
"Some marker waterfall nodes should have been created.");
|
||||
ok($$(".timeline-marker-waterfall:not(spacer) > .timeline-marker-bar").length,
|
||||
ok($$(".waterfall-marker-item:not(spacer) > .waterfall-marker-bar").length,
|
||||
"Some marker color bars should have been created inside the waterfall.");
|
||||
|
||||
yield teardown(panel);
|
||||
|
|
|
@ -27,7 +27,7 @@ let test = Task.async(function*() {
|
|||
panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++);
|
||||
|
||||
ok((yield waitUntil(() => updated > 0)),
|
||||
"The overview graph was updated a bunch of times.");
|
||||
"The overview graphs were updated a bunch of times.");
|
||||
ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)),
|
||||
"There are some markers available.");
|
||||
|
||||
|
|
|
@ -12,11 +12,16 @@ devtools.lazyRequireGetter(this, "promise");
|
|||
devtools.lazyRequireGetter(this, "EventEmitter",
|
||||
"devtools/toolkit/event-emitter");
|
||||
|
||||
devtools.lazyRequireGetter(this, "Overview",
|
||||
"devtools/timeline/overview", true);
|
||||
devtools.lazyRequireGetter(this, "MarkersOverview",
|
||||
"devtools/timeline/markers-overview", true);
|
||||
devtools.lazyRequireGetter(this, "MemoryOverview",
|
||||
"devtools/timeline/memory-overview", true);
|
||||
devtools.lazyRequireGetter(this, "Waterfall",
|
||||
"devtools/timeline/waterfall", true);
|
||||
|
||||
devtools.lazyImporter(this, "CanvasGraphUtils",
|
||||
"resource:///modules/devtools/Graphs.jsm");
|
||||
|
||||
devtools.lazyImporter(this, "PluralForm",
|
||||
"resource://gre/modules/PluralForm.jsm");
|
||||
|
||||
|
@ -29,7 +34,7 @@ const EVENTS = {
|
|||
RECORDING_STARTED: "Timeline:RecordingStarted",
|
||||
RECORDING_ENDED: "Timeline:RecordingEnded",
|
||||
|
||||
// When the overview graph is populated with new markers.
|
||||
// When the overview graphs are populated with new markers.
|
||||
OVERVIEW_UPDATED: "Timeline:OverviewUpdated",
|
||||
|
||||
// When the waterfall view is populated with new markers.
|
||||
|
@ -63,9 +68,13 @@ let shutdownTimeline = Task.async(function*() {
|
|||
*/
|
||||
let TimelineController = {
|
||||
/**
|
||||
* Permanent storage for the markers streamed by the backend.
|
||||
* Permanent storage for the markers and the memory measurements streamed by
|
||||
* the backend, along with the start and end timestamps.
|
||||
*/
|
||||
_starTime: 0,
|
||||
_endTime: 0,
|
||||
_markers: [],
|
||||
_memory: [],
|
||||
|
||||
/**
|
||||
* Initialization function, called when the tool is started.
|
||||
|
@ -73,7 +82,9 @@ let TimelineController = {
|
|||
initialize: function() {
|
||||
this._onRecordingTick = this._onRecordingTick.bind(this);
|
||||
this._onMarkers = this._onMarkers.bind(this);
|
||||
this._onMemory = this._onMemory.bind(this);
|
||||
gFront.on("markers", this._onMarkers);
|
||||
gFront.on("memory", this._onMemory);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -81,16 +92,44 @@ let TimelineController = {
|
|||
*/
|
||||
destroy: function() {
|
||||
gFront.off("markers", this._onMarkers);
|
||||
gFront.off("memory", this._onMemory);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the { stat, end } time interval for this recording.
|
||||
* @return object
|
||||
*/
|
||||
getInterval: function() {
|
||||
return { startTime: this._startTime, endTime: this._endTime };
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the accumulated markers in this recording.
|
||||
* @return array.
|
||||
* @return array
|
||||
*/
|
||||
getMarkers: function() {
|
||||
return this._markers;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the accumulated memory measurements in this recording.
|
||||
* @return array
|
||||
*/
|
||||
getMemory: function() {
|
||||
return this._memory;
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the views to show or hide the memory recording data.
|
||||
*/
|
||||
updateMemoryRecording: Task.async(function*() {
|
||||
if ($("#memory-checkbox").checked) {
|
||||
yield TimelineView.showMemoryOverview();
|
||||
} else {
|
||||
yield TimelineView.hideMemoryOverview();
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Starts/stops the timeline recording and streaming.
|
||||
*/
|
||||
|
@ -108,17 +147,20 @@ let TimelineController = {
|
|||
*/
|
||||
_startRecording: function*() {
|
||||
TimelineView.handleRecordingStarted();
|
||||
let startTime = yield gFront.start();
|
||||
|
||||
let withMemory = $("#memory-checkbox").checked;
|
||||
let startTime = yield gFront.start({ withMemory });
|
||||
|
||||
// Times must come from the actor in order to be self-consistent.
|
||||
// However, we also want to update the view with the elapsed time
|
||||
// even when the actor is not generating data. To do this we get
|
||||
// the local time and use it to compute a reasonable elapsed time.
|
||||
// See _onRecordingTick.
|
||||
this._localStartTime = performance.now();
|
||||
|
||||
this._startTime = startTime;
|
||||
this._endTime = startTime;
|
||||
this._markers = [];
|
||||
this._markers.startTime = startTime;
|
||||
this._markers.endTime = startTime;
|
||||
this._memory = [];
|
||||
this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL);
|
||||
},
|
||||
|
||||
|
@ -131,7 +173,7 @@ let TimelineController = {
|
|||
// Sorting markers is only important when displayed in the waterfall.
|
||||
this._markers = this._markers.sort((a,b) => (a.start > b.start));
|
||||
|
||||
TimelineView.handleMarkersUpdate(this._markers);
|
||||
TimelineView.handleRecordingUpdate();
|
||||
TimelineView.handleRecordingEnded();
|
||||
yield gFront.stop();
|
||||
},
|
||||
|
@ -143,6 +185,7 @@ let TimelineController = {
|
|||
_stopRecordingAndDiscardData: function*() {
|
||||
yield this._stopRecording();
|
||||
this._markers.length = 0;
|
||||
this._memory.length = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -156,22 +199,34 @@ let TimelineController = {
|
|||
*/
|
||||
_onMarkers: function(markers, endTime) {
|
||||
Array.prototype.push.apply(this._markers, markers);
|
||||
this._markers.endTime = endTime;
|
||||
this._endTime = endTime;
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback handling the "memory" event on the timeline front.
|
||||
*
|
||||
* @param number delta
|
||||
* The number of milliseconds elapsed since epoch.
|
||||
* @param object measurement
|
||||
* A detailed breakdown of the current memory usage.
|
||||
*/
|
||||
_onMemory: function(delta, measurement) {
|
||||
this._memory.push({ delta, value: measurement.total / 1024 / 1024 });
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback invoked at a fixed interval while recording.
|
||||
* Updates the markers store with the current time and the timeline overview.
|
||||
* Updates the current time and the timeline overview.
|
||||
*/
|
||||
_onRecordingTick: function() {
|
||||
// Compute an approximate ending time for the view. This is
|
||||
// needed to ensure that the view updates even when new data is
|
||||
// not being generated.
|
||||
let fakeTime = this._markers.startTime + (performance.now() - this._localStartTime);
|
||||
if (fakeTime > this._markers.endTime) {
|
||||
this._markers.endTime = fakeTime;
|
||||
let fakeTime = this._startTime + (performance.now() - this._localStartTime);
|
||||
if (fakeTime > this._endTime) {
|
||||
this._endTime = fakeTime;
|
||||
}
|
||||
TimelineView.handleMarkersUpdate(this._markers);
|
||||
TimelineView.handleRecordingUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -183,15 +238,15 @@ let TimelineView = {
|
|||
* Initialization function, called when the tool is started.
|
||||
*/
|
||||
initialize: Task.async(function*() {
|
||||
this.overview = new Overview($("#timeline-overview"));
|
||||
this.markersOverview = new MarkersOverview($("#markers-overview"));
|
||||
this.waterfall = new Waterfall($("#timeline-waterfall"));
|
||||
|
||||
this._onSelecting = this._onSelecting.bind(this);
|
||||
this._onRefresh = this._onRefresh.bind(this);
|
||||
this.overview.on("selecting", this._onSelecting);
|
||||
this.overview.on("refresh", this._onRefresh);
|
||||
this.markersOverview.on("selecting", this._onSelecting);
|
||||
this.markersOverview.on("refresh", this._onRefresh);
|
||||
|
||||
yield this.overview.ready();
|
||||
yield this.markersOverview.ready();
|
||||
yield this.waterfall.recalculateBounds();
|
||||
}),
|
||||
|
||||
|
@ -199,9 +254,40 @@ let TimelineView = {
|
|||
* Destruction function, called when the tool is closed.
|
||||
*/
|
||||
destroy: function() {
|
||||
this.overview.off("selecting", this._onSelecting);
|
||||
this.overview.off("refresh", this._onRefresh);
|
||||
this.overview.destroy();
|
||||
this.markersOverview.off("selecting", this._onSelecting);
|
||||
this.markersOverview.off("refresh", this._onRefresh);
|
||||
this.markersOverview.destroy();
|
||||
|
||||
// The memory overview graph is not always available.
|
||||
if (this.memoryOverview) {
|
||||
this.memoryOverview.destroy();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shows the memory overview graph.
|
||||
*/
|
||||
showMemoryOverview: Task.async(function*() {
|
||||
this.memoryOverview = new MemoryOverview($("#memory-overview"));
|
||||
yield this.memoryOverview.ready();
|
||||
|
||||
let interval = TimelineController.getInterval();
|
||||
let memory = TimelineController.getMemory();
|
||||
this.memoryOverview.setData({ interval, memory });
|
||||
|
||||
CanvasGraphUtils.linkAnimation(this.markersOverview, this.memoryOverview);
|
||||
CanvasGraphUtils.linkSelection(this.markersOverview, this.memoryOverview);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Hides the memory overview graph.
|
||||
*/
|
||||
hideMemoryOverview: function() {
|
||||
if (!this.memoryOverview) {
|
||||
return;
|
||||
}
|
||||
this.memoryOverview.destroy();
|
||||
this.memoryOverview = null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -210,11 +296,16 @@ let TimelineView = {
|
|||
*/
|
||||
handleRecordingStarted: function() {
|
||||
$("#record-button").setAttribute("checked", "true");
|
||||
$("#memory-checkbox").setAttribute("disabled", "true");
|
||||
$("#timeline-pane").selectedPanel = $("#recording-notice");
|
||||
|
||||
this.overview.selectionEnabled = false;
|
||||
this.overview.dropSelection();
|
||||
this.overview.setData([]);
|
||||
this.markersOverview.clearView();
|
||||
|
||||
// The memory overview graph is not always available.
|
||||
if (this.memoryOverview) {
|
||||
this.memoryOverview.clearView();
|
||||
}
|
||||
|
||||
this.waterfall.clearView();
|
||||
|
||||
window.emit(EVENTS.RECORDING_STARTED);
|
||||
|
@ -226,18 +317,28 @@ let TimelineView = {
|
|||
*/
|
||||
handleRecordingEnded: function() {
|
||||
$("#record-button").removeAttribute("checked");
|
||||
$("#memory-checkbox").removeAttribute("disabled");
|
||||
$("#timeline-pane").selectedPanel = $("#timeline-waterfall");
|
||||
|
||||
this.overview.selectionEnabled = true;
|
||||
this.markersOverview.selectionEnabled = true;
|
||||
|
||||
// The memory overview graph is not always available.
|
||||
if (this.memoryOverview) {
|
||||
this.memoryOverview.selectionEnabled = true;
|
||||
}
|
||||
|
||||
let interval = TimelineController.getInterval();
|
||||
let markers = TimelineController.getMarkers();
|
||||
let memory = TimelineController.getMemory();
|
||||
|
||||
if (markers.length) {
|
||||
let start = (markers[0].start - markers.startTime) * this.overview.dataScaleX;
|
||||
let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
|
||||
this.overview.setSelection({ start, end });
|
||||
let start = (markers[0].start - interval.startTime) * this.markersOverview.dataScaleX;
|
||||
let end = start + this.markersOverview.width * OVERVIEW_INITIAL_SELECTION_RATIO;
|
||||
this.markersOverview.setSelection({ start, end });
|
||||
} else {
|
||||
let duration = markers.endTime - markers.startTime;
|
||||
this.waterfall.setData(markers, markers.startTime, markers.endTime);
|
||||
let timeStart = interval.startTime;
|
||||
let timeEnd = interval.endTime;
|
||||
this.waterfall.setData(markers, timeStart, timeStart, timeEnd);
|
||||
}
|
||||
|
||||
window.emit(EVENTS.RECORDING_ENDED);
|
||||
|
@ -246,12 +347,19 @@ let TimelineView = {
|
|||
/**
|
||||
* Signals that a new set of markers was made available by the controller,
|
||||
* or that the overview graph needs to be updated.
|
||||
*
|
||||
* @param array markers
|
||||
* A list of new markers collected since the recording has started.
|
||||
*/
|
||||
handleMarkersUpdate: function(markers) {
|
||||
this.overview.setData(markers);
|
||||
handleRecordingUpdate: function() {
|
||||
let interval = TimelineController.getInterval();
|
||||
let markers = TimelineController.getMarkers();
|
||||
let memory = TimelineController.getMemory();
|
||||
|
||||
this.markersOverview.setData({ interval, markers });
|
||||
|
||||
// The memory overview graph is not always available.
|
||||
if (this.memoryOverview) {
|
||||
this.memoryOverview.setData({ interval, memory });
|
||||
}
|
||||
|
||||
window.emit(EVENTS.OVERVIEW_UPDATED);
|
||||
},
|
||||
|
||||
|
@ -259,19 +367,21 @@ let TimelineView = {
|
|||
* Callback handling the "selecting" event on the timeline overview.
|
||||
*/
|
||||
_onSelecting: function() {
|
||||
if (!this.overview.hasSelection() &&
|
||||
!this.overview.hasSelectionInProgress()) {
|
||||
if (!this.markersOverview.hasSelection() &&
|
||||
!this.markersOverview.hasSelectionInProgress()) {
|
||||
this.waterfall.clearView();
|
||||
return;
|
||||
}
|
||||
let selection = this.overview.getSelection();
|
||||
let start = selection.start / this.overview.dataScaleX;
|
||||
let end = selection.end / this.overview.dataScaleX;
|
||||
let selection = this.markersOverview.getSelection();
|
||||
let start = selection.start / this.markersOverview.dataScaleX;
|
||||
let end = selection.end / this.markersOverview.dataScaleX;
|
||||
|
||||
let markers = TimelineController.getMarkers();
|
||||
let timeStart = markers.startTime + Math.min(start, end);
|
||||
let timeEnd = markers.startTime + Math.max(start, end);
|
||||
this.waterfall.setData(markers, timeStart, timeEnd);
|
||||
let interval = TimelineController.getInterval();
|
||||
|
||||
let timeStart = interval.startTime + Math.min(start, end);
|
||||
let timeEnd = interval.startTime + Math.max(start, end);
|
||||
this.waterfall.setData(markers, interval.startTime, timeStart, timeEnd);
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,13 +25,17 @@
|
|||
class="devtools-toolbarbutton"
|
||||
oncommand="TimelineController.toggleRecording()"
|
||||
tooltiptext="&timelineUI.recordButton.tooltip;"/>
|
||||
<spacer flex="1"/>
|
||||
<checkbox id="memory-checkbox"
|
||||
label="&timelineUI.memoryCheckbox.label;"
|
||||
oncommand="TimelineController.updateMemoryRecording()"
|
||||
tooltiptext="&timelineUI.memoryCheckbox.tooltip;"/>
|
||||
<label id="record-label"
|
||||
value="&timelineUI.recordLabel;"/>
|
||||
</hbox>
|
||||
</toolbar>
|
||||
|
||||
<vbox id="timeline-overview"/>
|
||||
<vbox id="markers-overview"/>
|
||||
<vbox id="memory-overview"/>
|
||||
|
||||
<deck id="timeline-pane"
|
||||
flex="1">
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
"use strict";
|
||||
|
||||
/**
|
||||
* This file contains the "overview" graph, which is a minimap of all the
|
||||
* timeline data. Regions inside it may be selected, determining which markers
|
||||
* are visible in the "waterfall".
|
||||
* This file contains the "markers overview" graph, which is a minimap of all
|
||||
* the timeline data. Regions inside it may be selected, determining which
|
||||
* markers are visible in the "waterfall".
|
||||
*/
|
||||
|
||||
const {Cc, Ci, Cu, Cr} = require("chrome");
|
||||
|
@ -21,8 +21,8 @@ loader.lazyRequireGetter(this, "TIMELINE_BLUEPRINT",
|
|||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const OVERVIEW_HEADER_HEIGHT = 20; // px
|
||||
const OVERVIEW_BODY_HEIGHT = 50; // px
|
||||
const OVERVIEW_HEADER_HEIGHT = 14; // px
|
||||
const OVERVIEW_BODY_HEIGHT = 55; // 11px * 5 groups
|
||||
|
||||
const OVERVIEW_BACKGROUND_COLOR = "#fff";
|
||||
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
|
||||
|
@ -33,36 +33,36 @@ const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
|
|||
const OVERVIEW_HEADER_TICKS_MULTIPLE = 100; // ms
|
||||
const OVERVIEW_HEADER_TICKS_SPACING_MIN = 75; // px
|
||||
const OVERVIEW_HEADER_SAFE_BOUNDS = 50; // px
|
||||
const OVERVIEW_HEADER_BACKGROUND = "#ebeced";
|
||||
const OVERVIEW_HEADER_BACKGROUND = "#fff";
|
||||
const OVERVIEW_HEADER_TEXT_COLOR = "#18191a";
|
||||
const OVERVIEW_HEADER_TEXT_FONT_SIZE = 9; // px
|
||||
const OVERVIEW_HEADER_TEXT_FONT_FAMILY = "sans-serif";
|
||||
const OVERVIEW_HEADER_TEXT_PADDING = 6; // px
|
||||
const OVERVIEW_TIMELINE_STROKES = "#aaa";
|
||||
const OVERVIEW_HEADER_TEXT_PADDING_LEFT = 6; // px
|
||||
const OVERVIEW_HEADER_TEXT_PADDING_TOP = 1; // px
|
||||
const OVERVIEW_TIMELINE_STROKES = "#ccc";
|
||||
const OVERVIEW_MARKERS_COLOR_STOPS = [0, 0.1, 0.75, 1];
|
||||
const OVERVIEW_MARKER_DURATION_MIN = 4; // ms
|
||||
const OVERVIEW_GROUP_VERTICAL_PADDING = 6; // px
|
||||
const OVERVIEW_GROUP_VERTICAL_PADDING = 5; // px
|
||||
const OVERVIEW_GROUP_ALTERNATING_BACKGROUND = "rgba(0,0,0,0.05)";
|
||||
|
||||
/**
|
||||
* An overview for the timeline data.
|
||||
* An overview for the markers data.
|
||||
*
|
||||
* @param nsIDOMNode parent
|
||||
* The parent node holding the overview.
|
||||
*/
|
||||
function Overview(parent, ...args) {
|
||||
AbstractCanvasGraph.apply(this, [parent, "timeline-overview", ...args]);
|
||||
function MarkersOverview(parent, ...args) {
|
||||
AbstractCanvasGraph.apply(this, [parent, "markers-overview", ...args]);
|
||||
this.once("ready", () => {
|
||||
// Set the list of names, properties and colors used to paint this overview.
|
||||
this.setBlueprint(TIMELINE_BLUEPRINT);
|
||||
|
||||
var preview = [];
|
||||
preview.startTime = 0;
|
||||
preview.endTime = 1000;
|
||||
this.setData(preview);
|
||||
// Populate this overview with some dummy initial data.
|
||||
this.setData({ interval: { startTime: 0, endTime: 1000 }, markers: [] });
|
||||
});
|
||||
}
|
||||
|
||||
Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
||||
MarkersOverview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
||||
fixedHeight: OVERVIEW_HEADER_HEIGHT + OVERVIEW_BODY_HEIGHT,
|
||||
clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
|
||||
selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
|
||||
|
@ -83,11 +83,23 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Disables selection and empties this graph.
|
||||
*/
|
||||
clearView: function() {
|
||||
this.selectionEnabled = false;
|
||||
this.dropSelection();
|
||||
this.setData({ interval: { startTime: 0, endTime: 0 }, markers: [] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Renders the graph's data source.
|
||||
* @see AbstractCanvasGraph.prototype.buildGraphImage
|
||||
*/
|
||||
buildGraphImage: function() {
|
||||
let { interval, markers } = this._data;
|
||||
let { startTime, endTime } = interval;
|
||||
|
||||
let { canvas, ctx } = this._getNamedCanvas("overview-data");
|
||||
let canvasWidth = this._width;
|
||||
let canvasHeight = this._height;
|
||||
|
@ -97,7 +109,7 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
// Group markers into separate paint batches. This is necessary to
|
||||
// draw all markers sharing the same style at once.
|
||||
|
||||
for (let marker of this._data) {
|
||||
for (let marker of markers) {
|
||||
this._paintBatches.get(marker.name).batch.push(marker);
|
||||
}
|
||||
|
||||
|
@ -108,7 +120,7 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
let groupHeight = OVERVIEW_BODY_HEIGHT * this._pixelRatio / totalGroups;
|
||||
let groupPadding = OVERVIEW_GROUP_VERTICAL_PADDING * this._pixelRatio;
|
||||
|
||||
let totalTime = (this._data.endTime - this._data.startTime) || 0;
|
||||
let totalTime = (endTime - startTime) || 0;
|
||||
let dataScale = this.dataScaleX = availableWidth / totalTime;
|
||||
|
||||
// Draw the header and overview background.
|
||||
|
@ -124,7 +136,7 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
ctx.fillStyle = OVERVIEW_GROUP_ALTERNATING_BACKGROUND;
|
||||
ctx.beginPath();
|
||||
|
||||
for (let i = 1; i < totalGroups; i += 2) {
|
||||
for (let i = 0; i < totalGroups; i += 2) {
|
||||
let top = headerHeight + i * groupHeight;
|
||||
ctx.rect(0, top, canvasWidth, groupHeight);
|
||||
}
|
||||
|
@ -133,24 +145,26 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
|
||||
// Draw the timeline header ticks.
|
||||
|
||||
ctx.textBaseline = "middle";
|
||||
let fontSize = OVERVIEW_HEADER_TEXT_FONT_SIZE * this._pixelRatio;
|
||||
let fontFamily = OVERVIEW_HEADER_TEXT_FONT_FAMILY;
|
||||
let textPaddingLeft = OVERVIEW_HEADER_TEXT_PADDING_LEFT * this._pixelRatio;
|
||||
let textPaddingTop = OVERVIEW_HEADER_TEXT_PADDING_TOP * this._pixelRatio;
|
||||
let tickInterval = this._findOptimalTickInterval(dataScale);
|
||||
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.font = fontSize + "px " + fontFamily;
|
||||
ctx.fillStyle = OVERVIEW_HEADER_TEXT_COLOR;
|
||||
ctx.strokeStyle = OVERVIEW_TIMELINE_STROKES;
|
||||
ctx.beginPath();
|
||||
|
||||
let tickInterval = this._findOptimalTickInterval(dataScale);
|
||||
let headerTextPadding = OVERVIEW_HEADER_TEXT_PADDING * this._pixelRatio;
|
||||
|
||||
for (let x = 0; x < availableWidth; x += tickInterval) {
|
||||
let left = x + headerTextPadding;
|
||||
let lineLeft = x;
|
||||
let textLeft = lineLeft + textPaddingLeft;
|
||||
let time = Math.round(x / dataScale);
|
||||
let label = L10N.getFormatStr("timeline.tick", time);
|
||||
ctx.fillText(label, left, headerHeight / 2 + 1);
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, canvasHeight);
|
||||
ctx.fillText(label, textLeft, headerHeight / 2 + textPaddingTop);
|
||||
ctx.moveTo(lineLeft, 0);
|
||||
ctx.lineTo(lineLeft, canvasHeight);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
|
@ -170,8 +184,8 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
ctx.beginPath();
|
||||
|
||||
for (let { start, end } of batch) {
|
||||
start -= this._data.startTime;
|
||||
end -= this._data.startTime;
|
||||
start -= interval.startTime;
|
||||
end -= interval.startTime;
|
||||
|
||||
let left = start * dataScale;
|
||||
let duration = Math.max(end - start, OVERVIEW_MARKER_DURATION_MIN);
|
||||
|
@ -208,4 +222,4 @@ Overview.prototype = Heritage.extend(AbstractCanvasGraph.prototype, {
|
|||
}
|
||||
});
|
||||
|
||||
exports.Overview = Overview;
|
||||
exports.MarkersOverview = MarkersOverview;
|
|
@ -0,0 +1,88 @@
|
|||
/* 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 file contains the "memory overview" graph, a simple representation of
|
||||
* of all the memory measurements taken while streaming the timeline data.
|
||||
*/
|
||||
|
||||
const {Cc, Ci, Cu, Cr} = require("chrome");
|
||||
|
||||
Cu.import("resource:///modules/devtools/Graphs.jsm");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
|
||||
loader.lazyRequireGetter(this, "L10N",
|
||||
"devtools/timeline/global", true);
|
||||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const OVERVIEW_HEIGHT = 30; // px
|
||||
|
||||
const OVERVIEW_BACKGROUND_COLOR = "#fff";
|
||||
const OVERVIEW_BACKGROUND_GRADIENT_START = "rgba(0,136,204,0.1)";
|
||||
const OVERVIEW_BACKGROUND_GRADIENT_END = "rgba(0,136,204,0.0)";
|
||||
const OVERVIEW_STROKE_WIDTH = 1; // px
|
||||
const OVERVIEW_STROKE_COLOR = "rgba(0,136,204,1)";
|
||||
const OVERVIEW_CLIPHEAD_LINE_COLOR = "#666";
|
||||
const OVERVIEW_SELECTION_LINE_COLOR = "#555";
|
||||
const OVERVIEW_MAXIMUM_LINE_COLOR = "rgba(0,136,204,0.4)";
|
||||
const OVERVIEW_AVERAGE_LINE_COLOR = "rgba(0,136,204,0.7)";
|
||||
const OVERVIEW_MINIMUM_LINE_COLOR = "rgba(0,136,204,0.9)";
|
||||
|
||||
const OVERVIEW_SELECTION_BACKGROUND_COLOR = "rgba(76,158,217,0.25)";
|
||||
const OVERVIEW_SELECTION_STRIPES_COLOR = "rgba(255,255,255,0.1)";
|
||||
|
||||
/**
|
||||
* An overview for the memory data.
|
||||
*
|
||||
* @param nsIDOMNode parent
|
||||
* The parent node holding the overview.
|
||||
*/
|
||||
function MemoryOverview(parent) {
|
||||
LineGraphWidget.call(this, parent, L10N.getStr("graphs.memory"));
|
||||
|
||||
this.once("ready", () => {
|
||||
// Populate this overview with some dummy initial data.
|
||||
this.setData({ interval: { startTime: 0, endTime: 1000 }, memory: [] });
|
||||
});
|
||||
}
|
||||
|
||||
MemoryOverview.prototype = Heritage.extend(LineGraphWidget.prototype, {
|
||||
dampenValuesFactor: 0.95,
|
||||
fixedHeight: OVERVIEW_HEIGHT,
|
||||
backgroundColor: OVERVIEW_BACKGROUND_COLOR,
|
||||
backgroundGradientStart: OVERVIEW_BACKGROUND_GRADIENT_START,
|
||||
backgroundGradientEnd: OVERVIEW_BACKGROUND_GRADIENT_END,
|
||||
strokeColor: OVERVIEW_STROKE_COLOR,
|
||||
strokeWidth: OVERVIEW_STROKE_WIDTH,
|
||||
maximumLineColor: OVERVIEW_MAXIMUM_LINE_COLOR,
|
||||
averageLineColor: OVERVIEW_AVERAGE_LINE_COLOR,
|
||||
minimumLineColor: OVERVIEW_MINIMUM_LINE_COLOR,
|
||||
clipheadLineColor: OVERVIEW_CLIPHEAD_LINE_COLOR,
|
||||
selectionLineColor: OVERVIEW_SELECTION_LINE_COLOR,
|
||||
selectionBackgroundColor: OVERVIEW_SELECTION_BACKGROUND_COLOR,
|
||||
selectionStripesColor: OVERVIEW_SELECTION_STRIPES_COLOR,
|
||||
withTooltipArrows: false,
|
||||
withFixedTooltipPositions: true,
|
||||
|
||||
/**
|
||||
* Disables selection and empties this graph.
|
||||
*/
|
||||
clearView: function() {
|
||||
this.selectionEnabled = false;
|
||||
this.dropSelection();
|
||||
this.setData({ interval: { startTime: 0, endTime: 0 }, memory: [] });
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets the data source for this graph.
|
||||
*/
|
||||
setData: function({ interval, memory }) {
|
||||
this.dataOffsetX = interval.startTime;
|
||||
LineGraphWidget.prototype.setData.call(this, memory);
|
||||
}
|
||||
});
|
||||
|
||||
exports.MemoryOverview = MemoryOverview;
|
|
@ -22,15 +22,14 @@ loader.lazyImporter(this, "clearNamedTimeout",
|
|||
|
||||
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
|
||||
const TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
|
||||
const TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
|
||||
const WATERFALL_SIDEBAR_WIDTH = 150; // px
|
||||
|
||||
const TIMELINE_HEADER_TICKS_MULTIPLE = 5; // ms
|
||||
const TIMELINE_HEADER_TICKS_SPACING_MIN = 50; // px
|
||||
const TIMELINE_HEADER_TEXT_PADDING = 3; // px
|
||||
const WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT = 30;
|
||||
const WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY = 75; // ms
|
||||
|
||||
const TIMELINE_MARKER_SIDEBAR_WIDTH = 150; // px
|
||||
const TIMELINE_MARKER_BAR_WIDTH_MIN = 5; // px
|
||||
const WATERFALL_HEADER_TICKS_MULTIPLE = 5; // ms
|
||||
const WATERFALL_HEADER_TICKS_SPACING_MIN = 50; // px
|
||||
const WATERFALL_HEADER_TEXT_PADDING = 3; // px
|
||||
|
||||
const WATERFALL_BACKGROUND_TICKS_MULTIPLE = 5; // ms
|
||||
const WATERFALL_BACKGROUND_TICKS_SCALES = 3;
|
||||
|
@ -38,6 +37,7 @@ const WATERFALL_BACKGROUND_TICKS_SPACING_MIN = 10; // px
|
|||
const WATERFALL_BACKGROUND_TICKS_COLOR_RGB = [128, 136, 144];
|
||||
const WATERFALL_BACKGROUND_TICKS_OPACITY_MIN = 32; // byte
|
||||
const WATERFALL_BACKGROUND_TICKS_OPACITY_ADD = 32; // byte
|
||||
const WATERFALL_MARKER_BAR_WIDTH_MIN = 5; // px
|
||||
|
||||
/**
|
||||
* A detailed waterfall view for the timeline data.
|
||||
|
@ -52,11 +52,11 @@ function Waterfall(parent) {
|
|||
this._outstandingMarkers = [];
|
||||
|
||||
this._headerContents = this._document.createElement("hbox");
|
||||
this._headerContents.className = "timeline-header-contents";
|
||||
this._headerContents.className = "waterfall-header-contents";
|
||||
this._parent.appendChild(this._headerContents);
|
||||
|
||||
this._listContents = this._document.createElement("vbox");
|
||||
this._listContents.className = "timeline-list-contents";
|
||||
this._listContents.className = "waterfall-list-contents";
|
||||
this._listContents.setAttribute("flex", "1");
|
||||
this._parent.appendChild(this._listContents);
|
||||
|
||||
|
@ -75,18 +75,21 @@ Waterfall.prototype = {
|
|||
*
|
||||
* @param array markers
|
||||
* A list of markers received from the controller.
|
||||
* @param number timeEpoch
|
||||
* The absolute time (in milliseconds) when the recording started.
|
||||
* @param number timeStart
|
||||
* The time (in milliseconds) to start drawing from.
|
||||
* @param number timeEnd
|
||||
* The time (in milliseconds) to end drawing at.
|
||||
*/
|
||||
setData: function(markers, timeStart, timeEnd) {
|
||||
setData: function(markers, timeEpoch, timeStart, timeEnd) {
|
||||
this.clearView();
|
||||
|
||||
let dataScale = this._waterfallWidth / (timeEnd - timeStart);
|
||||
this._drawWaterfallBackground(dataScale);
|
||||
|
||||
// Label the header as if the first possible marker was at T=0.
|
||||
this._buildHeader(this._headerContents, timeStart - markers.startTime, dataScale);
|
||||
this._buildHeader(this._headerContents, timeStart - timeEpoch, dataScale);
|
||||
this._buildMarkers(this._listContents, markers, timeStart, timeEnd, dataScale);
|
||||
},
|
||||
|
||||
|
@ -111,7 +114,7 @@ Waterfall.prototype = {
|
|||
*/
|
||||
recalculateBounds: function() {
|
||||
let bounds = this._parent.getBoundingClientRect();
|
||||
this._waterfallWidth = bounds.width - TIMELINE_MARKER_SIDEBAR_WIDTH;
|
||||
this._waterfallWidth = bounds.width - WATERFALL_SIDEBAR_WIDTH;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -126,22 +129,22 @@ Waterfall.prototype = {
|
|||
*/
|
||||
_buildHeader: function(parent, timeStart, dataScale) {
|
||||
let container = this._document.createElement("hbox");
|
||||
container.className = "timeline-header-container";
|
||||
container.className = "waterfall-header-container";
|
||||
container.setAttribute("flex", "1");
|
||||
|
||||
let sidebar = this._document.createElement("hbox");
|
||||
sidebar.className = "timeline-header-sidebar theme-sidebar";
|
||||
sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
|
||||
sidebar.className = "waterfall-sidebar theme-sidebar";
|
||||
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
|
||||
sidebar.setAttribute("align", "center");
|
||||
container.appendChild(sidebar);
|
||||
|
||||
let name = this._document.createElement("label");
|
||||
name.className = "plain timeline-header-name";
|
||||
name.className = "plain waterfall-header-name";
|
||||
name.setAttribute("value", this._l10n.getStr("timeline.records"));
|
||||
sidebar.appendChild(name);
|
||||
|
||||
let ticks = this._document.createElement("hbox");
|
||||
ticks.className = "timeline-header-ticks";
|
||||
ticks.className = "waterfall-header-ticks waterfall-background-ticks";
|
||||
ticks.setAttribute("align", "center");
|
||||
ticks.setAttribute("flex", "1");
|
||||
container.appendChild(ticks);
|
||||
|
@ -149,18 +152,18 @@ Waterfall.prototype = {
|
|||
let offset = this._isRTL ? this._waterfallWidth : 0;
|
||||
let direction = this._isRTL ? -1 : 1;
|
||||
let tickInterval = this._findOptimalTickInterval({
|
||||
ticksMultiple: TIMELINE_HEADER_TICKS_MULTIPLE,
|
||||
ticksSpacingMin: TIMELINE_HEADER_TICKS_SPACING_MIN,
|
||||
ticksMultiple: WATERFALL_HEADER_TICKS_MULTIPLE,
|
||||
ticksSpacingMin: WATERFALL_HEADER_TICKS_SPACING_MIN,
|
||||
dataScale: dataScale
|
||||
});
|
||||
|
||||
for (let x = 0; x < this._waterfallWidth; x += tickInterval) {
|
||||
let start = x + direction * TIMELINE_HEADER_TEXT_PADDING;
|
||||
let start = x + direction * WATERFALL_HEADER_TEXT_PADDING;
|
||||
let time = Math.round(timeStart + x / dataScale);
|
||||
let label = this._l10n.getFormatStr("timeline.tick", time);
|
||||
|
||||
let node = this._document.createElement("label");
|
||||
node.className = "plain timeline-header-tick";
|
||||
node.className = "plain waterfall-header-tick";
|
||||
node.style.transform = "translateX(" + (start - offset) + "px)";
|
||||
node.setAttribute("value", label);
|
||||
ticks.appendChild(node);
|
||||
|
@ -190,7 +193,7 @@ Waterfall.prototype = {
|
|||
// preserve a snappy UI. After a certain delay, continue building the
|
||||
// outstanding markers while there's (hopefully) no user interaction.
|
||||
let arguments_ = [this._fragment, marker, timeStart, dataScale];
|
||||
if (processed++ < TIMELINE_IMMEDIATE_DRAW_MARKERS_COUNT) {
|
||||
if (processed++ < WATERFALL_IMMEDIATE_DRAW_MARKERS_COUNT) {
|
||||
this._buildMarker.apply(this, arguments_);
|
||||
} else {
|
||||
this._outstandingMarkers.push(arguments_);
|
||||
|
@ -205,7 +208,7 @@ Waterfall.prototype = {
|
|||
// Otherwise prepare flushing the outstanding markers after a small delay.
|
||||
else {
|
||||
this._setNamedTimeout("flush-outstanding-markers",
|
||||
TIMELINE_FLUSH_OUTSTANDING_MARKERS_DELAY,
|
||||
WATERFALL_FLUSH_OUTSTANDING_MARKERS_DELAY,
|
||||
() => this._buildOutstandingMarkers(parent));
|
||||
}
|
||||
|
||||
|
@ -241,7 +244,7 @@ Waterfall.prototype = {
|
|||
*/
|
||||
_buildMarker: function(parent, marker, timeStart, dataScale) {
|
||||
let container = this._document.createElement("hbox");
|
||||
container.className = "timeline-marker-container";
|
||||
container.className = "waterfall-marker-container";
|
||||
|
||||
if (marker) {
|
||||
this._buildMarkerSidebar(container, marker);
|
||||
|
@ -267,12 +270,12 @@ Waterfall.prototype = {
|
|||
let blueprint = this._blueprint[marker.name];
|
||||
|
||||
let sidebar = this._document.createElement("hbox");
|
||||
sidebar.className = "timeline-marker-sidebar theme-sidebar";
|
||||
sidebar.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
|
||||
sidebar.className = "waterfall-sidebar theme-sidebar";
|
||||
sidebar.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
|
||||
sidebar.setAttribute("align", "center");
|
||||
|
||||
let bullet = this._document.createElement("hbox");
|
||||
bullet.className = "timeline-marker-bullet";
|
||||
bullet.className = "waterfall-marker-bullet";
|
||||
bullet.style.backgroundColor = blueprint.fill;
|
||||
bullet.style.borderColor = blueprint.stroke;
|
||||
bullet.setAttribute("type", marker.name);
|
||||
|
@ -281,7 +284,7 @@ Waterfall.prototype = {
|
|||
let name = this._document.createElement("label");
|
||||
name.setAttribute("crop", "end");
|
||||
name.setAttribute("flex", "1");
|
||||
name.className = "plain timeline-marker-name";
|
||||
name.className = "plain waterfall-marker-name";
|
||||
|
||||
let label;
|
||||
if (marker.detail && marker.detail.causeName) {
|
||||
|
@ -314,7 +317,8 @@ Waterfall.prototype = {
|
|||
let blueprint = this._blueprint[marker.name];
|
||||
|
||||
let waterfall = this._document.createElement("hbox");
|
||||
waterfall.className = "timeline-marker-waterfall";
|
||||
waterfall.className = "waterfall-marker-item waterfall-background-ticks";
|
||||
waterfall.setAttribute("align", "center");
|
||||
waterfall.setAttribute("flex", "1");
|
||||
|
||||
let start = (marker.start - timeStart) * dataScale;
|
||||
|
@ -322,12 +326,12 @@ Waterfall.prototype = {
|
|||
let offset = this._isRTL ? this._waterfallWidth : 0;
|
||||
|
||||
let bar = this._document.createElement("hbox");
|
||||
bar.className = "timeline-marker-bar";
|
||||
bar.className = "waterfall-marker-bar";
|
||||
bar.style.backgroundColor = blueprint.fill;
|
||||
bar.style.borderColor = blueprint.stroke;
|
||||
bar.style.transform = "translateX(" + (start - offset) + "px)";
|
||||
bar.setAttribute("type", marker.name);
|
||||
bar.setAttribute("width", Math.max(width, TIMELINE_MARKER_BAR_WIDTH_MIN));
|
||||
bar.setAttribute("width", Math.max(width, WATERFALL_MARKER_BAR_WIDTH_MIN));
|
||||
waterfall.appendChild(bar);
|
||||
|
||||
container.appendChild(waterfall);
|
||||
|
@ -341,11 +345,11 @@ Waterfall.prototype = {
|
|||
*/
|
||||
_buildMarkerSpacer: function(container) {
|
||||
let sidebarSpacer = this._document.createElement("spacer");
|
||||
sidebarSpacer.className = "timeline-marker-sidebar theme-sidebar";
|
||||
sidebarSpacer.setAttribute("width", TIMELINE_MARKER_SIDEBAR_WIDTH);
|
||||
sidebarSpacer.className = "waterfall-sidebar theme-sidebar";
|
||||
sidebarSpacer.setAttribute("width", WATERFALL_SIDEBAR_WIDTH);
|
||||
|
||||
let waterfallSpacer = this._document.createElement("spacer");
|
||||
waterfallSpacer.className = "timeline-marker-waterfall";
|
||||
waterfallSpacer.className = "waterfall-marker-item waterfall-background-ticks";
|
||||
waterfallSpacer.setAttribute("flex", "1");
|
||||
|
||||
container.appendChild(sidebarSpacer);
|
||||
|
|
|
@ -15,10 +15,19 @@
|
|||
- on a button that starts a new recording. -->
|
||||
<!ENTITY timelineUI.recordButton.tooltip "Record timeline operations">
|
||||
|
||||
<!-- LOCALIZATION NOTE (timelineUI.recordButton): This string is displayed
|
||||
<!-- LOCALIZATION NOTE (timelineUI.recordLabel): This string is displayed
|
||||
- as a label to signal that a recording is in progress. -->
|
||||
<!ENTITY timelineUI.recordLabel "Recording…">
|
||||
|
||||
<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.label): This string
|
||||
- is displayed next to a checkbox determining whether or not memory
|
||||
- measurements are enabled. -->
|
||||
<!ENTITY timelineUI.memoryCheckbox.label "Memory">
|
||||
|
||||
<!-- LOCALIZATION NOTE (timelineUI.timelineUI.memoryCheckbox.tooltip): This string
|
||||
- is displayed next to the memory checkbox -->
|
||||
<!ENTITY timelineUI.memoryCheckbox.tooltip "Enable memory measurements">
|
||||
|
||||
<!-- LOCALIZATION NOTE (timelineUI.emptyNotice1/2): This is the label shown
|
||||
- in the timeline view when empty. -->
|
||||
<!ENTITY timelineUI.emptyNotice1 "Click on the">
|
||||
|
|
|
@ -41,6 +41,12 @@ timeline.label.paint=Paint
|
|||
timeline.label.domevent=DOM Event
|
||||
timeline.label.consoleTime=Console
|
||||
|
||||
# LOCALIZATION NOTE (graphs.memory):
|
||||
# This string is displayed in the memory graph of the Performance tool,
|
||||
# as the unit used to memory consumption. This label should be kept
|
||||
# AS SHORT AS POSSIBLE so it doesn't obstruct important parts of the graph.
|
||||
graphs.memory=MB
|
||||
|
||||
# LOCALIZATION NOTE (timeline.markerDetailFormat):
|
||||
# Some timeline markers come with details, like a size, a name, a js function.
|
||||
# %1$S is replaced with one of the above label (timeline.label.*) and %2$S
|
||||
|
|
|
@ -46,7 +46,7 @@ display_name_available_status=Available
|
|||
## LOCALIZATION NOTE(unable_retrieve_url,session_expired_error_description,could_not_authenticate,password_changed_question,try_again_later,could_not_connect,check_internet_connection,login_expired,service_not_available,problem_accessing_account):
|
||||
## These may be displayed at the top of the panel here:
|
||||
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#error
|
||||
unable_retrieve_url=Sorry, we were unable to retrieve a call url.
|
||||
unable_retrieve_url=Sorry, we were unable to retrieve a call URL.
|
||||
session_expired_error_description=Session expired. All URLs you have previously created and shared will no longer work.
|
||||
could_not_authenticate=Could Not Authenticate
|
||||
password_changed_question=Did you change your password?
|
||||
|
|
|
@ -25,6 +25,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry",
|
|||
"resource:///modules/BrowserUITelemetry.jsm");
|
||||
|
||||
|
||||
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
|
||||
const PREF_LOG_LEVEL = "browser.uitour.loglevel";
|
||||
const PREF_SEENPAGEIDS = "browser.uitour.seenPageIDs";
|
||||
const MAX_BUTTONS = 4;
|
||||
|
||||
|
@ -42,6 +44,16 @@ const SEENPAGEID_EXPIRY = 8 * 7 * 24 * 60 * 60 * 1000; // 8 weeks.
|
|||
// Prefix for any target matching a search engine.
|
||||
const TARGET_SEARCHENGINE_PREFIX = "searchEngine-";
|
||||
|
||||
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
|
||||
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||
let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
|
||||
let consoleOptions = {
|
||||
// toLowerCase is because the loglevel values use title case to be compatible with Log.jsm.
|
||||
maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
|
||||
prefix: "UITour",
|
||||
};
|
||||
return new ConsoleAPI(consoleOptions);
|
||||
});
|
||||
|
||||
this.UITour = {
|
||||
url: null,
|
||||
|
@ -141,6 +153,7 @@ this.UITour = {
|
|||
]),
|
||||
|
||||
init: function() {
|
||||
log.debug("Initializing UITour");
|
||||
// Lazy getter is initialized here so it can be replicated any time
|
||||
// in a test.
|
||||
delete this.seenPageIDs;
|
||||
|
@ -224,22 +237,29 @@ this.UITour = {
|
|||
|
||||
onPageEvent: function(aMessage, aEvent) {
|
||||
let contentDocument = null;
|
||||
|
||||
let browser = aMessage.target;
|
||||
let window = browser.ownerDocument.defaultView;
|
||||
let tab = window.gBrowser.getTabForBrowser(browser);
|
||||
let messageManager = browser.messageManager;
|
||||
|
||||
if (typeof aEvent.detail != "object")
|
||||
log.debug("onPageEvent:", aEvent.detail);
|
||||
|
||||
if (typeof aEvent.detail != "object") {
|
||||
log.warn("Malformed event - detail not an object");
|
||||
return false;
|
||||
}
|
||||
|
||||
let action = aEvent.detail.action;
|
||||
if (typeof action != "string" || !action)
|
||||
if (typeof action != "string" || !action) {
|
||||
log.warn("Action not defined");
|
||||
return false;
|
||||
}
|
||||
|
||||
let data = aEvent.detail.data;
|
||||
if (typeof data != "object")
|
||||
if (typeof data != "object") {
|
||||
log.warn("Malformed event - data not an object");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do this before bailing if there's no tab, so later we can pick up the pieces:
|
||||
window.gBrowser.tabContainer.addEventListener("TabSelect", this);
|
||||
|
@ -249,11 +269,12 @@ this.UITour = {
|
|||
if (!tab) {
|
||||
// This should only happen while detaching a tab:
|
||||
if (this._detachingTab) {
|
||||
log.debug("Got event while detatching a tab");
|
||||
this._queuedEvents.push(aEvent);
|
||||
this._pendingDoc = Cu.getWeakReference(contentDocument);
|
||||
return;
|
||||
}
|
||||
Cu.reportError("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
|
||||
log.error("Discarding tabless UITour event (" + action + ") while not detaching a tab." +
|
||||
"This shouldn't happen!");
|
||||
return;
|
||||
}
|
||||
|
@ -262,22 +283,28 @@ this.UITour = {
|
|||
switch (action) {
|
||||
case "registerPageID": {
|
||||
// This is only relevant if Telemtry is enabled.
|
||||
if (!UITelemetry.enabled)
|
||||
if (!UITelemetry.enabled) {
|
||||
log.debug("registerPageID: Telemery disabled, not doing anything");
|
||||
break;
|
||||
}
|
||||
|
||||
// We don't want to allow BrowserUITelemetry.BUCKET_SEPARATOR in the
|
||||
// pageID, as it could make parsing the telemetry bucket name difficult.
|
||||
if (typeof data.pageID == "string" &&
|
||||
!data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
|
||||
this.addSeenPageID(data.pageID);
|
||||
|
||||
// Store tabs and windows separately so we don't need to loop over all
|
||||
// tabs when a window is closed.
|
||||
this.pageIDSourceTabs.set(tab, data.pageID);
|
||||
this.pageIDSourceWindows.set(window, data.pageID);
|
||||
|
||||
this.setTelemetryBucket(data.pageID);
|
||||
if (typeof data.pageID != "string" ||
|
||||
data.pageID.contains(BrowserUITelemetry.BUCKET_SEPARATOR)) {
|
||||
log.warn("registerPageID: Invalid page ID specified");
|
||||
break;
|
||||
}
|
||||
|
||||
this.addSeenPageID(data.pageID);
|
||||
|
||||
// Store tabs and windows separately so we don't need to loop over all
|
||||
// tabs when a window is closed.
|
||||
this.pageIDSourceTabs.set(tab, data.pageID);
|
||||
this.pageIDSourceWindows.set(window, data.pageID);
|
||||
|
||||
this.setTelemetryBucket(data.pageID);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -285,7 +312,7 @@ this.UITour = {
|
|||
let targetPromise = this.getTarget(window, data.target);
|
||||
targetPromise.then(target => {
|
||||
if (!target.node) {
|
||||
Cu.reportError("UITour: Target could not be resolved: " + data.target);
|
||||
log.error("UITour: Target could not be resolved: " + data.target);
|
||||
return;
|
||||
}
|
||||
let effect = undefined;
|
||||
|
@ -293,7 +320,7 @@ this.UITour = {
|
|||
effect = data.effect;
|
||||
}
|
||||
this.showHighlight(target, effect);
|
||||
}).then(null, Cu.reportError);
|
||||
}).catch(log.error);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -306,7 +333,7 @@ this.UITour = {
|
|||
let targetPromise = this.getTarget(window, data.target, true);
|
||||
targetPromise.then(target => {
|
||||
if (!target.node) {
|
||||
Cu.reportError("UITour: Target could not be resolved: " + data.target);
|
||||
log.error("UITour: Target could not be resolved: " + data.target);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -333,8 +360,10 @@ this.UITour = {
|
|||
|
||||
buttons.push(button);
|
||||
|
||||
if (buttons.length == MAX_BUTTONS)
|
||||
if (buttons.length == MAX_BUTTONS) {
|
||||
log.warn("showInfo: Reached limit of allowed number of buttons");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -347,7 +376,7 @@ this.UITour = {
|
|||
infoOptions.targetCallbackID = data.targetCallbackID;
|
||||
|
||||
this.showInfo(messageManager, target, data.title, data.text, iconURL, buttons, infoOptions);
|
||||
}).then(null, Cu.reportError);
|
||||
}).catch(log.error);
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -392,6 +421,7 @@ this.UITour = {
|
|||
case "startUrlbarCapture": {
|
||||
if (typeof data.text != "string" || !data.text ||
|
||||
typeof data.url != "string" || !data.url) {
|
||||
log.warn("startUrlbarCapture: Text or URL not specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -399,6 +429,7 @@ this.UITour = {
|
|||
try {
|
||||
uri = Services.io.newURI(data.url, null, null);
|
||||
} catch (e) {
|
||||
log.warn("startUrlbarCapture: Malformed URL specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -408,6 +439,7 @@ this.UITour = {
|
|||
try {
|
||||
secman.checkLoadURIWithPrincipal(principal, uri, flags);
|
||||
} catch (e) {
|
||||
log.warn("startUrlbarCapture: Orginating page doesn't have permission to open specified URL");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -422,6 +454,7 @@ this.UITour = {
|
|||
|
||||
case "getConfiguration": {
|
||||
if (typeof data.configuration != "string") {
|
||||
log.warn("getConfiguration: No configuration option specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -448,7 +481,7 @@ this.UITour = {
|
|||
let targetPromise = this.getTarget(window, data.name);
|
||||
targetPromise.then(target => {
|
||||
this.addNavBarWidget(target, messageManager, data.callbackID);
|
||||
}).then(null, Cu.reportError);
|
||||
}).catch(log.error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -529,7 +562,7 @@ this.UITour = {
|
|||
try {
|
||||
this.onPageEvent(this._queuedEvents.shift());
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
log.error(ex);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
@ -583,6 +616,7 @@ this.UITour = {
|
|||
},
|
||||
|
||||
teardownTour: function(aWindow, aWindowClosing = false) {
|
||||
log.debug("teardownTour: aWindowClosing = " + aWindowClosing);
|
||||
aWindow.gBrowser.tabContainer.removeEventListener("TabSelect", this);
|
||||
aWindow.PanelUI.panel.removeEventListener("popuphiding", this.hidePanelAnnotations);
|
||||
aWindow.PanelUI.panel.removeEventListener("ViewShowing", this.hidePanelAnnotations);
|
||||
|
@ -627,8 +661,10 @@ this.UITour = {
|
|||
if (!Services.prefs.getBoolPref("browser.uitour.requireSecure"))
|
||||
allowedSchemes.add("http");
|
||||
|
||||
if (!allowedSchemes.has(aURI.scheme))
|
||||
if (!allowedSchemes.has(aURI.scheme)) {
|
||||
log.error("Unsafe scheme:", aURI.scheme);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
@ -648,6 +684,7 @@ this.UITour = {
|
|||
|
||||
sendPageCallback: function(aMessageManager, aCallbackID, aData = {}) {
|
||||
let detail = {data: aData, callbackID: aCallbackID};
|
||||
log.debug("sendPageCallback", detail);
|
||||
aMessageManager.sendAsyncMessage("UITour:SendPageCallback", detail);
|
||||
},
|
||||
|
||||
|
@ -657,8 +694,10 @@ this.UITour = {
|
|||
},
|
||||
|
||||
getTarget: function(aWindow, aTargetName, aSticky = false) {
|
||||
log.debug("getTarget:", aTargetName);
|
||||
let deferred = Promise.defer();
|
||||
if (typeof aTargetName != "string" || !aTargetName) {
|
||||
log.warn("getTarget: Invalid target name specified");
|
||||
deferred.reject("Invalid target name specified");
|
||||
return deferred.promise;
|
||||
}
|
||||
|
@ -678,6 +717,7 @@ this.UITour = {
|
|||
|
||||
let targetObject = this.targets.get(aTargetName);
|
||||
if (!targetObject) {
|
||||
log.warn("getTarget: The specified target name is not in the allowed set");
|
||||
deferred.reject("The specified target name is not in the allowed set");
|
||||
return deferred.promise;
|
||||
}
|
||||
|
@ -689,6 +729,7 @@ this.UITour = {
|
|||
try {
|
||||
node = targetQuery(aWindow.document);
|
||||
} catch (ex) {
|
||||
log.warn("getTarget: Error running target query:", ex);
|
||||
node = null;
|
||||
}
|
||||
} else {
|
||||
|
@ -703,7 +744,7 @@ this.UITour = {
|
|||
widgetName: targetObject.widgetName,
|
||||
allowAdd: targetObject.allowAdd,
|
||||
});
|
||||
}).then(null, Cu.reportError);
|
||||
}).catch(log.error);
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
|
@ -729,9 +770,13 @@ this.UITour = {
|
|||
* we need to open or close the appMenu to see the annotation's anchor.
|
||||
*/
|
||||
_setAppMenuStateForAnnotation: function(aWindow, aAnnotationType, aShouldOpenForHighlight, aCallback = null) {
|
||||
log.debug("_setAppMenuStateForAnnotation:", aAnnotationType);
|
||||
log.debug("_setAppMenuStateForAnnotation: Menu is exptected to be:", aShouldOpenForHighlight ? "open" : "closed");
|
||||
|
||||
// If the panel is in the desired state, we're done.
|
||||
let panelIsOpen = aWindow.PanelUI.panel.state != "closed";
|
||||
if (aShouldOpenForHighlight == panelIsOpen) {
|
||||
log.debug("_setAppMenuStateForAnnotation: Panel already in expected state");
|
||||
if (aCallback)
|
||||
aCallback();
|
||||
return;
|
||||
|
@ -739,6 +784,7 @@ this.UITour = {
|
|||
|
||||
// Don't close the menu if it wasn't opened by us (e.g. via showmenu instead).
|
||||
if (!aShouldOpenForHighlight && !this.appMenuOpenForAnnotation.has(aAnnotationType)) {
|
||||
log.debug("_setAppMenuStateForAnnotation: Menu not opened by us, not closing");
|
||||
if (aCallback)
|
||||
aCallback();
|
||||
return;
|
||||
|
@ -752,8 +798,10 @@ this.UITour = {
|
|||
|
||||
// Actually show or hide the menu
|
||||
if (this.appMenuOpenForAnnotation.size) {
|
||||
log.debug("_setAppMenuStateForAnnotation: Opening the menu");
|
||||
this.showMenu(aWindow, "appMenu", aCallback);
|
||||
} else {
|
||||
log.debug("_setAppMenuStateForAnnotation: Closing the menu");
|
||||
this.hideMenu(aWindow, "appMenu");
|
||||
if (aCallback)
|
||||
aCallback();
|
||||
|
@ -870,6 +918,7 @@ this.UITour = {
|
|||
|
||||
// Close a previous highlight so we can relocate the panel.
|
||||
if (highlighter.parentElement.state == "showing" || highlighter.parentElement.state == "open") {
|
||||
log.debug("showHighlight: Closing previous highlight first");
|
||||
highlighter.parentElement.hidePopup();
|
||||
}
|
||||
/* The "overlap" position anchors from the top-left but we want to centre highlights at their
|
||||
|
@ -890,8 +939,10 @@ this.UITour = {
|
|||
}
|
||||
|
||||
// Prevent showing a panel at an undefined position.
|
||||
if (!this.isElementVisible(aTarget.node))
|
||||
if (!this.isElementVisible(aTarget.node)) {
|
||||
log.warn("showHighlight: Not showing a highlight since the target isn't visible", aTarget);
|
||||
return;
|
||||
}
|
||||
|
||||
this._setAppMenuStateForAnnotation(aTarget.node.ownerDocument.defaultView, "highlight",
|
||||
this.targetIsInAppMenu(aTarget),
|
||||
|
@ -1020,6 +1071,12 @@ this.UITour = {
|
|||
let alignment = "bottomcenter topright";
|
||||
this._addAnnotationPanelMutationObserver(tooltip);
|
||||
tooltip.openPopup(aAnchorEl, alignment);
|
||||
if (tooltip.state == "closed") {
|
||||
document.defaultView.addEventListener("endmodalstate", function endModalStateHandler() {
|
||||
document.defaultView.removeEventListener("endmodalstate", endModalStateHandler);
|
||||
tooltip.openPopup(aAnchorEl, alignment);
|
||||
}, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent showing a panel at an undefined position.
|
||||
|
@ -1083,7 +1140,7 @@ this.UITour = {
|
|||
} else if (aMenuName == "searchEngines") {
|
||||
this.getTarget(aWindow, "searchProvider").then(target => {
|
||||
openMenuButton(target.node);
|
||||
}).catch(Cu.reportError);
|
||||
}).catch(log.error);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1125,7 +1182,7 @@ this.UITour = {
|
|||
return;
|
||||
}
|
||||
hideMethod(win);
|
||||
}).then(null, Cu.reportError);
|
||||
}).catch(log.error);
|
||||
}
|
||||
});
|
||||
UITour.appMenuOpenForAnnotation.clear();
|
||||
|
@ -1197,7 +1254,7 @@ this.UITour = {
|
|||
this.sendPageCallback(aMessageManager, aCallbackID, appinfo);
|
||||
break;
|
||||
default:
|
||||
Cu.reportError("getConfiguration: Unknown configuration requested: " + aConfiguration);
|
||||
log.error("getConfiguration: Unknown configuration requested: " + aConfiguration);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
@ -1207,6 +1264,7 @@ this.UITour = {
|
|||
let window = aChromeWindow;
|
||||
let data = this.availableTargetsCache.get(window);
|
||||
if (data) {
|
||||
log.debug("getAvailableTargets: Using cached targets list", data.targets.join(","));
|
||||
this.sendPageCallback(aMessageManager, aCallbackID, data);
|
||||
return;
|
||||
}
|
||||
|
@ -1236,7 +1294,7 @@ this.UITour = {
|
|||
this.availableTargetsCache.set(window, data);
|
||||
this.sendPageCallback(aMessageManager, aCallbackID, data);
|
||||
}.bind(this)).catch(err => {
|
||||
Cu.reportError(err);
|
||||
log.error(err);
|
||||
this.sendPageCallback(aMessageManager, aCallbackID, {
|
||||
targets: [],
|
||||
});
|
||||
|
@ -1245,15 +1303,15 @@ this.UITour = {
|
|||
|
||||
addNavBarWidget: function (aTarget, aMessageManager, aCallbackID) {
|
||||
if (aTarget.node) {
|
||||
Cu.reportError("UITour: can't add a widget already present: " + data.target);
|
||||
log.error("UITour: can't add a widget already present: " + data.target);
|
||||
return;
|
||||
}
|
||||
if (!aTarget.allowAdd) {
|
||||
Cu.reportError("UITour: not allowed to add this widget: " + data.target);
|
||||
log.error("UITour: not allowed to add this widget: " + data.target);
|
||||
return;
|
||||
}
|
||||
if (!aTarget.widgetName) {
|
||||
Cu.reportError("UITour: can't add a widget without a widgetName property: " + data.target);
|
||||
log.error("UITour: can't add a widget without a widgetName property: " + data.target);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -28,6 +28,9 @@ skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
|
|||
skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
|
||||
[browser_UITour_annotation_size_attributes.js]
|
||||
skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly.
|
||||
[browser_UITour_modalDialog.js]
|
||||
run-if = os == "mac" # modal dialog disabling only working on OS X
|
||||
skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
|
||||
[browser_UITour_panel_close_annotation.js]
|
||||
skip-if = true # Disabled due to frequent failures, bugs 1026310 and 1032137
|
||||
[browser_UITour_registerPageID.js]
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
"use strict";
|
||||
|
||||
let gTestTab;
|
||||
let gContentAPI;
|
||||
let gContentWindow;
|
||||
let handleDialog;
|
||||
|
||||
// Modified from toolkit/components/passwordmgr/test/prompt_common.js
|
||||
var didDialog;
|
||||
|
||||
var timer; // keep in outer scope so it's not GC'd before firing
|
||||
function startCallbackTimer() {
|
||||
didDialog = false;
|
||||
|
||||
// Delay before the callback twiddles the prompt.
|
||||
const dialogDelay = 10;
|
||||
|
||||
// Use a timer to invoke a callback to twiddle the authentication dialog
|
||||
timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
timer.init(observer, dialogDelay, Ci.nsITimer.TYPE_ONE_SHOT);
|
||||
}
|
||||
|
||||
|
||||
var observer = SpecialPowers.wrapCallbackObject({
|
||||
QueryInterface : function (iid) {
|
||||
const interfaces = [Ci.nsIObserver,
|
||||
Ci.nsISupports, Ci.nsISupportsWeakReference];
|
||||
|
||||
if (!interfaces.some( function(v) { return iid.equals(v) } ))
|
||||
throw SpecialPowers.Components.results.NS_ERROR_NO_INTERFACE;
|
||||
return this;
|
||||
},
|
||||
|
||||
observe : function (subject, topic, data) {
|
||||
var doc = getDialogDoc();
|
||||
if (doc)
|
||||
handleDialog(doc);
|
||||
else
|
||||
startCallbackTimer(); // try again in a bit
|
||||
}
|
||||
});
|
||||
|
||||
function getDialogDoc() {
|
||||
// Find the <browser> which contains notifyWindow, by looking
|
||||
// through all the open windows and all the <browsers> in each.
|
||||
var wm = Cc["@mozilla.org/appshell/window-mediator;1"].
|
||||
getService(Ci.nsIWindowMediator);
|
||||
//var enumerator = wm.getEnumerator("navigator:browser");
|
||||
var enumerator = wm.getXULWindowEnumerator(null);
|
||||
|
||||
while (enumerator.hasMoreElements()) {
|
||||
var win = enumerator.getNext();
|
||||
var windowDocShell = win.QueryInterface(Ci.nsIXULWindow).docShell;
|
||||
|
||||
var containedDocShells = windowDocShell.getDocShellEnumerator(
|
||||
Ci.nsIDocShellTreeItem.typeChrome,
|
||||
Ci.nsIDocShell.ENUMERATE_FORWARDS);
|
||||
while (containedDocShells.hasMoreElements()) {
|
||||
// Get the corresponding document for this docshell
|
||||
var childDocShell = containedDocShells.getNext();
|
||||
// We don't want it if it's not done loading.
|
||||
if (childDocShell.busyFlags != Ci.nsIDocShell.BUSY_FLAGS_NONE)
|
||||
continue;
|
||||
var childDoc = childDocShell.QueryInterface(Ci.nsIDocShell)
|
||||
.contentViewer
|
||||
.DOMDocument;
|
||||
|
||||
//ok(true, "Got window: " + childDoc.location.href);
|
||||
if (childDoc.location.href == "chrome://global/content/commonDialog.xul")
|
||||
return childDoc;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Components.utils.import("resource:///modules/UITour.jsm");
|
||||
|
||||
function test() {
|
||||
UITourTest();
|
||||
}
|
||||
|
||||
|
||||
let tests = [
|
||||
taskify(function* test_modal_dialog_while_opening_tooltip(done) {
|
||||
let panelShown;
|
||||
let popup;
|
||||
|
||||
handleDialog = (doc) => {
|
||||
popup = document.getElementById("UITourTooltip");
|
||||
gContentAPI.showInfo("appMenu", "test title", "test text");
|
||||
doc.defaultView.setTimeout(function() {
|
||||
is(popup.state, "closed", "Popup shouldn't be shown while dialog is up");
|
||||
panelShown = promisePanelElementShown(window, popup);
|
||||
let dialog = doc.getElementById("commonDialog");
|
||||
dialog.acceptDialog();
|
||||
}, 1000);
|
||||
};
|
||||
startCallbackTimer();
|
||||
executeSoon(() => alert("test"));
|
||||
yield waitForConditionPromise(() => panelShown, "Timed out waiting for panel promise to be assigned", 100);
|
||||
yield panelShown;
|
||||
|
||||
yield hideInfoPromise();
|
||||
})
|
||||
];
|
|
@ -8,11 +8,11 @@ Cu.import("resource://gre/modules/Task.jsm");
|
|||
const SINGLE_TRY_TIMEOUT = 100;
|
||||
const NUMBER_OF_TRIES = 30;
|
||||
|
||||
function waitForConditionPromise(condition, timeoutMsg) {
|
||||
function waitForConditionPromise(condition, timeoutMsg, tryCount=NUMBER_OF_TRIES) {
|
||||
let defer = Promise.defer();
|
||||
let tries = 0;
|
||||
function checkCondition() {
|
||||
if (tries >= NUMBER_OF_TRIES) {
|
||||
if (tries >= tryCount) {
|
||||
defer.reject(timeoutMsg);
|
||||
}
|
||||
var conditionPassed;
|
||||
|
|
|
@ -244,7 +244,6 @@ browser.jar:
|
|||
skin/classic/browser/devtools/webconsole.png (../shared/devtools/images/webconsole.png)
|
||||
skin/classic/browser/devtools/webconsole@2x.png (../shared/devtools/images/webconsole@2x.png)
|
||||
skin/classic/browser/devtools/commandline.css (devtools/commandline.css)
|
||||
skin/classic/browser/devtools/highlighter.css (../shared/devtools/highlighter.css)
|
||||
skin/classic/browser/devtools/markup-view.css (../shared/devtools/markup-view.css)
|
||||
skin/classic/browser/devtools/editor-error.png (../shared/devtools/images/editor-error.png)
|
||||
skin/classic/browser/devtools/editor-breakpoint.png (../shared/devtools/images/editor-breakpoint.png)
|
||||
|
|
|
@ -102,6 +102,12 @@
|
|||
-moz-appearance: -moz-window-titlebar;
|
||||
}
|
||||
|
||||
@media (-moz-mac-yosemite-theme) {
|
||||
#main-window:not(:-moz-lwtheme) > #titlebar {
|
||||
-moz-appearance: -moz-mac-vibrancy-light;
|
||||
}
|
||||
}
|
||||
|
||||
#main-window:not([tabsintitlebar]) > #titlebar {
|
||||
height: 22px; /* The native titlebar on OS X is 22px tall. */
|
||||
}
|
||||
|
|
|
@ -182,6 +182,7 @@ browser.jar:
|
|||
skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
|
||||
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
|
||||
skin/classic/browser/downloads/buttons@2x.png (downloads/buttons@2x.png)
|
||||
* skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
|
||||
skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
|
||||
skin/classic/browser/downloads/download-glow-menuPanel@2x.png (downloads/download-glow-menuPanel@2x.png)
|
||||
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
|
||||
|
@ -361,7 +362,6 @@ browser.jar:
|
|||
skin/classic/browser/devtools/alerticon-warning@2x.png (../shared/devtools/images/alerticon-warning@2x.png)
|
||||
* skin/classic/browser/devtools/ruleview.css (../shared/devtools/ruleview.css)
|
||||
skin/classic/browser/devtools/commandline.css (devtools/commandline.css)
|
||||
skin/classic/browser/devtools/highlighter.css (../shared/devtools/highlighter.css)
|
||||
skin/classic/browser/devtools/markup-view.css (../shared/devtools/markup-view.css)
|
||||
skin/classic/browser/devtools/editor-error.png (../shared/devtools/images/editor-error.png)
|
||||
skin/classic/browser/devtools/editor-breakpoint.png (../shared/devtools/images/editor-breakpoint.png)
|
||||
|
|
|
@ -48,23 +48,22 @@
|
|||
display: none;
|
||||
}
|
||||
|
||||
.theme-dark #timeline-overview {
|
||||
border-bottom: 1px solid #000;
|
||||
.theme-dark #timeline-pane {
|
||||
border-top: 1px solid #000;
|
||||
}
|
||||
|
||||
.theme-light #timeline-overview {
|
||||
border-bottom: 1px solid #aaa;
|
||||
.theme-light #timeline-pane {
|
||||
border-top: 1px solid #aaa;
|
||||
}
|
||||
|
||||
.timeline-list-contents {
|
||||
.waterfall-list-contents {
|
||||
/* Hack: force hardware acceleration */
|
||||
transform: translateZ(1px);
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.timeline-header-ticks,
|
||||
.timeline-marker-waterfall {
|
||||
.waterfall-background-ticks {
|
||||
/* Background created on a <canvas> in js. */
|
||||
/* @see browser/devtools/timeline/widgets/waterfall.js */
|
||||
background-image: -moz-element(#waterfall-background);
|
||||
|
@ -72,76 +71,69 @@
|
|||
background-position: -1px center;
|
||||
}
|
||||
|
||||
.timeline-marker-waterfall {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.timeline-marker-container[is-spacer] {
|
||||
.waterfall-marker-container[is-spacer] {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.theme-dark .timeline-marker-container:not([is-spacer]):nth-child(2n) {
|
||||
.theme-dark .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
|
||||
background-color: rgba(255,255,255,0.03);
|
||||
}
|
||||
|
||||
.theme-light .timeline-marker-container:not([is-spacer]):nth-child(2n) {
|
||||
.theme-light .waterfall-marker-container:not([is-spacer]):nth-child(2n) {
|
||||
background-color: rgba(128,128,128,0.03);
|
||||
}
|
||||
|
||||
.theme-dark .timeline-marker-container:hover {
|
||||
.theme-dark .waterfall-marker-container:hover {
|
||||
background-color: rgba(255,255,255,0.1) !important;
|
||||
}
|
||||
|
||||
.theme-light .timeline-marker-container:hover {
|
||||
.theme-light .waterfall-marker-container:hover {
|
||||
background-color: rgba(128,128,128,0.1) !important;
|
||||
}
|
||||
|
||||
.timeline-header-sidebar,
|
||||
.timeline-marker-sidebar {
|
||||
.waterfall-marker-item {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.waterfall-sidebar {
|
||||
-moz-border-end: 1px solid;
|
||||
}
|
||||
|
||||
.theme-dark .timeline-header-sidebar,
|
||||
.theme-dark .timeline-marker-sidebar {
|
||||
.theme-dark .waterfall-sidebar {
|
||||
-moz-border-end-color: #000;
|
||||
}
|
||||
|
||||
.theme-light .timeline-header-sidebar,
|
||||
.theme-light .timeline-marker-sidebar {
|
||||
.theme-light .waterfall-sidebar {
|
||||
-moz-border-end-color: #aaa;
|
||||
}
|
||||
|
||||
.timeline-header-sidebar {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.timeline-marker-sidebar {
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.timeline-marker-container:hover > .timeline-marker-sidebar {
|
||||
.waterfall-marker-container:hover > .waterfall-sidebar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.timeline-header-tick {
|
||||
.waterfall-header-name {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.waterfall-header-tick {
|
||||
width: 100px;
|
||||
font-size: 9px;
|
||||
transform-origin: left center;
|
||||
}
|
||||
|
||||
.theme-dark .timeline-header-tick {
|
||||
.theme-dark .waterfall-header-tick {
|
||||
color: #a9bacb;
|
||||
}
|
||||
|
||||
.theme-light .timeline-header-tick {
|
||||
.theme-light .waterfall-header-tick {
|
||||
color: #292e33;
|
||||
}
|
||||
|
||||
.timeline-header-tick:not(:first-child) {
|
||||
.waterfall-header-tick:not(:first-child) {
|
||||
-moz-margin-start: -100px !important; /* Don't affect layout. */
|
||||
}
|
||||
|
||||
.timeline-marker-bullet {
|
||||
.waterfall-marker-bullet {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
-moz-margin-start: 8px;
|
||||
|
@ -150,9 +142,13 @@
|
|||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.timeline-marker-bar {
|
||||
margin-top: 4px;
|
||||
margin-bottom: 4px;
|
||||
.waterfall-marker-name {
|
||||
font-size: 95%;
|
||||
padding-bottom: 1px !important;
|
||||
}
|
||||
|
||||
.waterfall-marker-bar {
|
||||
height: 9px;
|
||||
border: 1px solid;
|
||||
border-radius: 1px;
|
||||
transform-origin: left center;
|
||||
|
|
|
@ -938,10 +938,6 @@
|
|||
|
||||
/* Line graph widget */
|
||||
|
||||
.line-graph-widget-canvas {
|
||||
background: #0088cc;
|
||||
}
|
||||
|
||||
.line-graph-widget-gutter {
|
||||
position: absolute;
|
||||
background: rgba(255,255,255,0.75);
|
||||
|
@ -957,7 +953,6 @@
|
|||
position: absolute;
|
||||
width: 100%;
|
||||
border-top: 1px solid;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.line-graph-widget-gutter-line[type=maximum] {
|
||||
|
@ -975,7 +970,6 @@
|
|||
.line-graph-widget-tooltip {
|
||||
position: absolute;
|
||||
background: rgba(255,255,255,0.75);
|
||||
box-shadow: 0 2px 1px rgba(0,0,0,0.1);
|
||||
border-radius: 2px;
|
||||
line-height: 15px;
|
||||
-moz-padding-start: 6px;
|
||||
|
@ -986,7 +980,7 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip::before {
|
||||
.line-graph-widget-tooltip[with-arrows=true]::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-top: 3px solid transparent;
|
||||
|
@ -994,26 +988,38 @@
|
|||
top: calc(50% - 3px);
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[arrow=start]::before {
|
||||
.line-graph-widget-tooltip[arrow=start][with-arrows=true]::before {
|
||||
-moz-border-end: 3px solid rgba(255,255,255,0.75);
|
||||
left: -3px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[arrow=end]::before {
|
||||
.line-graph-widget-tooltip[arrow=end][with-arrows=true]::before {
|
||||
-moz-border-start: 3px solid rgba(255,255,255,0.75);
|
||||
right: -3px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=maximum] {
|
||||
left: calc(10px + 6px);
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=minimum] {
|
||||
left: calc(10px + 6px);
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=average] {
|
||||
right: 6px;
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=maximum][with-arrows=true] {
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=minimum][with-arrows=true] {
|
||||
left: 14px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip[type=average][with-arrows=true] {
|
||||
right: 4px;
|
||||
}
|
||||
|
||||
.line-graph-widget-tooltip > [text=info] {
|
||||
|
|
|
@ -275,7 +275,6 @@ browser.jar:
|
|||
skin/classic/browser/devtools/command-console@2x.png (../shared/devtools/images/command-console@2x.png)
|
||||
skin/classic/browser/devtools/command-eyedropper.png (../shared/devtools/images/command-eyedropper.png)
|
||||
skin/classic/browser/devtools/command-eyedropper@2x.png (../shared/devtools/images/command-eyedropper@2x.png)
|
||||
skin/classic/browser/devtools/highlighter.css (../shared/devtools/highlighter.css)
|
||||
skin/classic/browser/devtools/markup-view.css (../shared/devtools/markup-view.css)
|
||||
skin/classic/browser/devtools/editor-error.png (../shared/devtools/images/editor-error.png)
|
||||
skin/classic/browser/devtools/editor-breakpoint.png (../shared/devtools/images/editor-breakpoint.png)
|
||||
|
@ -712,7 +711,6 @@ browser.jar:
|
|||
skin/classic/aero/browser/devtools/alerticon-warning@2x.png (../shared/devtools/images/alerticon-warning@2x.png)
|
||||
* skin/classic/aero/browser/devtools/ruleview.css (../shared/devtools/ruleview.css)
|
||||
skin/classic/aero/browser/devtools/commandline.css (devtools/commandline.css)
|
||||
skin/classic/aero/browser/devtools/highlighter.css (../shared/devtools/highlighter.css)
|
||||
skin/classic/aero/browser/devtools/markup-view.css (../shared/devtools/markup-view.css)
|
||||
skin/classic/aero/browser/devtools/editor-error.png (../shared/devtools/images/editor-error.png)
|
||||
skin/classic/aero/browser/devtools/editor-breakpoint.png (../shared/devtools/images/editor-breakpoint.png)
|
||||
|
|
|
@ -8823,6 +8823,16 @@ nsGlobalWindow::LeaveModalState()
|
|||
nsGlobalWindow *inner = topWin->GetCurrentInnerWindowInternal();
|
||||
if (inner)
|
||||
inner->mLastDialogQuitTime = TimeStamp::Now();
|
||||
|
||||
if (topWin->mModalStateDepth == 0) {
|
||||
nsCOMPtr<nsIDOMEvent> event;
|
||||
NS_NewDOMEvent(getter_AddRefs(event), topWin, nullptr, nullptr);
|
||||
event->InitEvent(NS_LITERAL_STRING("endmodalstate"), true, false);
|
||||
event->SetTrusted(true);
|
||||
event->GetInternalNSEvent()->mFlags.mOnlyChromeDispatch = true;
|
||||
bool dummy;
|
||||
topWin->DispatchEvent(event, &dummy);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
|
|
|
@ -433,6 +433,10 @@ nsGeolocationRequest::GetElement(nsIDOMElement * *aRequestingElement)
|
|||
NS_IMETHODIMP
|
||||
nsGeolocationRequest::Cancel()
|
||||
{
|
||||
if (mLocator->ClearPendingRequest(this)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NotifyError(nsIDOMGeoPositionError::PERMISSION_DENIED);
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -442,6 +446,10 @@ nsGeolocationRequest::Allow(JS::HandleValue aChoices)
|
|||
{
|
||||
MOZ_ASSERT(aChoices.isUndefined());
|
||||
|
||||
if (mLocator->ClearPendingRequest(this)) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Kick off the geo device, if it isn't already running
|
||||
nsRefPtr<nsGeolocationService> gs = nsGeolocationService::GetGeolocationService();
|
||||
nsresult rv = gs->StartDevice(GetPrincipal());
|
||||
|
@ -1304,6 +1312,28 @@ Geolocation::NotifyError(uint16_t aErrorCode)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
bool
|
||||
Geolocation::IsAlreadyCleared(nsGeolocationRequest* aRequest)
|
||||
{
|
||||
for (uint32_t i = 0, length = mClearedWatchIDs.Length(); i < length; ++i) {
|
||||
if (mClearedWatchIDs[i] == aRequest->WatchId()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
Geolocation::ClearPendingRequest(nsGeolocationRequest* aRequest)
|
||||
{
|
||||
if (aRequest->IsWatch() && this->IsAlreadyCleared(aRequest)) {
|
||||
this->NotifyAllowedRequest(aRequest);
|
||||
this->ClearWatch(aRequest->WatchId());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void
|
||||
Geolocation::GetCurrentPosition(PositionCallback& aCallback,
|
||||
PositionErrorCallback* aErrorCallback,
|
||||
|
@ -1489,10 +1519,15 @@ Geolocation::ClearWatch(int32_t aWatchId)
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
if (!mClearedWatchIDs.Contains(aWatchId)) {
|
||||
mClearedWatchIDs.AppendElement(aWatchId);
|
||||
}
|
||||
|
||||
for (uint32_t i = 0, length = mWatchingCallbacks.Length(); i < length; ++i) {
|
||||
if (mWatchingCallbacks[i]->WatchId() == aWatchId) {
|
||||
mWatchingCallbacks[i]->Shutdown();
|
||||
RemoveRequest(mWatchingCallbacks[i]);
|
||||
mClearedWatchIDs.RemoveElement(aWatchId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -154,6 +154,10 @@ public:
|
|||
// Remove request from all callbacks arrays
|
||||
void RemoveRequest(nsGeolocationRequest* request);
|
||||
|
||||
// Check if there is already ClearWatch called for current
|
||||
// request & clear if yes
|
||||
bool ClearPendingRequest(nsGeolocationRequest* aRequest);
|
||||
|
||||
// Shutting down.
|
||||
void Shutdown();
|
||||
|
||||
|
@ -185,6 +189,9 @@ private:
|
|||
nsresult GetCurrentPositionReady(nsGeolocationRequest* aRequest);
|
||||
nsresult WatchPositionReady(nsGeolocationRequest* aRequest);
|
||||
|
||||
// Check if clearWatch is already called
|
||||
bool IsAlreadyCleared(nsGeolocationRequest* aRequest);
|
||||
|
||||
// Two callback arrays. The first |mPendingCallbacks| holds objects for only
|
||||
// one callback and then they are released/removed from the array. The second
|
||||
// |mWatchingCallbacks| holds objects until the object is explictly removed or
|
||||
|
@ -208,6 +215,9 @@ private:
|
|||
|
||||
// Pending requests are used when the service is not ready
|
||||
nsTArray<nsRefPtr<nsGeolocationRequest> > mPendingRequests;
|
||||
|
||||
// Array containing already cleared watch IDs
|
||||
nsTArray<int32_t> mClearedWatchIDs;
|
||||
};
|
||||
|
||||
class PositionError MOZ_FINAL : public nsIDOMGeoPositionError,
|
||||
|
|
|
@ -17,6 +17,8 @@ skip-if = buildapp == 'b2g'
|
|||
skip-if = buildapp == 'b2g'
|
||||
[test_clearWatch.html]
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
|
||||
[test_clearWatchBeforeAllowing.html]
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
|
||||
[test_clearWatch_invalid.html]
|
||||
skip-if = buildapp == 'b2g'
|
||||
[test_errorcheck.html]
|
||||
|
@ -47,4 +49,4 @@ skip-if = buildapp == 'b2g' || toolkit == 'android' || e10s #TIMED_OUT
|
|||
[test_windowClose.html]
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
|
||||
[test_worseAccuracyDoesNotBlockCallback.html]
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
|
||||
skip-if = buildapp == 'b2g' || toolkit == 'android' #TIMED_OUT
|
|
@ -0,0 +1,59 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
https://bugzilla.mozilla.org/show_bug.cgi?id=886026
|
||||
-->
|
||||
<head>
|
||||
<title>Test for getCurrentPosition </title>
|
||||
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script type="text/javascript" src="geolocation_common.js"></script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
|
||||
</head>
|
||||
<body>
|
||||
<a target="_blank"
|
||||
href="https://bugzilla.mozilla.org/show_bug.cgi?id=886026">Mozilla Bug 886026</a>
|
||||
<p id="display"></p>
|
||||
<div id="content" style="display: none">
|
||||
|
||||
</div>
|
||||
<pre id="test">
|
||||
<script class="testbody" type="text/javascript">
|
||||
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
resume_geolocationProvider(function() {
|
||||
force_prompt(true, run_test);
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
var successCallbackCalled = false,
|
||||
errorCallbackCalled = false;
|
||||
|
||||
var watchId = navigator.geolocation.watchPosition(
|
||||
function(pos) {
|
||||
successCallbackCalled = true;
|
||||
}, function(err) {
|
||||
errorCallbackCalled = true;
|
||||
}
|
||||
);
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
function(pos) {
|
||||
SimpleTest.executeSoon(function() {
|
||||
ok(successCallbackCalled == false,
|
||||
"getCurrentPosition : Success callback should not have been called");
|
||||
|
||||
ok(errorCallbackCalled == false,
|
||||
"getCurrentPosition : Error callback should not have been called");
|
||||
|
||||
SimpleTest.finish();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
navigator.geolocation.clearWatch(watchId);
|
||||
}
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|
|
@ -16,6 +16,7 @@
|
|||
#include "nsIXULDocument.h"
|
||||
#include "nsIXULTemplateBuilder.h"
|
||||
#include "nsCSSFrameConstructor.h"
|
||||
#include "nsGlobalWindow.h"
|
||||
#include "nsLayoutUtils.h"
|
||||
#include "nsViewManager.h"
|
||||
#include "nsIComponentManager.h"
|
||||
|
@ -1594,18 +1595,18 @@ nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup)
|
|||
if (!baseWin)
|
||||
return false;
|
||||
|
||||
nsCOMPtr<nsIDocShellTreeItem> root;
|
||||
dsti->GetRootTreeItem(getter_AddRefs(root));
|
||||
if (!root) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIDOMWindow> rootWin = root->GetWindow();
|
||||
|
||||
// chrome shells can always open popups, but other types of shells can only
|
||||
// open popups when they are focused and visible
|
||||
if (dsti->ItemType() != nsIDocShellTreeItem::typeChrome) {
|
||||
// only allow popups in active windows
|
||||
nsCOMPtr<nsIDocShellTreeItem> root;
|
||||
dsti->GetRootTreeItem(getter_AddRefs(root));
|
||||
if (!root) {
|
||||
return false;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsIDOMWindow> rootWin = root->GetWindow();
|
||||
|
||||
nsIFocusManager* fm = nsFocusManager::GetFocusManager();
|
||||
if (!fm || !rootWin)
|
||||
return false;
|
||||
|
@ -1630,6 +1631,15 @@ nsXULPopupManager::MayShowPopup(nsMenuPopupFrame* aPopup)
|
|||
return false;
|
||||
}
|
||||
|
||||
#ifdef XP_MACOSX
|
||||
if (rootWin) {
|
||||
nsGlobalWindow *globalWin = static_cast<nsGlobalWindow *>(rootWin.get());
|
||||
if (globalWin->IsInModalState()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// cannot open a popup that is a submenu of a menupopup that isn't open.
|
||||
nsMenuFrame* menuFrame = do_QueryFrame(aPopup->GetParent());
|
||||
if (menuFrame) {
|
||||
|
|
|
@ -529,6 +529,7 @@ public class BrowserApp extends GeckoApp
|
|||
public void run() {
|
||||
final TabHistoryFragment fragment = TabHistoryFragment.newInstance(historyPageList, toIndex);
|
||||
final FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
GeckoAppShell.vibrateOnHapticFeedbackEnabled(getResources().getInteger(R.integer.long_press_vibrate_msec));
|
||||
fragment.show(R.id.tab_history_panel, fragmentManager.beginTransaction(), TAB_HISTORY_FRAGMENT_TAG);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1415,6 +1415,13 @@ public class GeckoAppShell
|
|||
return (Vibrator) layerView.getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
}
|
||||
|
||||
// Vibrate only if haptic feedback is enabled.
|
||||
public static void vibrateOnHapticFeedbackEnabled(long milliseconds) {
|
||||
if (Settings.System.getInt(getContext().getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 0) > 0) {
|
||||
vibrate(milliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
@WrapElementForJNI(stubName = "Vibrate1")
|
||||
public static void vibrate(long milliseconds) {
|
||||
sVibrationEndTime = System.nanoTime() + milliseconds * 1000000;
|
||||
|
|
|
@ -8,5 +8,6 @@
|
|||
<integer name="number_of_top_sites">6</integer>
|
||||
<integer name="number_of_top_sites_cols">2</integer>
|
||||
<integer name="max_icon_grid_columns">4</integer>
|
||||
<integer name="long_press_vibrate_msec">100</integer>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -11,7 +11,7 @@ import android.graphics.Canvas;
|
|||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
|
@ -33,8 +33,8 @@ public class TabStripView extends TwoWayView {
|
|||
private static final String LOGTAG = "GeckoTabStrip";
|
||||
|
||||
private static final int ANIM_TIME_MS = 200;
|
||||
private static final AccelerateDecelerateInterpolator ANIM_INTERPOLATOR =
|
||||
new AccelerateDecelerateInterpolator();
|
||||
private static final DecelerateInterpolator ANIM_INTERPOLATOR =
|
||||
new DecelerateInterpolator();
|
||||
|
||||
private final TabStripAdapter adapter;
|
||||
private final Drawable divider;
|
||||
|
|
|
@ -3362,9 +3362,9 @@ nsDownload::FixTargetPermissions()
|
|||
rv = infoService->GetPropertyAsUint32(NS_LITERAL_STRING("umask"),
|
||||
&gUserUmask);
|
||||
if (NS_SUCCEEDED(rv)) {
|
||||
rv = target->SetPermissions(0666 & ~gUserUmask);
|
||||
(void)target->SetPermissions(0666 & ~gUserUmask);
|
||||
}
|
||||
return rv;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
|
|
|
@ -18,6 +18,7 @@ const PREF_ENABLED = [ "autocomplete.enabled", true ];
|
|||
const PREF_AUTOFILL = [ "autoFill", true ];
|
||||
const PREF_AUTOFILL_TYPED = [ "autoFill.typed", true ];
|
||||
const PREF_AUTOFILL_SEARCHENGINES = [ "autoFill.searchEngines", true ];
|
||||
const PREF_RESTYLESEARCHES = [ "restyleSearches", false ];
|
||||
const PREF_DELAY = [ "delay", 50 ];
|
||||
const PREF_BEHAVIOR = [ "matchBehavior", MATCH_BOUNDARY_ANYWHERE ];
|
||||
const PREF_FILTER_JS = [ "filter.javascript", true ];
|
||||
|
@ -384,6 +385,7 @@ XPCOMUtils.defineLazyGetter(this, "Prefs", () => {
|
|||
store.autofill = prefs.get(...PREF_AUTOFILL);
|
||||
store.autofillTyped = prefs.get(...PREF_AUTOFILL_TYPED);
|
||||
store.autofillSearchEngines = prefs.get(...PREF_AUTOFILL_SEARCHENGINES);
|
||||
store.restyleSearches = prefs.get(...PREF_RESTYLESEARCHES);
|
||||
store.delay = prefs.get(...PREF_DELAY);
|
||||
store.matchBehavior = prefs.get(...PREF_BEHAVIOR);
|
||||
store.filterJavaScript = prefs.get(...PREF_FILTER_JS);
|
||||
|
@ -1109,7 +1111,7 @@ Search.prototype = {
|
|||
}
|
||||
|
||||
// Restyle past searches, unless they are bookmarks or special results.
|
||||
if (match.style == "favicon") {
|
||||
if (Prefs.restyleSearches && match.style == "favicon") {
|
||||
this._maybeRestyleSearchMatch(match);
|
||||
}
|
||||
|
||||
|
|
|
@ -20,31 +20,6 @@
|
|||
#include "GeckoProfiler.h"
|
||||
|
||||
#define BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH 32
|
||||
#define RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH 10
|
||||
// Threshold to expire old bookmarks if the initial cache size is exceeded.
|
||||
#define RECENT_BOOKMARKS_THRESHOLD PRTime((int64_t)1 * 60 * PR_USEC_PER_SEC)
|
||||
|
||||
#define BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(_itemId_) \
|
||||
mUncachableBookmarks.PutEntry(_itemId_); \
|
||||
mRecentBookmarksCache.RemoveEntry(_itemId_)
|
||||
|
||||
#define END_CRITICAL_BOOKMARK_CACHE_SECTION(_itemId_) \
|
||||
MOZ_ASSERT(!mRecentBookmarksCache.GetEntry(_itemId_)); \
|
||||
MOZ_ASSERT(mUncachableBookmarks.GetEntry(_itemId_)); \
|
||||
mUncachableBookmarks.RemoveEntry(_itemId_)
|
||||
|
||||
#define ADD_TO_BOOKMARK_CACHE(_itemId_, _data_) \
|
||||
PR_BEGIN_MACRO \
|
||||
ExpireNonrecentBookmarks(&mRecentBookmarksCache); \
|
||||
if (!mUncachableBookmarks.GetEntry(_itemId_)) { \
|
||||
BookmarkKeyClass* key = mRecentBookmarksCache.PutEntry(_itemId_); \
|
||||
if (key) { \
|
||||
key->bookmark = _data_; \
|
||||
} \
|
||||
} \
|
||||
PR_END_MACRO
|
||||
|
||||
#define TOPIC_PLACES_MAINTENANCE "places-maintenance-finished"
|
||||
|
||||
using namespace mozilla;
|
||||
|
||||
|
@ -155,46 +130,6 @@ private:
|
|||
DataType mData;
|
||||
};
|
||||
|
||||
static PLDHashOperator
|
||||
ExpireNonrecentBookmarksCallback(BookmarkKeyClass* aKey,
|
||||
void* userArg)
|
||||
{
|
||||
int64_t* threshold = reinterpret_cast<int64_t*>(userArg);
|
||||
if (aKey->creationTime < *threshold) {
|
||||
return PL_DHASH_REMOVE;
|
||||
}
|
||||
return PL_DHASH_NEXT;
|
||||
}
|
||||
|
||||
static void
|
||||
ExpireNonrecentBookmarks(nsTHashtable<BookmarkKeyClass>* hashTable)
|
||||
{
|
||||
if (hashTable->Count() > RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH) {
|
||||
int64_t threshold = PR_Now() - RECENT_BOOKMARKS_THRESHOLD;
|
||||
(void)hashTable->EnumerateEntries(ExpireNonrecentBookmarksCallback,
|
||||
reinterpret_cast<void*>(&threshold));
|
||||
}
|
||||
}
|
||||
|
||||
static PLDHashOperator
|
||||
ExpireRecentBookmarksByParentCallback(BookmarkKeyClass* aKey,
|
||||
void* userArg)
|
||||
{
|
||||
int64_t* parentId = reinterpret_cast<int64_t*>(userArg);
|
||||
if (aKey->bookmark.parentId == *parentId) {
|
||||
return PL_DHASH_REMOVE;
|
||||
}
|
||||
return PL_DHASH_NEXT;
|
||||
}
|
||||
|
||||
static void
|
||||
ExpireRecentBookmarksByParent(nsTHashtable<BookmarkKeyClass>* hashTable,
|
||||
int64_t aParentId)
|
||||
{
|
||||
(void)hashTable->EnumerateEntries(ExpireRecentBookmarksByParentCallback,
|
||||
reinterpret_cast<void*>(&aParentId));
|
||||
}
|
||||
|
||||
} // Anonymous namespace.
|
||||
|
||||
|
||||
|
@ -210,8 +145,6 @@ nsNavBookmarks::nsNavBookmarks()
|
|||
, mBatching(false)
|
||||
, mBookmarkToKeywordHash(BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH)
|
||||
, mBookmarkToKeywordHashInitialized(false)
|
||||
, mRecentBookmarksCache(RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH)
|
||||
, mUncachableBookmarks(RECENT_BOOKMARKS_INITIAL_CACHE_LENGTH)
|
||||
{
|
||||
NS_ASSERTION(!gBookmarksService,
|
||||
"Attempting to create two instances of the service!");
|
||||
|
@ -245,7 +178,6 @@ nsNavBookmarks::Init()
|
|||
|
||||
nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
|
||||
if (os) {
|
||||
(void)os->AddObserver(this, TOPIC_PLACES_MAINTENANCE, true);
|
||||
(void)os->AddObserver(this, TOPIC_PLACES_SHUTDOWN, true);
|
||||
(void)os->AddObserver(this, TOPIC_PLACES_CONNECTION_CLOSED, true);
|
||||
}
|
||||
|
@ -346,10 +278,6 @@ nsNavBookmarks::AdjustIndices(int64_t aFolderId,
|
|||
NS_ASSERTION(aStartIndex >= 0 && aEndIndex <= INT32_MAX &&
|
||||
aStartIndex <= aEndIndex, "Bad indices");
|
||||
|
||||
// Expire all cached items for this parent, since all positions are going to
|
||||
// change.
|
||||
ExpireRecentBookmarksByParent(&mRecentBookmarksCache, aFolderId);
|
||||
|
||||
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
|
||||
"UPDATE moz_bookmarks SET position = position + :delta "
|
||||
"WHERE parent = :parent "
|
||||
|
@ -552,8 +480,6 @@ nsNavBookmarks::InsertBookmarkInDB(int64_t aPlaceId,
|
|||
bookmark.parentGuid = aParentGuid;
|
||||
bookmark.grandParentId = aGrandParentId;
|
||||
|
||||
ADD_TO_BOOKMARK_CACHE(*_itemId, bookmark);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -684,8 +610,6 @@ nsNavBookmarks::RemoveItem(int64_t aItemId)
|
|||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
|
||||
"DELETE FROM moz_bookmarks WHERE id = :item_id"
|
||||
);
|
||||
|
@ -712,8 +636,6 @@ nsNavBookmarks::RemoveItem(int64_t aItemId)
|
|||
rv = transaction.Commit();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
nsCOMPtr<nsIURI> uri;
|
||||
if (bookmark.type == TYPE_BOOKMARK) {
|
||||
// If not a tag, recalculate frecency for this entry, since it changed.
|
||||
|
@ -1154,8 +1076,6 @@ nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId)
|
|||
foldersToRemove.Append(',');
|
||||
foldersToRemove.AppendInt(child.id);
|
||||
}
|
||||
|
||||
BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(child.id);
|
||||
}
|
||||
|
||||
// Delete items from the database now.
|
||||
|
@ -1202,7 +1122,6 @@ nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId)
|
|||
rv = UpdateKeywordsHashForRemovedBookmark(child.id);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
END_CRITICAL_BOOKMARK_CACHE_SECTION(child.id);
|
||||
}
|
||||
|
||||
rv = transaction.Commit();
|
||||
|
@ -1351,8 +1270,6 @@ nsNavBookmarks::MoveItem(int64_t aItemId, int64_t aNewParent, int32_t aIndex)
|
|||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
{
|
||||
// Update parent and position.
|
||||
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
|
||||
|
@ -1381,8 +1298,6 @@ nsNavBookmarks::MoveItem(int64_t aItemId, int64_t aNewParent, int32_t aIndex)
|
|||
rv = transaction.Commit();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
|
||||
nsINavBookmarkObserver,
|
||||
OnItemMoved(bookmark.id,
|
||||
|
@ -1401,14 +1316,6 @@ nsresult
|
|||
nsNavBookmarks::FetchItemInfo(int64_t aItemId,
|
||||
BookmarkData& _bookmark)
|
||||
{
|
||||
// Check if the requested id is in the recent cache and avoid the database
|
||||
// lookup if so. Invalidate the cache after getting data if requested.
|
||||
BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId);
|
||||
if (key) {
|
||||
_bookmark = key->bookmark;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// LEFT JOIN since not all bookmarks have an associated place.
|
||||
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
|
||||
"SELECT b.id, h.url, b.title, b.position, b.fk, b.parent, b.type, "
|
||||
|
@ -1471,8 +1378,6 @@ nsNavBookmarks::FetchItemInfo(int64_t aItemId,
|
|||
_bookmark.grandParentId = -1;
|
||||
}
|
||||
|
||||
ADD_TO_BOOKMARK_CACHE(aItemId, _bookmark);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -1507,16 +1412,6 @@ nsNavBookmarks::SetItemDateInternal(enum BookmarkDate aDateType,
|
|||
rv = stmt->Execute();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Update the cache entry, if needed.
|
||||
BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId);
|
||||
if (key) {
|
||||
if (aDateType == DATE_ADDED) {
|
||||
key->bookmark.dateAdded = aValue;
|
||||
}
|
||||
// Set lastModified in both cases.
|
||||
key->bookmark.lastModified = aValue;
|
||||
}
|
||||
|
||||
// note, we are not notifying the observers
|
||||
// that the item has changed.
|
||||
|
||||
|
@ -1650,18 +1545,6 @@ nsNavBookmarks::SetItemTitle(int64_t aItemId, const nsACString& aTitle)
|
|||
rv = statement->Execute();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Update the cache entry, if needed.
|
||||
BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aItemId);
|
||||
if (key) {
|
||||
if (title.IsVoid()) {
|
||||
key->bookmark.title.SetIsVoid(true);
|
||||
}
|
||||
else {
|
||||
key->bookmark.title.Assign(title);
|
||||
}
|
||||
key->bookmark.lastModified = bookmark.lastModified;
|
||||
}
|
||||
|
||||
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
|
||||
nsINavBookmarkObserver,
|
||||
OnItemChanged(bookmark.id,
|
||||
|
@ -2109,8 +1992,6 @@ nsNavBookmarks::ChangeBookmarkURI(int64_t aBookmarkId, nsIURI* aNewURI)
|
|||
if (!newPlaceId)
|
||||
return NS_ERROR_INVALID_ARG;
|
||||
|
||||
BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
nsCOMPtr<mozIStorageStatement> statement = mDB->GetStatement(
|
||||
"UPDATE moz_bookmarks SET fk = :page_id, lastModified = :date "
|
||||
"WHERE id = :item_id "
|
||||
|
@ -2132,8 +2013,6 @@ nsNavBookmarks::ChangeBookmarkURI(int64_t aBookmarkId, nsIURI* aNewURI)
|
|||
rv = transaction.Commit();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
rv = history->UpdateFrecency(newPlaceId);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
|
@ -2345,8 +2224,6 @@ nsNavBookmarks::SetItemIndex(int64_t aItemId, int32_t aNewIndex)
|
|||
// Check the parent's guid is the expected one.
|
||||
MOZ_ASSERT(bookmark.parentGuid == folderGuid);
|
||||
|
||||
BEGIN_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
|
||||
"UPDATE moz_bookmarks SET position = :item_index WHERE id = :item_id"
|
||||
);
|
||||
|
@ -2361,8 +2238,6 @@ nsNavBookmarks::SetItemIndex(int64_t aItemId, int32_t aNewIndex)
|
|||
rv = stmt->Execute();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
END_CRITICAL_BOOKMARK_CACHE_SECTION(bookmark.id);
|
||||
|
||||
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
|
||||
nsINavBookmarkObserver,
|
||||
OnItemMoved(bookmark.id,
|
||||
|
@ -2495,12 +2370,6 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
|
|||
rv = transaction.Commit();
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Update the cache entry, if needed.
|
||||
BookmarkKeyClass* key = mRecentBookmarksCache.GetEntry(aBookmarkId);
|
||||
if (key) {
|
||||
key->bookmark.lastModified = bookmark.lastModified;
|
||||
}
|
||||
|
||||
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
|
||||
nsINavBookmarkObserver,
|
||||
OnItemChanged(bookmark.id,
|
||||
|
@ -2758,12 +2627,7 @@ nsNavBookmarks::Observe(nsISupports *aSubject, const char *aTopic,
|
|||
{
|
||||
NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
|
||||
|
||||
if (strcmp(aTopic, TOPIC_PLACES_MAINTENANCE) == 0) {
|
||||
// Maintenance can execute direct writes to the database, thus clear all
|
||||
// the cached bookmarks.
|
||||
mRecentBookmarksCache.Clear();
|
||||
}
|
||||
else if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) {
|
||||
if (strcmp(aTopic, TOPIC_PLACES_SHUTDOWN) == 0) {
|
||||
// Stop Observing annotations.
|
||||
nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
|
||||
if (annosvc) {
|
||||
|
|
|
@ -61,24 +61,6 @@ namespace places {
|
|||
typedef void (nsNavBookmarks::*ItemVisitMethod)(const ItemVisitData&);
|
||||
typedef void (nsNavBookmarks::*ItemChangeMethod)(const ItemChangeData&);
|
||||
|
||||
class BookmarkKeyClass : public nsTrimInt64HashKey
|
||||
{
|
||||
public:
|
||||
explicit BookmarkKeyClass(const int64_t* aItemId)
|
||||
: nsTrimInt64HashKey(aItemId)
|
||||
, creationTime(PR_Now())
|
||||
{
|
||||
}
|
||||
BookmarkKeyClass(const BookmarkKeyClass& aOther)
|
||||
: nsTrimInt64HashKey(aOther)
|
||||
, creationTime(PR_Now())
|
||||
{
|
||||
NS_NOTREACHED("Do not call me!");
|
||||
}
|
||||
BookmarkData bookmark;
|
||||
PRTime creationTime;
|
||||
};
|
||||
|
||||
enum BookmarkDate {
|
||||
DATE_ADDED = 0
|
||||
, LAST_MODIFIED
|
||||
|
@ -124,7 +106,6 @@ public:
|
|||
}
|
||||
|
||||
typedef mozilla::places::BookmarkData BookmarkData;
|
||||
typedef mozilla::places::BookmarkKeyClass BookmarkKeyClass;
|
||||
typedef mozilla::places::ItemVisitData ItemVisitData;
|
||||
typedef mozilla::places::ItemChangeData ItemChangeData;
|
||||
typedef mozilla::places::BookmarkStatementId BookmarkStatementId;
|
||||
|
@ -455,18 +436,6 @@ private:
|
|||
* Uri to test.
|
||||
*/
|
||||
nsresult UpdateKeywordsHashForRemovedBookmark(int64_t aItemId);
|
||||
|
||||
/**
|
||||
* Cache for the last fetched BookmarkData entries.
|
||||
* This is used to speed up repeated requests to the same item id.
|
||||
*/
|
||||
nsTHashtable<BookmarkKeyClass> mRecentBookmarksCache;
|
||||
|
||||
/**
|
||||
* Tracks bookmarks in the cache critical path. Items should not be
|
||||
* added to the cache till they are removed from this hash.
|
||||
*/
|
||||
nsTHashtable<nsTrimInt64HashKey> mUncachableBookmarks;
|
||||
};
|
||||
|
||||
#endif // nsNavBookmarks_h_
|
||||
|
|
|
@ -3508,9 +3508,23 @@ nsNavHistoryFolderResultNode::OnItemAdded(int64_t aItemId,
|
|||
{
|
||||
NS_ASSERTION(aParentFolder == mItemId, "Got wrong bookmark update");
|
||||
|
||||
RESTART_AND_RETURN_IF_ASYNC_PENDING();
|
||||
|
||||
{
|
||||
uint32_t index;
|
||||
nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
|
||||
// Bug 1097528.
|
||||
// It's possible our result registered due to a previous notification, for
|
||||
// example the Library left pane could have refreshed and replaced the
|
||||
// right pane as a consequence. In such a case our contents are already
|
||||
// up-to-date. That's OK.
|
||||
if (node)
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
|
||||
(mParent && mParent->mOptions->ExcludeItems()) ||
|
||||
mOptions->ExcludeItems();
|
||||
(mParent && mParent->mOptions->ExcludeItems()) ||
|
||||
mOptions->ExcludeItems();
|
||||
|
||||
// here, try to do something reasonable if the bookmark service gives us
|
||||
// a bogus index.
|
||||
|
@ -3526,8 +3540,6 @@ nsNavHistoryFolderResultNode::OnItemAdded(int64_t aItemId,
|
|||
aIndex = mChildren.Count();
|
||||
}
|
||||
|
||||
RESTART_AND_RETURN_IF_ASYNC_PENDING();
|
||||
|
||||
nsresult rv;
|
||||
|
||||
// Check for query URIs, which are bookmarks, but treated as containers
|
||||
|
@ -3606,23 +3618,23 @@ nsNavHistoryFolderResultNode::OnItemRemoved(int64_t aItemId,
|
|||
|
||||
RESTART_AND_RETURN_IF_ASYNC_PENDING();
|
||||
|
||||
bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
|
||||
(mParent && mParent->mOptions->ExcludeItems()) ||
|
||||
mOptions->ExcludeItems();
|
||||
|
||||
// don't trust the index from the bookmark service, find it ourselves. The
|
||||
// sorting could be different, or the bookmark services indices and ours might
|
||||
// be out of sync somehow.
|
||||
uint32_t index;
|
||||
nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
|
||||
// Bug 1097528.
|
||||
// It's possible our result registered due to a previous notification, for
|
||||
// example the Library left pane could have refreshed and replaced the
|
||||
// right pane as a consequence. In such a case our contents are already
|
||||
// up-to-date. That's OK.
|
||||
if (!node) {
|
||||
if (excludeItems)
|
||||
return NS_OK;
|
||||
|
||||
NS_NOTREACHED("Removing item we don't have");
|
||||
return NS_ERROR_FAILURE;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
|
||||
(mParent && mParent->mOptions->ExcludeItems()) ||
|
||||
mOptions->ExcludeItems();
|
||||
if ((node->IsURI() || node->IsSeparator()) && excludeItems) {
|
||||
// don't update items when we aren't displaying them, but we do need to
|
||||
// adjust everybody's bookmark indices to account for the removal
|
||||
|
@ -3854,6 +3866,26 @@ nsNavHistoryFolderResultNode::OnItemMoved(int64_t aItemId,
|
|||
|
||||
RESTART_AND_RETURN_IF_ASYNC_PENDING();
|
||||
|
||||
uint32_t index;
|
||||
nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
|
||||
// Bug 1097528.
|
||||
// It's possible our result registered due to a previous notification, for
|
||||
// example the Library left pane could have refreshed and replaced the
|
||||
// right pane as a consequence. In such a case our contents are already
|
||||
// up-to-date. That's OK.
|
||||
if (node && aNewParent == mItemId && index == static_cast<uint32_t>(aNewIndex))
|
||||
return NS_OK;
|
||||
if (!node && aOldParent == mItemId)
|
||||
return NS_OK;
|
||||
|
||||
bool excludeItems = (mResult && mResult->mRootNode->mOptions->ExcludeItems()) ||
|
||||
(mParent && mParent->mOptions->ExcludeItems()) ||
|
||||
mOptions->ExcludeItems();
|
||||
if (node && excludeItems && (node->IsURI() || node->IsSeparator())) {
|
||||
// Don't update items when we aren't displaying them.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (!StartIncrementalUpdate())
|
||||
return NS_OK; // entire container was refreshed for us
|
||||
|
||||
|
@ -3865,13 +3897,11 @@ nsNavHistoryFolderResultNode::OnItemMoved(int64_t aItemId,
|
|||
ReindexRange(aOldIndex + 1, INT32_MAX, -1);
|
||||
ReindexRange(aNewIndex, INT32_MAX, 1);
|
||||
|
||||
uint32_t index;
|
||||
nsNavHistoryResultNode* node = FindChildById(aItemId, &index);
|
||||
MOZ_ASSERT(node, "Can't find folder that is moving!");
|
||||
if (!node) {
|
||||
NS_NOTREACHED("Can't find folder that is moving!");
|
||||
return NS_ERROR_FAILURE;
|
||||
}
|
||||
NS_ASSERTION(index < uint32_t(mChildren.Count()), "Invalid index!");
|
||||
MOZ_ASSERT(index < uint32_t(mChildren.Count()), "Invalid index!");
|
||||
node->mBookmarkIndex = aNewIndex;
|
||||
|
||||
// adjust position
|
||||
|
|
|
@ -15,11 +15,20 @@ add_task(function* test_searchEngine() {
|
|||
addBookmark({ uri: uri2, title: "Terms - SearchEngine Search" });
|
||||
|
||||
do_log_info("Past search terms should be styled, unless bookmarked");
|
||||
Services.prefs.setBoolPref("browser.urlbar.restyleSearches", true);
|
||||
yield check_autocomplete({
|
||||
search: "term",
|
||||
matches: [ { uri: uri1, title: "Terms", searchEngine: "SearchEngine", style: ["favicon", "search"] },
|
||||
{ uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ]
|
||||
});
|
||||
|
||||
do_log_info("Past search terms should not be styled if restyling is disabled");
|
||||
Services.prefs.setBoolPref("browser.urlbar.restyleSearches", false);
|
||||
yield check_autocomplete({
|
||||
search: "term",
|
||||
matches: [ { uri: uri1, title: "Terms - SearchEngine Search" },
|
||||
{ uri: uri2, title: "Terms - SearchEngine Search", style: ["bookmark"] } ]
|
||||
});
|
||||
|
||||
yield cleanup();
|
||||
});
|
||||
|
|
|
@ -45,3 +45,7 @@ EXTRA_JS_MODULES.devtools += [
|
|||
'Loader.jsm',
|
||||
'Require.jsm',
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.devtools.server.actors += [
|
||||
'server/actors/highlighter.css'
|
||||
]
|
||||
|
|
|
@ -22,7 +22,7 @@ const BOX_MODEL_REGIONS = ["margin", "border", "padding", "content"];
|
|||
const BOX_MODEL_SIDES = ["top", "right", "bottom", "left"];
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const HIGHLIGHTER_STYLESHEET_URI = "chrome://browser/skin/devtools/highlighter.css";
|
||||
const HIGHLIGHTER_STYLESHEET_URI = "resource://gre/modules/devtools/server/actors/highlighter.css";
|
||||
const HIGHLIGHTER_PICKED_TIMER = 1000;
|
||||
// How high is the nodeinfobar
|
||||
const NODE_INFOBAR_HEIGHT = 40; //px
|
||||
|
|
|
@ -176,6 +176,27 @@ BreakpointStore.prototype = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Move the breakpoint to the new location.
|
||||
*
|
||||
* @param Object aBreakpoint
|
||||
* The breakpoint being moved. See `addBreakpoint` for a description of
|
||||
* its expected properties.
|
||||
* @param Object aNewLocation
|
||||
* The location to move the breakpoint to. Properties:
|
||||
* - line
|
||||
* - column (optional; omission implies whole line breakpoint)
|
||||
*/
|
||||
moveBreakpoint: function (aBreakpoint, aNewLocation) {
|
||||
const existingBreakpoint = this.getBreakpoint(aBreakpoint);
|
||||
this.removeBreakpoint(existingBreakpoint);
|
||||
|
||||
const { line, column } = aNewLocation;
|
||||
existingBreakpoint.line = line;
|
||||
existingBreakpoint.column = column;
|
||||
this.addBreakpoint(existingBreakpoint);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a breakpoint from the breakpoint store. Will throw an error if the
|
||||
* breakpoint is not found.
|
||||
|
@ -1426,12 +1447,132 @@ ThreadActor.prototype = {
|
|||
return this._setBreakpoint(aLocation);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Set a breakpoint using the jsdbg2 API. If the line on which the breakpoint
|
||||
* is being set contains no code, then the breakpoint will slide down to the
|
||||
* next line that has runnable code. In this case the server breakpoint cache
|
||||
* will be updated, so callers that iterate over the breakpoint cache should
|
||||
* take that into account.
|
||||
* Get or create the BreakpointActor for the breakpoint at the given location.
|
||||
*
|
||||
* NB: This will override a pre-existing BreakpointActor's condition with
|
||||
* the given the location's condition.
|
||||
*
|
||||
* @param Object location
|
||||
* The breakpoint location. See BreakpointStore.prototype.addBreakpoint
|
||||
* for more information.
|
||||
* @returns BreakpointActor
|
||||
*/
|
||||
_getOrCreateBreakpointActor: function (location) {
|
||||
let actor;
|
||||
const storedBp = this.breakpointStore.getBreakpoint(location);
|
||||
|
||||
if (storedBp.actor) {
|
||||
actor = storedBp.actor;
|
||||
actor.condition = location.condition;
|
||||
return actor;
|
||||
}
|
||||
|
||||
storedBp.actor = actor = new BreakpointActor(this, {
|
||||
url: location.url,
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
condition: location.condition
|
||||
});
|
||||
this.threadLifetimePool.addActor(actor);
|
||||
return actor;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set breakpoints at the offsets closest to our target location's column.
|
||||
*
|
||||
* @param Array scripts
|
||||
* The set of Debugger.Script instances to consider.
|
||||
* @param Object location
|
||||
* The target location.
|
||||
* @param BreakpointActor actor
|
||||
* The BreakpointActor to handle hitting the breakpoints we set.
|
||||
* @returns Object
|
||||
* The RDP response.
|
||||
*/
|
||||
_setBreakpointAtColumn: function (scripts, location, actor) {
|
||||
// Debugger.Script -> array of offset mappings
|
||||
const scriptsAndOffsetMappings = new Map();
|
||||
|
||||
for (let script of scripts) {
|
||||
this._findClosestOffsetMappings(location, script, scriptsAndOffsetMappings);
|
||||
}
|
||||
|
||||
for (let [script, mappings] of scriptsAndOffsetMappings) {
|
||||
for (let offsetMapping of mappings) {
|
||||
script.setBreakpoint(offsetMapping.offset, actor);
|
||||
}
|
||||
actor.addScript(script, this);
|
||||
}
|
||||
|
||||
return {
|
||||
actor: actor.actorID
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the scripts which contain offsets that are an entry point to the given
|
||||
* line.
|
||||
*
|
||||
* @param Array scripts
|
||||
* The set of Debugger.Scripts to consider.
|
||||
* @param Number line
|
||||
* The line we are searching for entry points into.
|
||||
* @returns Array of objects of the form { script, offsets } where:
|
||||
* - script is a Debugger.Script
|
||||
* - offsets is an array of offsets that are entry points into the
|
||||
* given line.
|
||||
*/
|
||||
_findEntryPointsForLine: function (scripts, line) {
|
||||
const entryPoints = [];
|
||||
for (let script of scripts) {
|
||||
const offsets = script.getLineOffsets(line);
|
||||
if (offsets.length) {
|
||||
entryPoints.push({ script, offsets });
|
||||
}
|
||||
}
|
||||
return entryPoints;
|
||||
},
|
||||
|
||||
/**
|
||||
* Find the first line that is associated with bytecode offsets, and is
|
||||
* greater than or equal to the given start line.
|
||||
*
|
||||
* @param Array scripts
|
||||
* The set of Debugger.Script instances to consider.
|
||||
* @param Number startLine
|
||||
* The target line.
|
||||
* @return Object|null
|
||||
* If we can't find a line matching our constraints, return
|
||||
* null. Otherwise, return an object of the form:
|
||||
* {
|
||||
* line: Number,
|
||||
* entryPoints: [
|
||||
* { script: Debugger.Script, offsets: [offset, ...] },
|
||||
* ...
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
_findNextLineWithOffsets: function (scripts, startLine) {
|
||||
const maxLine = Math.max(...scripts.map(s => s.startLine + s.lineCount));
|
||||
|
||||
for (let line = startLine; line < maxLine; line++) {
|
||||
const entryPoints = this._findEntryPointsForLine(scripts, line);
|
||||
if (entryPoints.length) {
|
||||
return { line, entryPoints };
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set a breakpoint using the Debugger API. If the line on which the
|
||||
* breakpoint is being set contains no code, then the breakpoint will slide
|
||||
* down to the next line that has runnable code. In this case the server
|
||||
* breakpoint cache will be updated, so callers that iterate over the
|
||||
* breakpoint cache should take that into account.
|
||||
*
|
||||
* @param object aLocation
|
||||
* The location of the breakpoint (in the generated source, if source
|
||||
|
@ -1441,170 +1582,134 @@ ThreadActor.prototype = {
|
|||
* nowhere else.
|
||||
*/
|
||||
_setBreakpoint: function (aLocation, aOnlyThisScript=null) {
|
||||
let location = {
|
||||
const location = {
|
||||
url: aLocation.url,
|
||||
line: aLocation.line,
|
||||
column: aLocation.column,
|
||||
condition: aLocation.condition
|
||||
};
|
||||
|
||||
let actor;
|
||||
let storedBp = this.breakpointStore.getBreakpoint(location);
|
||||
if (storedBp.actor) {
|
||||
actor = storedBp.actor;
|
||||
actor.condition = location.condition;
|
||||
} else {
|
||||
storedBp.actor = actor = new BreakpointActor(this, {
|
||||
url: location.url,
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
condition: location.condition
|
||||
});
|
||||
this.threadLifetimePool.addActor(actor);
|
||||
}
|
||||
|
||||
// Find all scripts matching the given location
|
||||
let scripts = this.dbg.findScripts(location);
|
||||
if (scripts.length == 0) {
|
||||
// Since we did not find any scripts to set the breakpoint on now, return
|
||||
// early. When a new script that matches this breakpoint location is
|
||||
// introduced, the breakpoint actor will already be in the breakpoint store
|
||||
// and will be set at that time.
|
||||
return {
|
||||
actor: actor.actorID
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* For each script, if the given line has at least one entry point, set a
|
||||
* breakpoint on the bytecode offets for each of them.
|
||||
*/
|
||||
|
||||
// Debugger.Script -> array of offset mappings
|
||||
let scriptsAndOffsetMappings = new Map();
|
||||
|
||||
for (let script of scripts) {
|
||||
this._findClosestOffsetMappings(location,
|
||||
script,
|
||||
scriptsAndOffsetMappings);
|
||||
}
|
||||
|
||||
if (scriptsAndOffsetMappings.size > 0) {
|
||||
for (let [script, mappings] of scriptsAndOffsetMappings) {
|
||||
if (aOnlyThisScript && script !== aOnlyThisScript) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let offsetMapping of mappings) {
|
||||
script.setBreakpoint(offsetMapping.offset, actor);
|
||||
}
|
||||
actor.addScript(script, this);
|
||||
}
|
||||
|
||||
return {
|
||||
actor: actor.actorID
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If we get here, no breakpoint was set. This is because the given line
|
||||
* has no entry points, for example because it is empty. As a fallback
|
||||
* strategy, we try to set the breakpoint on the smallest line greater
|
||||
* than or equal to the given line that as at least one entry point.
|
||||
*/
|
||||
|
||||
// Find all innermost scripts matching the given location
|
||||
scripts = this.dbg.findScripts({
|
||||
url: aLocation.url,
|
||||
line: aLocation.line,
|
||||
innermost: true
|
||||
const actor = location.actor = this._getOrCreateBreakpointActor(location);
|
||||
const scripts = this.dbg.findScripts({
|
||||
url: location.url,
|
||||
// Although we will automatically slide the breakpoint down to the first
|
||||
// line with code when the requested line doesn't have any, we want to
|
||||
// restrict the sliding to within functions that contain the requested
|
||||
// line.
|
||||
line: location.line
|
||||
});
|
||||
|
||||
/**
|
||||
* For each innermost script, look for the smallest line greater than or
|
||||
* equal to the given line that has one or more entry points. If found, set
|
||||
* a breakpoint on the bytecode offset for each of its entry points.
|
||||
*/
|
||||
let actualLocation;
|
||||
let found = false;
|
||||
for (let script of scripts) {
|
||||
let offsets = script.getAllOffsets();
|
||||
|
||||
for (let line = location.line; line < offsets.length; ++line) {
|
||||
if (offsets[line]) {
|
||||
if (!aOnlyThisScript || script === aOnlyThisScript) {
|
||||
for (let offset of offsets[line]) {
|
||||
script.setBreakpoint(offset, actor);
|
||||
}
|
||||
actor.addScript(script, this);
|
||||
}
|
||||
|
||||
if (!actualLocation) {
|
||||
actualLocation = {
|
||||
url: location.url,
|
||||
line: line
|
||||
};
|
||||
}
|
||||
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scripts.length === 0) {
|
||||
// Since we did not find any scripts to set the breakpoint on now, return
|
||||
// early. When a new script that matches this breakpoint location is
|
||||
// introduced, the breakpoint actor will already be in the breakpoint
|
||||
// store and the breakpoint will be set at that time. This is similar to
|
||||
// GDB's "pending" breakpoints for shared libraries that aren't loaded
|
||||
// yet.
|
||||
return {
|
||||
actor: actor.actorID
|
||||
};
|
||||
}
|
||||
|
||||
if (found) {
|
||||
let existingBp = this.breakpointStore.hasBreakpoint(actualLocation);
|
||||
if (location.column) {
|
||||
return this._setBreakpointAtColumn(scripts, location, actor);
|
||||
}
|
||||
|
||||
if (existingBp && existingBp.actor) {
|
||||
/**
|
||||
* We already have a breakpoint actor for the actual location, so actor
|
||||
* we created earlier is now redundant. Delete it, update the breakpoint
|
||||
* store, and return the actor for the actual location.
|
||||
*/
|
||||
// Select the first line that has offsets, and is greater than or equal to
|
||||
// the requested line. Set breakpoints on each of the offsets that is an
|
||||
// entry point to our selected line.
|
||||
|
||||
const result = this._findNextLineWithOffsets(scripts, location.line);
|
||||
if (!result) {
|
||||
return {
|
||||
error: "noCodeAtLineColumn",
|
||||
actor: actor.actorID
|
||||
};
|
||||
}
|
||||
|
||||
const { line, entryPoints } = result;
|
||||
const actualLocation = line !== location.line
|
||||
? { url: location.url, line }
|
||||
: undefined;
|
||||
|
||||
if (actualLocation) {
|
||||
// Check whether we already have a breakpoint actor for the actual
|
||||
// location. If we do have an existing actor, then the actor we created
|
||||
// above is redundant and must be destroyed. If we do not have an existing
|
||||
// actor, we need to update the breakpoint store with the new location.
|
||||
|
||||
const existingBreakpoint = this.breakpointStore.hasBreakpoint(actualLocation);
|
||||
if (existingBreakpoint && existingBreakpoint.actor) {
|
||||
actor.onDelete();
|
||||
this.breakpointStore.removeBreakpoint(location);
|
||||
return {
|
||||
actor: existingBp.actor.actorID,
|
||||
actualLocation: actualLocation
|
||||
actor: existingBreakpoint.actor.actorID,
|
||||
actualLocation
|
||||
};
|
||||
} else {
|
||||
actor.location = actualLocation;
|
||||
this.breakpointStore.moveBreakpoint(location, actualLocation);
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't have a breakpoint actor for the actual location yet. Instead
|
||||
* or creating a new actor, reuse the actor we created earlier, and update
|
||||
* the breakpoint store.
|
||||
*/
|
||||
actor.location = actualLocation;
|
||||
this.breakpointStore.addBreakpoint({
|
||||
actor: actor,
|
||||
url: actualLocation.url,
|
||||
line: actualLocation.line,
|
||||
column: actualLocation.column
|
||||
});
|
||||
this.breakpointStore.removeBreakpoint(location);
|
||||
return {
|
||||
actor: actor.actorID,
|
||||
actualLocation: actualLocation
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If we get here, no line matching the given line was found, so just fail
|
||||
* epically.
|
||||
*/
|
||||
this._setBreakpointOnEntryPoints(
|
||||
actor,
|
||||
aOnlyThisScript
|
||||
? entryPoints.filter(o => o.script === aOnlyThisScript)
|
||||
: entryPoints
|
||||
);
|
||||
|
||||
return {
|
||||
error: "noCodeAtLineColumn",
|
||||
actor: actor.actorID
|
||||
actor: actor.actorID,
|
||||
actualLocation
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Set breakpoints on all the given entry points with the given
|
||||
* BreakpointActor as the handler.
|
||||
*
|
||||
* @param BreakpointActor actor
|
||||
* The actor handling the breakpoint hits.
|
||||
* @param Array entryPoints
|
||||
* An array of objects of the form `{ script, offsets }`.
|
||||
*/
|
||||
_setBreakpointOnEntryPoints: function (actor, entryPoints) {
|
||||
for (let { script, offsets } of entryPoints) {
|
||||
for (let offset of offsets) {
|
||||
script.setBreakpoint(offset, actor);
|
||||
}
|
||||
actor.addScript(script, this);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find all of the offset mappings associated with `aScript` that are closest
|
||||
* to `aTargetLocation`. If new offset mappings are found that are closer to
|
||||
* `aTargetOffset` than the existing offset mappings inside
|
||||
* `aScriptsAndOffsetMappings`, we empty that map and only consider the
|
||||
* closest offset mappings. If there is no column in `aTargetLocation`, we add
|
||||
* all offset mappings that are on the given line.
|
||||
* closest offset mappings.
|
||||
*
|
||||
* In many cases, but not all, this method finds only one closest offset.
|
||||
* Consider the following case, where multiple offsets will be found:
|
||||
*
|
||||
* 0 1 2 3
|
||||
* 0123456789012345678901234567890
|
||||
* +-------------------------------
|
||||
* 1|function f() {
|
||||
* 2| return g() + h();
|
||||
* 3|}
|
||||
*
|
||||
* The Debugger reports three offsets on line 2 upon which we could set a
|
||||
* breakpoint: the `return` statement at column 2, the call expression `g()`
|
||||
* at column 9, and the call expression `h()` at column 15. (Careful readers
|
||||
* will note that complete source location information isn't saved by
|
||||
* SpiderMonkey's frontend, and we don't get an offset associated specifically
|
||||
* with the `+` operation.)
|
||||
*
|
||||
* If our target location is line 2 column 12, the offset for the call to `g`
|
||||
* is 3 columns to the left and the offset for the call to `h` is 3 columns to
|
||||
* the right. Because they are equally close, we will return both offsets to
|
||||
* have breakpoints set upon them.
|
||||
*
|
||||
* @param Object aTargetLocation
|
||||
* An object of the form { url, line[, column] }.
|
||||
|
@ -1617,21 +1722,6 @@ ThreadActor.prototype = {
|
|||
_findClosestOffsetMappings: function (aTargetLocation,
|
||||
aScript,
|
||||
aScriptsAndOffsetMappings) {
|
||||
// If we are given a column, we will try and break only at that location,
|
||||
// otherwise we will break anytime we get on that line.
|
||||
|
||||
if (aTargetLocation.column == null) {
|
||||
let offsetMappings = aScript.getLineOffsets(aTargetLocation.line)
|
||||
.map(o => ({
|
||||
line: aTargetLocation.line,
|
||||
offset: o
|
||||
}));
|
||||
if (offsetMappings.length) {
|
||||
aScriptsAndOffsetMappings.set(aScript, offsetMappings);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let offsetMappings = aScript.getAllColumnOffsets()
|
||||
.filter(({ lineNumber }) => lineNumber === aTargetLocation.line);
|
||||
|
||||
|
|
|
@ -16,10 +16,10 @@ Components.utils.import('resource:///modules/devtools/SourceMap.jsm');
|
|||
function run_test()
|
||||
{
|
||||
initTestTracerServer();
|
||||
gDebuggee = addTestGlobal("test-tracer-actor");
|
||||
gDebuggee = addTestGlobal("test-breakpoints");
|
||||
gClient = new DebuggerClient(DebuggerServer.connectPipe());
|
||||
gClient.connect(function() {
|
||||
attachTestThread(gClient, "test-tracer-actor", testBreakpoint);
|
||||
attachTestThread(gClient, "test-breakpoints", testBreakpoint);
|
||||
});
|
||||
do_test_pending();
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ function run_test()
|
|||
test_remove_breakpoint();
|
||||
test_find_breakpoints();
|
||||
test_duplicate_breakpoints();
|
||||
test_move_breakpoint();
|
||||
}
|
||||
|
||||
function test_has_breakpoint() {
|
||||
|
@ -180,3 +181,31 @@ function test_duplicate_breakpoints() {
|
|||
do_check_eq(bpStore.size, 1, "We should have only 1 whole line breakpoint");
|
||||
bpStore.removeBreakpoint(location);
|
||||
}
|
||||
|
||||
function test_move_breakpoint() {
|
||||
let bpStore = new BreakpointStore();
|
||||
|
||||
let oldLocation = {
|
||||
url: "http://example.com/foo.js",
|
||||
line: 10
|
||||
};
|
||||
|
||||
let newLocation = {
|
||||
url: "http://example.com/foo.js",
|
||||
line: 12
|
||||
};
|
||||
|
||||
bpStore.addBreakpoint(oldLocation);
|
||||
bpStore.moveBreakpoint(oldLocation, newLocation);
|
||||
|
||||
equal(bpStore.size, 1, "Moving a breakpoint maintains the correct size.");
|
||||
|
||||
let bp = bpStore.getBreakpoint(newLocation);
|
||||
ok(bp, "We should be able to get a breakpoint at the new location.");
|
||||
equal(bp.line, newLocation.line,
|
||||
"We should get the moved line.");
|
||||
|
||||
equal(bpStore.hasBreakpoint({ url: "http://example.com/foo.js", line: 10 }),
|
||||
null,
|
||||
"And we shouldn't be able to get any BP at the old location.");
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ const N_INTERNALS = "{private:internals:" + salt + "}";
|
|||
const JS_HAS_SYMBOLS = typeof Symbol === "function";
|
||||
const ITERATOR_SYMBOL = JS_HAS_SYMBOLS ? Symbol.iterator : "@@iterator";
|
||||
|
||||
// We use DOM Promise for scheduling the walker loop.
|
||||
const DOMPromise = Promise;
|
||||
|
||||
/////// Warn-upon-finalization mechanism
|
||||
//
|
||||
// One of the difficult problems with promises is locating uncaught
|
||||
|
@ -685,8 +688,7 @@ this.PromiseWalker = {
|
|||
scheduleWalkerLoop: function()
|
||||
{
|
||||
this.walkerLoopScheduled = true;
|
||||
Services.tm.currentThread.dispatch(this.walkerLoop,
|
||||
Ci.nsIThread.DISPATCH_NORMAL);
|
||||
DOMPromise.resolve().then(() => this.walkerLoop());
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -259,24 +259,26 @@ add_test(function() {
|
|||
info("Part 3");
|
||||
is_in_list(aManager, "addons://list/extension", true, false);
|
||||
|
||||
go_back(aManager);
|
||||
executeSoon(() => go_back(aManager));
|
||||
gBrowser.addEventListener("pageshow", function() {
|
||||
gBrowser.removeEventListener("pageshow", arguments.callee, false);
|
||||
info("Part 4");
|
||||
is(gBrowser.currentURI.spec, "http://example.com/", "Should be showing the webpage");
|
||||
ok(!gBrowser.canGoBack, "Should not be able to go back");
|
||||
ok(gBrowser.canGoForward, "Should be able to go forward");
|
||||
executeSoon(() => executeSoon(function () {
|
||||
is(gBrowser.currentURI.spec, "http://example.com/", "Should be showing the webpage");
|
||||
ok(!gBrowser.canGoBack, "Should not be able to go back");
|
||||
ok(gBrowser.canGoForward, "Should be able to go forward");
|
||||
|
||||
go_forward(aManager);
|
||||
gBrowser.addEventListener("pageshow", function() {
|
||||
gBrowser.removeEventListener("pageshow", arguments.callee, false);
|
||||
wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
|
||||
info("Part 5");
|
||||
is_in_list(aManager, "addons://list/extension", true, false);
|
||||
go_forward(aManager);
|
||||
gBrowser.addEventListener("pageshow", function() {
|
||||
gBrowser.removeEventListener("pageshow", arguments.callee, false);
|
||||
wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
|
||||
info("Part 5");
|
||||
is_in_list(aManager, "addons://list/extension", true, false);
|
||||
|
||||
close_manager(aManager, run_next_test);
|
||||
});
|
||||
}, false);
|
||||
close_manager(aManager, run_next_test);
|
||||
});
|
||||
}, false);
|
||||
}));
|
||||
}, false);
|
||||
});
|
||||
}, true);
|
||||
|
@ -438,7 +440,7 @@ add_test(function() {
|
|||
info("Part 3");
|
||||
is_in_list(aManager, "addons://list/plugin", false, true);
|
||||
|
||||
go_forward(aManager);
|
||||
executeSoon(() => go_forward(aManager));
|
||||
gBrowser.addEventListener("pageshow", function(event) {
|
||||
if (event.target.location != "http://example.com/")
|
||||
return;
|
||||
|
|
|
@ -19,6 +19,13 @@
|
|||
color: WindowText;
|
||||
}
|
||||
|
||||
/* Use the new in-content colors for #contentAreaDownloadsView. After landing
|
||||
of bug 989469 the colors can be moved to *|*:root */
|
||||
*|*#contentAreaDownloadsView {
|
||||
background: #f1f1f1;
|
||||
color: #424e5a;
|
||||
}
|
||||
|
||||
html|html {
|
||||
font: message-box;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,13 @@
|
|||
linear-gradient(#ADB5C2, #BFC6D1);
|
||||
}
|
||||
|
||||
/* Use the new in-content colors for #contentAreaDownloadsView. After landing
|
||||
of bug 989469 the colors can be moved to *|*:root */
|
||||
*|*#contentAreaDownloadsView {
|
||||
background: #f1f1f1;
|
||||
color: #424e5a;
|
||||
}
|
||||
|
||||
html|html {
|
||||
font: message-box;
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ xul|tabpanels {
|
|||
border: none;
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
xul|tabs {
|
||||
|
|
|
@ -60,6 +60,13 @@ html|html {
|
|||
}
|
||||
%endif
|
||||
|
||||
/* Use the new in-content colors for #contentAreaDownloadsView. After landing
|
||||
of bug 989469 the colors can be moved to *|*:root */
|
||||
*|*#contentAreaDownloadsView {
|
||||
background: #f1f1f1;
|
||||
color: #424e5a;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
*|*.main-content {
|
||||
/* Needed to allow the radius to clip the inner content, see bug 595656 */
|
||||
|
|
Загрузка…
Ссылка в новой задаче