This commit is contained in:
Ryan VanderMeulen 2014-11-13 15:40:39 -05:00
Родитель 3816bf87a1 8bd42cc7c3
Коммит 28c2fca375
99 изменённых файлов: 2427 добавлений и 1068 удалений

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

@ -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 */