Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2014-10-27 17:49:25 -07:00
commit 7c8bee6a68
74 changed files with 1559 additions and 395 deletions

View File

@ -316,7 +316,7 @@ skip-if = e10s
run-if = datareporting
[browser_devedition.js]
[browser_devices_get_user_media.js]
skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent?
skip-if = buildapp == 'mulet' || os == "linux" || e10s # linux: bug 976544 & bug 1060315; e10s: Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent?
[browser_devices_get_user_media_about_urls.js]
skip-if = e10s # Bug 973001 - appears user media notifications only happen in the child and don't make their way to the parent?
[browser_discovery.js]

View File

@ -57,4 +57,5 @@ skip-if = e10s # Bug 1069162 - lots of orange
skip-if = e10s # Bug 915547 (social providers don't install)
[browser_social_window.js]
[browser_social_workercrash.js]
skip-if = !crashreporter
#skip-if = !crashreporter
skip-if = true # Bug 1060813 - frequent leaks on all platforms

View File

@ -201,9 +201,9 @@
<vbox id="PanelUI-panic-explanations">
<label id="PanelUI-panic-actionlist-main-label">&panicButton.view.mainActionDesc;</label>
<label id="PanelUI-panic-actionlist-windows" class="PanelUI-panic-actionlist">&panicButton.view.deleteTabsAndWindows;</label>
<label id="PanelUI-panic-actionlist-cookies" class="PanelUI-panic-actionlist">&panicButton.view.deleteCookies;</label>
<label id="PanelUI-panic-actionlist-history" class="PanelUI-panic-actionlist">&panicButton.view.deleteHistory;</label>
<label id="PanelUI-panic-actionlist-windows" class="PanelUI-panic-actionlist">&panicButton.view.deleteTabsAndWindows;</label>
<label id="PanelUI-panic-actionlist-newwindow" class="PanelUI-panic-actionlist">&panicButton.view.openNewWindow;</label>
<label id="PanelUI-panic-warning">&panicButton.view.undoWarning;</label>

View File

@ -179,8 +179,13 @@ CallProgressSocket.prototype = {
* and register with the Loop server.
*/
let LoopCallsInternal = {
callsData: {inUse: false},
_mocks: {webSocket: undefined},
callsData: {
inUse: false,
},
mocks: {
webSocket: undefined,
},
/**
* Callback from MozLoopPushHandler - A push notification has been received from
@ -308,7 +313,9 @@ let LoopCallsInternal = {
callData.progressURL,
callData.callId,
callData.websocketToken);
callProgress._websocket = this._mocks.webSocket;
if (this.mocks.webSocket) {
callProgress._websocket = this.mocks.webSocket;
}
// This instance of CallProgressSocket should stay alive until the underlying
// websocket is closed since it is passed to the websocket as the nsIWebSocketListener.
callProgress.connect(() => {callProgress.sendBusy();});

View File

@ -109,7 +109,6 @@ function getJSONPref(aName) {
// or the registration was successful. This is null if a registration attempt was
// unsuccessful.
let gRegisteredDeferred = null;
let gPushHandler = null;
let gHawkClient = null;
let gLocalizedStrings = null;
let gInitializeTimer = null;
@ -126,7 +125,12 @@ let gErrors = new Map();
* and register with the Loop server.
*/
let MozLoopServiceInternal = {
_mocks: {webSocket: undefined},
mocks: {
pushHandler: undefined,
webSocket: undefined,
},
get pushHandler() this.mocks.pushHandler || MozLoopPushHandler,
// The uri of the Loop server.
get loopServerUri() Services.prefs.getCharPref("loop.server"),
@ -307,21 +311,14 @@ let MozLoopServiceInternal = {
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
*
* @param {Object} mockPushHandler Optional, test-only mock push handler. Used
* to allow mocking of the MozLoopPushHandler.
* @param {Object} mockWebSocket Optional, test-only mock webSocket. To be passed
* through to MozLoopPushHandler.
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
*/
promiseRegisteredWithServers: function(mockPushHandler, mockWebSocket) {
promiseRegisteredWithServers: function() {
if (gRegisteredDeferred) {
return gRegisteredDeferred.promise;
}
this._mocks.webSocket = mockWebSocket;
this._mocks.pushHandler = mockPushHandler;
// Wrap push notification registration call-back in a Promise.
let registerForNotification = function(channelID, onNotification) {
return new Promise((resolve, reject) => {
@ -332,7 +329,7 @@ let MozLoopServiceInternal = {
resolve(pushUrl);
}
};
gPushHandler.register(channelID, onRegistered, onNotification);
MozLoopServiceInternal.pushHandler.register(channelID, onRegistered, onNotification);
});
};
@ -341,27 +338,28 @@ let MozLoopServiceInternal = {
// it back to null on error.
let result = gRegisteredDeferred.promise;
gPushHandler = mockPushHandler || MozLoopPushHandler;
let options = mockWebSocket ? {mockWebSocket: mockWebSocket} : {};
gPushHandler.initialize(options);
let options = this.mocks.webSocket ? { mockWebSocket: this.mocks.webSocket } : {};
this.pushHandler.initialize(options);
let callsRegGuest = registerForNotification(MozLoopService.channelIDs.callsGuest,
LoopCalls.onNotification);
let roomsRegGuest = registerForNotification(MozLoopService.channelIDs.roomsGuest,
roomsPushNotification);
let callsRegFxA = registerForNotification(MozLoopService.channelIDs.callsFxA,
LoopCalls.onNotification);
let roomsRegFxA = registerForNotification(MozLoopService.channelIDs.roomsFxA,
roomsPushNotification);
Promise.all([callsRegGuest, roomsRegGuest, callsRegFxA, roomsRegFxA])
.then((pushUrls) => {
return this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST,
{calls: pushUrls[0], rooms: pushUrls[1]}) })
.then(() => {
return this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST,{
calls: pushUrls[0],
rooms: pushUrls[1],
});
}).then(() => {
// storeSessionToken could have rejected and nulled the promise if the token was malformed.
if (!gRegisteredDeferred) {
return;
@ -873,13 +871,13 @@ let MozLoopServiceInternal = {
};
Object.freeze(MozLoopServiceInternal);
let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSocket) => {
let gInitializeTimerFunc = (deferredInitialization) => {
// Kick off the push notification service into registering after a timeout.
// This ensures we're not doing too much straight after the browser's finished
// starting up.
gInitializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
gInitializeTimer.initWithCallback(Task.async(function* initializationCallback() {
yield MozLoopService.register(mockPushHandler, mockWebSocket).then(Task.async(function*() {
yield MozLoopService.register().then(Task.async(function*() {
if (!MozLoopServiceInternal.fxAOAuthTokenData) {
log.debug("MozLoopService: Initialized without an already logged-in account");
deferredInitialization.resolve("initialized to guest status");
@ -890,8 +888,8 @@ let gInitializeTimerFunc = (deferredInitialization, mockPushHandler, mockWebSock
let registeredPromise =
MozLoopServiceInternal.registerWithLoopServer(
LOOP_SESSION_TYPE.FXA, {
calls: gPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA],
rooms: gPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA]
calls: MozLoopServiceInternal.pushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA],
rooms: MozLoopServiceInternal.pushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA]
});
registeredPromise.then(() => {
deferredInitialization.resolve("initialized to logged-in status");
@ -936,7 +934,7 @@ this.MozLoopService = {
*
* @return {Promise}
*/
initialize: Task.async(function*(mockPushHandler, mockWebSocket) {
initialize: Task.async(function*() {
// Do this here, rather than immediately after definition, so that we can
// stub out API functions for unit testing
Object.freeze(this);
@ -962,7 +960,7 @@ this.MozLoopService = {
}
let deferredInitialization = Promise.defer();
gInitializeTimerFunc(deferredInitialization, mockPushHandler, mockWebSocket);
gInitializeTimerFunc(deferredInitialization);
return deferredInitialization.promise.catch(error => {
if (typeof(error) == "object") {
@ -1088,12 +1086,10 @@ this.MozLoopService = {
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
*
* @param {Object} mockPushHandler Optional, test-only mock push handler. Used
* to allow mocking of the MozLoopPushHandler.
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
*/
register: function(mockPushHandler, mockWebSocket) {
register: function() {
log.debug("registering");
// Don't do anything if loop is not enabled.
if (!Services.prefs.getBoolPref("loop.enabled")) {
@ -1104,7 +1100,7 @@ this.MozLoopService = {
throw new Error("Loop is disabled by the soft-start mechanism");
}
return MozLoopServiceInternal.promiseRegisteredWithServers(mockPushHandler, mockWebSocket);
return MozLoopServiceInternal.promiseRegisteredWithServers();
},
/**
@ -1301,8 +1297,8 @@ this.MozLoopService = {
return tokenData;
}).then(tokenData => {
return gRegisteredDeferred.promise.then(Task.async(function*() {
let callsUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA],
roomsUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
let callsUrl = MozLoopServiceInternal.pushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA],
roomsUrl = MozLoopServiceInternal.pushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
if (callsUrl && roomsUrl) {
yield MozLoopServiceInternal.registerWithLoopServer(
LOOP_SESSION_TYPE.FXA, {calls: callsUrl, rooms: roomsUrl});
@ -1348,23 +1344,19 @@ this.MozLoopService = {
*/
logOutFromFxA: Task.async(function*() {
log.debug("logOutFromFxA");
let callsPushUrl, roomsPushUrl;
if (gPushHandler) {
callsPushUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA];
roomsPushUrl = gPushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
}
let pushHandler = MozLoopServiceInternal.pushHandler;
let callsPushUrl = pushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA];
let roomsPushUrl = pushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA];
try {
if (callsPushUrl) {
yield MozLoopServiceInternal.unregisterFromLoopServer(
LOOP_SESSION_TYPE.FXA, callsPushUrl);
yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA, callsPushUrl);
}
if (roomsPushUrl) {
yield MozLoopServiceInternal.unregisterFromLoopServer(
LOOP_SESSION_TYPE.FXA, roomsPushUrl);
yield MozLoopServiceInternal.unregisterFromLoopServer(LOOP_SESSION_TYPE.FXA, roomsPushUrl);
}
}
catch (error) {throw error}
finally {
} catch (error) {
throw error;
} finally {
MozLoopServiceInternal.clearSessionToken(LOOP_SESSION_TYPE.FXA);
}

View File

@ -539,11 +539,7 @@ loop.panel = (function(_, mozL10n) {
},
_onRoomListChanged: function() {
var storeState = this.props.store.getStoreState();
this.setState({
error: storeState.error,
rooms: storeState.rooms
});
this.setState(this.props.store.getStoreState());
},
_getListHeading: function() {

View File

@ -539,11 +539,7 @@ loop.panel = (function(_, mozL10n) {
},
_onRoomListChanged: function() {
var storeState = this.props.store.getStoreState();
this.setState({
error: storeState.error,
rooms: storeState.rooms
});
this.setState(this.props.store.getStoreState());
},
_getListHeading: function() {

View File

@ -434,21 +434,27 @@
cursor: pointer;
}
.feedback label {
.feedback-category-label {
display: block;
line-height: 1.5em;
}
.feedback form input[type="radio"] {
.feedback-category-radio {
margin-right: .5em;
}
.feedback form button[type="submit"],
.feedback form input[type="text"] {
.feedback > form > .btn-success,
.feedback-description {
width: 100%;
margin-top: 14px;
}
.feedback > form > .btn-success {
padding-top: .5em;
padding-bottom: .5em;
border-radius: 2px;
}
.feedback .info {
display: block;
font-size: 10px;

View File

@ -134,6 +134,22 @@ loop.shared.actions = (function() {
GetAllRooms: Action.define("getAllRooms", {
}),
/**
* An error occured while trying to fetch the room list.
* XXX: should move to some roomActions module - refs bug 1079284
*/
GetAllRoomsError: Action.define("getAllRoomsError", {
error: String
}),
/**
* Updates room list.
* XXX: should move to some roomActions module - refs bug 1079284
*/
UpdateRoomList: Action.define("updateRoomList", {
roomList: Array
}),
/**
* Primes localRoomStore with roomLocalId, which triggers the EmptyRoomView
* to do any necessary setup.

View File

@ -97,7 +97,58 @@ loop.shared.mixins = (function() {
}
};
/**
* Audio mixin. Allows playing a single audio file and ensuring it
* is stopped when the component is unmounted.
*/
var AudioMixin = {
audio: null,
_isLoopDesktop: function() {
return typeof rootObject.navigator.mozLoop === "object";
},
/**
* Starts playing an audio file, stopping any audio that is already in progress.
*
* @param {String} filename The filename to play (excluding the extension).
*/
play: function(filename, options) {
if (this._isLoopDesktop()) {
// XXX: We need navigator.mozLoop.playSound(name), see Bug 1089585.
return;
}
options = options || {};
options.loop = options.loop || false;
this._ensureAudioStopped();
this.audio = new Audio('shared/sounds/' + filename + ".ogg");
this.audio.loop = options.loop;
this.audio.play();
},
/**
* Ensures audio is stopped playing, and removes the object from memory.
*/
_ensureAudioStopped: function() {
if (this.audio) {
this.audio.pause();
this.audio.removeAttribute("src");
delete this.audio;
}
},
/**
* Ensures audio is stopped when the component is unmounted.
*/
componentWillUnmount: function() {
this._ensureAudioStopped();
}
};
return {
AudioMixin: AudioMixin,
setRootObject: setRootObject,
DropdownMenuMixin: DropdownMenuMixin,
DocumentVisibilityMixin: DocumentVisibilityMixin

View File

@ -10,49 +10,25 @@ loop.store = loop.store || {};
(function() {
"use strict";
/**
* Shared actions.
* @type {Object}
*/
var sharedActions = loop.shared.actions;
/**
* Room validation schema. See validate.js.
* @type {Object}
*/
var roomSchema = {
roomToken: String,
roomUrl: String,
roomName: String,
maxSize: Number,
currSize: Number,
ctime: Number
roomToken: String,
roomUrl: String,
roomName: String,
maxSize: Number,
participants: Array,
ctime: Number
};
/**
* Temporary sample raw room list data.
* XXX Should be removed when we plug the real mozLoop API for rooms.
* See bug 1074664.
* @type {Array}
*/
var temporaryRawRoomList = [{
roomToken: "_nxD4V4FflQ",
roomUrl: "http://sample/_nxD4V4FflQ",
roomName: "First Room Name",
maxSize: 2,
currSize: 0,
ctime: 1405517546
}, {
roomToken: "QzBbvGmIZWU",
roomUrl: "http://sample/QzBbvGmIZWU",
roomName: "Second Room Name",
maxSize: 2,
currSize: 0,
ctime: 1405517418
}, {
roomToken: "3jKS_Els9IU",
roomUrl: "http://sample/3jKS_Els9IU",
roomName: "Third Room Name",
maxSize: 3,
clientMaxSize: 2,
currSize: 1,
ctime: 1405518241
}];
/**
* Room type. Basically acts as a typed object constructor.
*
@ -95,7 +71,9 @@ loop.store = loop.store || {};
this.dispatcher.register(this, [
"getAllRooms",
"openRoom"
"getAllRoomsError",
"openRoom",
"updateRoomList"
]);
}
@ -119,21 +97,6 @@ loop.store = loop.store || {};
this.trigger("change");
},
/**
* Proxy to navigator.mozLoop.rooms.getAll.
* XXX Could probably be removed when bug 1074664 lands.
*
* @param {Function} cb Callback(error, roomList)
*/
_fetchRoomList: function(cb) {
// Faking this.mozLoop.rooms until it's available; bug 1074664.
if (!this.mozLoop.hasOwnProperty("rooms")) {
cb(null, temporaryRawRoomList);
return;
}
this.mozLoop.rooms.getAll(cb);
},
/**
* Maps and sorts the raw room list received from the mozLoop API.
*
@ -158,13 +121,37 @@ loop.store = loop.store || {};
* Gather the list of all available rooms from the MozLoop API.
*/
getAllRooms: function() {
this._fetchRoomList(function(err, rawRoomList) {
this.setStoreState({
error: err,
rooms: this._processRawRoomList(rawRoomList)
});
this.mozLoop.rooms.getAll(function(err, rawRoomList) {
var action;
if (err) {
action = new sharedActions.GetAllRoomsError({error: err});
} else {
action = new sharedActions.UpdateRoomList({roomList: rawRoomList});
}
this.dispatcher.dispatch(action);
}.bind(this));
}
},
/**
* Updates current error state in case getAllRooms failed.
*
* @param {sharedActions.UpdateRoomListError} actionData The action data.
*/
getAllRoomsError: function(actionData) {
this.setStoreState({error: actionData.error});
},
/**
* Updates current room list.
*
* @param {sharedActions.UpdateRoomList} actionData The action data.
*/
updateRoomList: function(actionData) {
this.setStoreState({
error: undefined,
rooms: this._processRawRoomList(actionData.roomList)
});
},
}, Backbone.Events);
loop.store.RoomListStore = RoomListStore;

View File

@ -135,7 +135,7 @@ loop.shared.views = (function(_, OT, l10n) {
* Conversation view.
*/
var ConversationView = React.createClass({displayName: 'ConversationView',
mixins: [Backbone.Events],
mixins: [Backbone.Events, sharedMixins.AudioMixin],
propTypes: {
sdk: React.PropTypes.object.isRequired,
@ -183,7 +183,7 @@ loop.shared.views = (function(_, OT, l10n) {
componentDidMount: function() {
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this._onSessionConnected);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
@ -225,6 +225,11 @@ loop.shared.views = (function(_, OT, l10n) {
this.props.model.endSession();
},
_onSessionConnected: function(event) {
this.startPublishing(event);
this.play("connected");
},
/**
* Subscribes and attaches each created stream to a DOM element.
*
@ -397,8 +402,9 @@ loop.shared.views = (function(_, OT, l10n) {
var categories = this._getCategories();
return Object.keys(categories).map(function(category, key) {
return (
React.DOM.label({key: key},
React.DOM.label({key: key, className: "feedback-category-label"},
React.DOM.input({type: "radio", ref: "category", name: "category",
className: "feedback-category-radio",
value: category,
onChange: this.handleCategoryChange,
checked: this.state.category === category}),
@ -466,6 +472,7 @@ loop.shared.views = (function(_, OT, l10n) {
this._getCategoryFields(),
React.DOM.p(null,
React.DOM.input({type: "text", ref: "description", name: "description",
className: "feedback-description",
onChange: this.handleDescriptionFieldChange,
onFocus: this.handleDescriptionFieldFocus,
value: descriptionDisplayValue,

View File

@ -135,7 +135,7 @@ loop.shared.views = (function(_, OT, l10n) {
* Conversation view.
*/
var ConversationView = React.createClass({
mixins: [Backbone.Events],
mixins: [Backbone.Events, sharedMixins.AudioMixin],
propTypes: {
sdk: React.PropTypes.object.isRequired,
@ -183,7 +183,7 @@ loop.shared.views = (function(_, OT, l10n) {
componentDidMount: function() {
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this._onSessionConnected);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
@ -225,6 +225,11 @@ loop.shared.views = (function(_, OT, l10n) {
this.props.model.endSession();
},
_onSessionConnected: function(event) {
this.startPublishing(event);
this.play("connected");
},
/**
* Subscribes and attaches each created stream to a DOM element.
*
@ -397,8 +402,9 @@ loop.shared.views = (function(_, OT, l10n) {
var categories = this._getCategories();
return Object.keys(categories).map(function(category, key) {
return (
<label key={key}>
<label key={key} className="feedback-category-label">
<input type="radio" ref="category" name="category"
className="feedback-category-radio"
value={category}
onChange={this.handleCategoryChange}
checked={this.state.category === category} />
@ -466,6 +472,7 @@ loop.shared.views = (function(_, OT, l10n) {
{this._getCategoryFields()}
<p>
<input type="text" ref="description" name="description"
className="feedback-description"
onChange={this.handleDescriptionFieldChange}
onFocus={this.handleDescriptionFieldFocus}
value={descriptionDisplayValue}

View File

@ -188,6 +188,7 @@ p.standalone-btn-label {
.btn-pending-cancel-group > .btn-cancel {
flex: 2 1 auto;
border-radius: 2px;
}
.btn-large {

View File

@ -262,9 +262,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
callState: "connecting"
};
},
@ -274,11 +277,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
componentDidMount: function() {
this.play("connecting", {loop: true});
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.play("ringing", {loop: true});
this.setState({callState: "ringing"});
},
@ -518,6 +523,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Ended conversation view.
*/
var EndedConversationView = React.createClass({displayName: 'EndedConversationView',
mixins: [sharedMixins.AudioMixin],
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
@ -526,6 +533,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("terminated");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),

View File

@ -262,9 +262,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var PendingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
callState: "connecting"
};
},
@ -274,11 +277,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
componentDidMount: function() {
this.play("connecting", {loop: true});
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.play("ringing", {loop: true});
this.setState({callState: "ringing"});
},
@ -518,6 +523,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Ended conversation view.
*/
var EndedConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
@ -526,6 +533,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("terminated");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),

View File

@ -49,6 +49,11 @@ describe("loop.panel", function() {
callback(null, []);
},
on: sandbox.stub()
},
rooms: {
getAll: function(callback) {
callback(null, []);
}
}
};

View File

@ -40,11 +40,13 @@ function* checkFxA401() {
add_task(function* setup() {
Services.prefs.setCharPref("loop.server", BASE_URL);
Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
MozLoopServiceInternal.mocks.pushHandler = mockPushHandler;
registerCleanupFunction(function* () {
info("cleanup time");
yield promiseDeletedOAuthParams(BASE_URL);
Services.prefs.clearUserPref("loop.server");
Services.prefs.clearUserPref("services.push.serverURL");
MozLoopServiceInternal.mocks.pushHandler = undefined;
yield resetFxA();
Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST));
});
@ -251,7 +253,7 @@ add_task(function* basicAuthorizationAndRegistration() {
mockPushHandler.registrationPushURL = "https://localhost/pushUrl/guest";
// Notification observed due to the error being cleared upon successful registration.
let statusChangedPromise = promiseObserverNotified("loop-status-changed");
yield MozLoopService.register(mockPushHandler);
yield MozLoopService.register();
yield statusChangedPromise;
// Normally the same pushUrl would be registered but we change it in the test
@ -316,7 +318,7 @@ add_task(function* loginWithParams401() {
test_error: "params_401",
};
yield promiseOAuthParamsSetup(BASE_URL, params);
yield MozLoopService.register(mockPushHandler);
yield MozLoopService.register();
let loginPromise = MozLoopService.logInToFxA();
yield loginPromise.then(tokenData => {

View File

@ -50,14 +50,14 @@ describe("loop.store.RoomListStore", function () {
roomUrl: "http://sample/_nxD4V4FflQ",
roomName: "First Room Name",
maxSize: 2,
currSize: 0,
participants: [],
ctime: 1405517546
}, {
roomToken: "QzBbvGmIZWU",
roomUrl: "http://sample/QzBbvGmIZWU",
roomName: "Second Room Name",
maxSize: 2,
currSize: 0,
participants: [],
ctime: 1405517418
}, {
roomToken: "3jKS_Els9IU",
@ -65,7 +65,7 @@ describe("loop.store.RoomListStore", function () {
roomName: "Third Room Name",
maxSize: 3,
clientMaxSize: 2,
currSize: 1,
participants: [],
ctime: 1405518241
}];
@ -93,7 +93,7 @@ describe("loop.store.RoomListStore", function () {
it("should fetch the room list from the mozLoop API", function(done) {
store.once("change", function() {
expect(store.getStoreState().error).to.be.a.null;
expect(store.getStoreState().error).to.be.a.undefined;
expect(store.getStoreState().rooms).to.have.length.of(3);
done();
});
@ -104,7 +104,7 @@ describe("loop.store.RoomListStore", function () {
it("should order the room list using ctime desc", function(done) {
store.once("change", function() {
var storeState = store.getStoreState();
expect(storeState.error).to.be.a.null;
expect(storeState.error).to.be.a.undefined;
expect(storeState.rooms[0].ctime).eql(1405518241);
expect(storeState.rooms[1].ctime).eql(1405517546);
expect(storeState.rooms[2].ctime).eql(1405517418);

View File

@ -161,13 +161,20 @@ describe("loop.shared.views", function() {
});
describe("ConversationView", function() {
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model;
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model, fakeAudio;
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(sharedViews.ConversationView(props));
}
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
@ -350,46 +357,69 @@ describe("loop.shared.views", function() {
});
describe("Model events", function() {
it("should start streaming on session:connected", function() {
model.trigger("session:connected");
sinon.assert.calledOnce(fakeSDK.initPublisher);
});
describe("for standalone", function() {
it("should publish remote stream on session:stream-created",
function() {
var s1 = {connection: {connectionId: 42}};
model.trigger("session:stream-created", {stream: s1});
sinon.assert.calledOnce(fakeSession.subscribe);
sinon.assert.calledWith(fakeSession.subscribe, s1);
beforeEach(function() {
// In standalone, navigator.mozLoop does not exists
if (navigator.hasOwnProperty("mozLoop"))
sandbox.stub(navigator, "mozLoop", undefined);
});
it("should unpublish local stream on session:ended", function() {
comp.startPublishing();
it("should play a connected sound, once, on session:connected",
function() {
model.trigger("session:connected");
model.trigger("session:ended");
sinon.assert.calledOnce(fakeSession.unpublish);
sinon.assert.calledOnce(window.Audio);
sinon.assert.calledWithExactly(
window.Audio, "shared/sounds/connected.ogg");
expect(fakeAudio.loop).to.not.equal(true);
});
});
it("should unpublish local stream on session:peer-hungup", function() {
comp.startPublishing();
describe("for both (standalone and desktop)", function() {
it("should start streaming on session:connected", function() {
model.trigger("session:connected");
model.trigger("session:peer-hungup");
sinon.assert.calledOnce(fakeSDK.initPublisher);
});
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should publish remote stream on session:stream-created",
function() {
var s1 = {connection: {connectionId: 42}};
it("should unpublish local stream on session:network-disconnected",
function() {
model.trigger("session:stream-created", {stream: s1});
sinon.assert.calledOnce(fakeSession.subscribe);
sinon.assert.calledWith(fakeSession.subscribe, s1);
});
it("should unpublish local stream on session:ended", function() {
comp.startPublishing();
model.trigger("session:network-disconnected");
model.trigger("session:ended");
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unpublish local stream on session:peer-hungup", function() {
comp.startPublishing();
model.trigger("session:peer-hungup");
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unpublish local stream on session:network-disconnected",
function() {
comp.startPublishing();
model.trigger("session:network-disconnected");
sinon.assert.calledOnce(fakeSession.unpublish);
});
});
});
describe("Publisher events", function() {
@ -526,7 +556,7 @@ describe("loop.shared.views", function() {
fillSadFeedbackForm(comp, "confusing");
expect(comp.getDOMNode()
.querySelector("form input[type='text']").value).eql("");
.querySelector(".feedback-description").value).eql("");
});
it("should enable the form submit button once a predefined category is " +

View File

@ -575,7 +575,7 @@ describe("loop.webapp", function() {
});
describe("PendingConversationView", function() {
var view, websocket;
var view, websocket, fakeAudio;
beforeEach(function() {
websocket = new loop.CallConnectionWebSocket({
@ -585,6 +585,12 @@ describe("loop.webapp", function() {
});
sinon.stub(websocket, "cancel");
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.PendingConversationView({
@ -593,6 +599,16 @@ describe("loop.webapp", function() {
);
});
describe("#componentDidMount", function() {
it("should play a looped connecting sound", function() {
sinon.assert.calledOnce(window.Audio);
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/connecting.ogg");
expect(fakeAudio.loop).to.equal(true);
});
});
describe("#_cancelOutgoingCall", function() {
it("should inform the websocket to cancel the setup", function() {
var button = view.getDOMNode().querySelector(".btn-cancel");
@ -609,6 +625,13 @@ describe("loop.webapp", function() {
expect(view.state.callState).to.be.equal("ringing");
});
it("should play a looped ringing sound", function() {
websocket.trigger("progress:alerting");
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/ringing.ogg");
expect(fakeAudio.loop).to.equal(true);
});
});
});
});
@ -843,9 +866,16 @@ describe("loop.webapp", function() {
});
describe("EndedConversationView", function() {
var view, conversation;
var view, conversation, fakeAudio;
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
conversation = new sharedModels.ConversationModel({}, {
sdk: {}
});
@ -866,6 +896,17 @@ describe("loop.webapp", function() {
it("should render a FeedbackView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
});
describe("#componentDidMount", function() {
it("should play a terminating sound, once", function() {
sinon.assert.calledOnce(window.Audio);
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/terminated.ogg");
expect(fakeAudio.loop).to.not.equal(true);
});
});
});
describe("PromoteFirefoxView", function() {

View File

@ -40,8 +40,11 @@ function setupFakeLoopServer() {
Services.prefs.setCharPref("loop.server",
"http://localhost:" + loopServer.identity.primaryPort);
MozLoopServiceInternal.mocks.pushHandler = mockPushHandler;
do_register_cleanup(function() {
loopServer.stop(function() {});
MozLoopServiceInternal.mocks.pushHandler = undefined;
});
}

View File

@ -19,17 +19,12 @@ let msgHandler = function(msg) {
msg.reason === "busy") {
actionReceived = true;
}
}
let mockWebSocket = new MockWebSocketChannel({defaultMsgHandler: msgHandler});
LoopCallsInternal._mocks.webSocket = mockWebSocket;
Services.io.offline = false;
};
add_test(function test_busy_2guest_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
MozLoopService.register().then(() => {
let opened = 0;
Chat.open = function() {
opened++;
@ -52,7 +47,7 @@ add_test(function test_busy_2guest_calls() {
add_test(function test_busy_1fxa_1guest_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
MozLoopService.register().then(() => {
let opened = 0;
Chat.open = function() {
opened++;
@ -76,7 +71,7 @@ add_test(function test_busy_1fxa_1guest_calls() {
add_test(function test_busy_2fxa_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
MozLoopService.register().then(() => {
let opened = 0;
Chat.open = function() {
opened++;
@ -99,7 +94,7 @@ add_test(function test_busy_2fxa_calls() {
add_test(function test_busy_1guest_1fxa_calls() {
actionReceived = false;
MozLoopService.register(mockPushHandler, mockWebSocket).then(() => {
MozLoopService.register().then(() => {
let opened = 0;
Chat.open = function() {
opened++;
@ -120,8 +115,7 @@ add_test(function test_busy_1guest_1fxa_calls() {
});
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
// Setup fake login state so we get FxA requests.
@ -129,6 +123,11 @@ function run_test()
MozLoopServiceInternal.fxAOAuthTokenData = {token_type:"bearer",access_token:"1bad3e44b12f77a88fe09f016f6a37c42e40f974bc7a8b432bb0d2f0e37e1752",scope:"profile"};
MozLoopServiceInternal.fxAOAuthProfile = {email: "test@example.com", uid: "abcd1234"};
let mockWebSocket = new MockWebSocketChannel({defaultMsgHandler: msgHandler});
LoopCallsInternal.mocks.webSocket = mockWebSocket;
Services.io.offline = false;
// For each notification received from the PushServer, MozLoopService will first query
// for any pending calls on the FxA hawk session and then again using the guest session.
// A pair of response objects in the callsResponses array will be consumed for each
@ -178,7 +177,7 @@ function run_test()
// clear test pref
Services.prefs.clearUserPref("loop.seenToS");
LoopCallsInternal._mocks.webSocket = undefined;
LoopCallsInternal.mocks.webSocket = undefined;
});
run_next_test();

View File

@ -31,7 +31,7 @@ add_test(function test_set_do_not_disturb() {
add_test(function test_do_not_disturb_disabled_should_open_chat_window() {
MozLoopService.doNotDisturb = false;
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
let opened = false;
Chat.open = function() {
opened = true;
@ -64,8 +64,7 @@ add_test(function test_do_not_disturb_enabled_shouldnt_open_chat_window() {
});
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
loopServer.registerPathHandler("/registration", (request, response) => {

View File

@ -10,7 +10,7 @@ let openChatOrig = Chat.open;
add_test(function test_openChatWindow_on_notification() {
Services.prefs.setCharPref("loop.seenToS", "unseen");
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
let opened = false;
Chat.open = function() {
opened = true;
@ -32,8 +32,7 @@ add_test(function test_openChatWindow_on_notification() {
});
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
loopServer.registerPathHandler("/registration", (request, response) => {

View File

@ -17,7 +17,7 @@ Cu.import("resource://services-common/utils.js");
add_test(function test_register_websocket_success_loop_server_fail() {
mockPushHandler.registrationResult = "404";
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
do_throw("should not succeed when loop server registration fails");
}, (err) => {
// 404 is an expected failure indicated by the lack of route being set
@ -49,15 +49,14 @@ add_test(function test_register_success() {
response.processAsync();
response.finish();
});
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
run_next_test();
}, err => {
do_throw("shouldn't error on a successful request");
});
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
do_register_cleanup(function() {

View File

@ -26,7 +26,7 @@ add_task(function test_initialize_with_expired_urls_and_no_auth_token() {
Services.prefs.setIntPref(LOOP_URL_EXPIRY_PREF, nowSeconds - 2);
Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
yield MozLoopService.initialize(mockPushHandler).then((msg) => {
yield MozLoopService.initialize().then((msg) => {
Assert.equal(msg, "registration not needed", "Initialize should not register when the " +
"URLs are expired and there are no auth tokens");
}, (error) => {
@ -42,7 +42,7 @@ add_task(function test_initialize_with_urls_and_no_auth_token() {
response.setStatusLine(null, 200, "OK");
});
yield MozLoopService.initialize(mockPushHandler).then((msg) => {
yield MozLoopService.initialize().then((msg) => {
Assert.equal(msg, "initialized to guest status", "Initialize should register as a " +
"guest when no auth tokens but expired URLs");
}, (error) => {
@ -66,12 +66,11 @@ add_task(function test_initialize_with_invalid_fxa_token() {
}));
});
yield MozLoopService.initialize(mockPushHandler).then(() => {
yield MozLoopService.initialize().then(() => {
Assert.ok(false, "Initializing with an invalid token should reject the promise");
},
(error) => {
let pushHandler = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gPushHandler;
Assert.equal(pushHandler.pushUrl, kEndPointUrl, "Push URL should match");
Assert.equal(MozLoopServiceInternal.pushHandler.pushUrl, kEndPointUrl, "Push URL should match");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), "",
"FXA pref should be cleared if token was invalid");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), "",
@ -86,7 +85,7 @@ add_task(function test_initialize_with_fxa_token() {
response.setStatusLine(null, 200, "OK");
});
yield MozLoopService.initialize(mockPushHandler).then(() => {
yield MozLoopService.initialize().then(() => {
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_TOKEN_PREF), FAKE_FXA_TOKEN_DATA,
"FXA pref should still be set after initialization");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), FAKE_FXA_PROFILE,
@ -98,9 +97,11 @@ function run_test() {
setupFakeLoopServer();
// Note, this is just used to speed up the test.
Services.prefs.setIntPref(LOOP_INITIAL_DELAY_PREF, 0);
MozLoopServiceInternal.mocks.pushHandler = mockPushHandler;
mockPushHandler.pushUrl = kEndPointUrl;
do_register_cleanup(function() {
MozLoopServiceInternal.mocks.pushHandler = undefined;
Services.prefs.clearUserPref(LOOP_INITIAL_DELAY_PREF);
Services.prefs.clearUserPref(LOOP_FXA_TOKEN_PREF);
Services.prefs.clearUserPref(LOOP_FXA_PROFILE_PREF);

View File

@ -31,7 +31,7 @@ add_test(function test_registration_invalid_token() {
response.finish();
});
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
// Due to the way the time stamp checking code works in hawkclient, we expect a couple
// of authorization requests before we reset the token.
Assert.equal(authorizationAttempts, 2);
@ -43,8 +43,7 @@ add_test(function test_registration_invalid_token() {
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
do_register_cleanup(function() {

View File

@ -16,7 +16,7 @@ add_test(function test_registration_returns_hawk_session_token() {
response.finish();
});
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
var hawkSessionPref;
try {
hawkSessionPref = Services.prefs.getCharPref("loop.hawk-session-token");
@ -31,8 +31,7 @@ add_test(function test_registration_returns_hawk_session_token() {
});
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
do_register_cleanup(function() {

View File

@ -24,7 +24,7 @@ add_test(function test_registration_uses_hawk_session_token() {
response.finish();
});
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
run_next_test();
}, err => {
do_throw("shouldn't error on a succesful request");
@ -32,8 +32,7 @@ add_test(function test_registration_uses_hawk_session_token() {
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
do_register_cleanup(function() {

View File

@ -16,7 +16,7 @@ add_test(function test_registration_handles_bogus_hawk_token() {
response.finish();
});
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
do_throw("should not succeed with a bogus token");
}, err => {
@ -36,8 +36,7 @@ add_test(function test_registration_handles_bogus_hawk_token() {
});
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
do_register_cleanup(function() {
@ -45,5 +44,4 @@ function run_test()
});
run_next_test();
}

View File

@ -83,7 +83,7 @@ add_test(function test_getAllRooms() {
returnRoomDetails(response, "Third Room Name");
});
MozLoopService.register(mockPushHandler).then(() => {
MozLoopService.register().then(() => {
LoopRooms.getAll((error, rooms) => {
do_check_false(error);
@ -113,8 +113,7 @@ add_test(function test_getAllRooms() {
});
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
mockPushHandler.registrationPushURL = kEndPointUrl;

View File

@ -2,6 +2,47 @@
* 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/. */
// Sample from https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms
var fakeRooms = [
{
"roomToken": "_nxD4V4FflQ",
"roomName": "First Room Name",
"roomUrl": "http://localhost:3000/rooms/_nxD4V4FflQ",
"roomOwner": "Alexis",
"maxSize": 2,
"creationTime": 1405517546,
"ctime": 1405517546,
"expiresAt": 1405534180,
"participants": []
},
{
"roomToken": "QzBbvGmIZWU",
"roomName": "Second Room Name",
"roomUrl": "http://localhost:3000/rooms/QzBbvGmIZWU",
"roomOwner": "Alexis",
"maxSize": 2,
"creationTime": 1405517546,
"ctime": 1405517546,
"expiresAt": 1405534180,
"participants": []
},
{
"roomToken": "3jKS_Els9IU",
"roomName": "UX Discussion",
"roomUrl": "http://localhost:3000/rooms/3jKS_Els9IU",
"roomOwner": "Alexis",
"maxSize": 2,
"clientMaxSize": 2,
"creationTime": 1405517546,
"ctime": 1405517818,
"expiresAt": 1405534180,
"participants": [
{ "displayName": "Alexis", "account": "alexis@example.com", "roomConnectionId": "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
{ "displayName": "Adam", "roomConnectionId": "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
]
}
];
/**
* Faking the mozLoop object which doesn't exist in regular web pages.
* @type {Object}
@ -22,5 +63,10 @@ navigator.mozLoop = {
},
on: function() {}
},
rooms: {
getAll: function(callback) {
callback(null, fakeRooms);
}
},
fxAEnabled: true
};

View File

@ -59,7 +59,7 @@
var dispatcher = new loop.Dispatcher();
var roomListStore = new loop.store.RoomListStore({
dispatcher: dispatcher,
mozLoop: {}
mozLoop: navigator.mozLoop
});
// Local mocks

View File

@ -59,7 +59,7 @@
var dispatcher = new loop.Dispatcher();
var roomListStore = new loop.store.RoomListStore({
dispatcher: dispatcher,
mozLoop: {}
mozLoop: navigator.mozLoop
});
// Local mocks

View File

@ -97,6 +97,19 @@ var gMainPane = {
e10sCheckbox.checked = e10sPref.value || e10sTempPref.value;
#endif
#ifdef MOZ_DEV_EDITION
Cu.import("resource://gre/modules/osfile.jsm");
let uAppData = OS.Constants.Path.userApplicationDataDir;
let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
setEventListener("separateProfileMode", "command", gMainPane.separateProfileModeChange);
let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
setEventListener("getStarted", "click", gMainPane.onGetStarted);
OS.File.stat(ignoreSeparateProfile).then(() => separateProfileModeCheckbox.checked = false,
() => separateProfileModeCheckbox.checked = true);
#endif
// Notify observers that the UI is now ready
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService)
@ -154,6 +167,66 @@ var gMainPane = {
},
#endif
#ifdef MOZ_DEV_EDITION
separateProfileModeChange: function ()
{
function quitApp() {
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestartNotSameProfile);
}
function revertCheckbox(error) {
separateProfileModeCheckbox.checked = !separateProfileModeCheckbox.checked;
if (error) {
Cu.reportError("Failed to toggle separate profile mode: " + error);
}
}
const Cc = Components.classes, Ci = Components.interfaces;
let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
let brandName = document.getElementById("bundleBrand").getString("brandShortName");
let bundle = document.getElementById("bundlePreferences");
let msg = bundle.getFormattedString(separateProfileModeCheckbox.checked ?
"featureEnableRequiresRestart" : "featureDisableRequiresRestart",
[brandName]);
let title = bundle.getFormattedString("shouldRestartTitle", [brandName]);
let shouldProceed = Services.prompt.confirm(window, title, msg)
if (shouldProceed) {
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
.createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, "quit-application-requested",
"restart");
shouldProceed = !cancelQuit.data;
if (shouldProceed) {
Cu.import("resource://gre/modules/osfile.jsm");
let uAppData = OS.Constants.Path.userApplicationDataDir;
let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
if (separateProfileModeCheckbox.checked) {
OS.File.remove(ignoreSeparateProfile).then(quitApp, revertCheckbox);
} else {
OS.File.writeAtomic(ignoreSeparateProfile, new Uint8Array()).then(quitApp, revertCheckbox);
}
return;
}
}
// Revert the checkbox in case we didn't quit
revertCheckbox();
},
onGetStarted: function (aEvent) {
const Cc = Components.classes, Ci = Components.interfaces;
let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
.getService(Ci.nsIWindowMediator);
let win = wm.getMostRecentWindow("navigator:browser");
if (win) {
let accountsTab = win.gBrowser.addTab("about:accounts?action=migrateToDevEdition");
win.gBrowser.selectedTab = accountsTab;
}
},
#endif
// HOME PAGE
/*

View File

@ -117,6 +117,17 @@
hidden="true">
<caption><label>&startup.label;</label></caption>
#ifdef MOZ_DEV_EDITION
<vbox id="separateProfileBox">
<checkbox id="separateProfileMode"
label="&separateProfileMode.label;"/>
<hbox align="center" class="indent">
<label id="useFirefoxSync">&useFirefoxSync.label;</label>
<label id="getStarted" class="text-link">&getStarted.label;</label>
</hbox>
</vbox>
#endif
#ifdef E10S_TESTING_ONLY
<checkbox id="e10sAutoStart"
label="Enable E10S (multi-process)"/>
@ -126,13 +137,13 @@
<vbox id="defaultBrowserBox">
<hbox align="center">
<checkbox id="alwaysCheckDefault" preference="browser.shell.checkDefaultBrowser"
label="&alwaysCheckDefault.label;" accesskey="&alwaysCheckDefault2.accesskey;"/>
label="&alwaysCheckDefault2.label;" accesskey="&alwaysCheckDefault2.accesskey;"/>
</hbox>
<deck id="setDefaultPane">
<hbox align="center" class="indent">
<label id="isNotDefaultLabel" flex="1">&isNotDefault.label;</label>
<button id="setDefaultButton"
label="&setAsMyDefaultBrowser.label;" accesskey="&setAsMyDefaultBrowser.accesskey;"
label="&setAsMyDefaultBrowser2.label;" accesskey="&setAsMyDefaultBrowser2.accesskey;"
preference="pref.general.disable_button.default_browser"/>
</hbox>
<hbox align="center" class="indent">

View File

@ -95,6 +95,8 @@ function gotoPref(aCategory) {
categories.selectedItem = item;
window.history.replaceState(category, document.title);
search(category, "data-category");
let mainContent = document.querySelector(".main-content");
mainContent.scrollTop = 0;
}
function search(aQuery, aAttribute) {

View File

@ -7,8 +7,9 @@ support-files =
[browser_advanced_update.js]
[browser_bug410900.js]
[browser_bug731866.js]
[browser_bug1020245_openPreferences_to_paneContent.js]
[browser_bug795764_cachedisabled.js]
[browser_bug1018066_resetScrollPosition.js]
[browser_bug1020245_openPreferences_to_paneContent.js]
[browser_connection.js]
[browser_connection_bug388287.js]
[browser_healthreport.js]

View File

@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Services.prefs.setBoolPref("browser.preferences.inContent", true);
let originalWindowHeight;
registerCleanupFunction(function() {
Services.prefs.clearUserPref("browser.preferences.inContent");
window.resizeTo(window.outerWidth, originalWindowHeight);
while (gBrowser.tabs[1])
gBrowser.removeTab(gBrowser.tabs[1]);
});
add_task(function() {
originalWindowHeight = window.outerHeight;
window.resizeTo(window.outerWidth, 300);
let prefs = yield openPreferencesViaOpenPreferencesAPI("paneApplications", undefined, {leaveOpen: true});
is(prefs.selectedPane, "paneApplications", "Applications pane was selected");
let mainContent = gBrowser.contentDocument.querySelector(".main-content");
mainContent.scrollTop = 50;
is(mainContent.scrollTop, 50, "main-content should be scrolled 50 pixels");
gBrowser.contentWindow.gotoPref("paneGeneral");
is(mainContent.scrollTop, 0,
"Switching to a different category should reset the scroll position");
});

View File

@ -25,28 +25,6 @@ add_task(function() {
is(prefs.selectedPane, "paneGeneral", "General pane is selected by default");
});
function openPreferencesViaOpenPreferencesAPI(aPane, aAdvancedTab) {
let deferred = Promise.defer();
gBrowser.selectedTab = gBrowser.addTab("about:blank");
openPreferences(aPane, aAdvancedTab ? {advancedTab: aAdvancedTab} : undefined);
let newTabBrowser = gBrowser.selectedBrowser;
newTabBrowser.addEventListener("Initialized", function PrefInit() {
newTabBrowser.removeEventListener("Initialized", PrefInit, true);
newTabBrowser.contentWindow.addEventListener("load", function prefLoad() {
newTabBrowser.contentWindow.removeEventListener("load", prefLoad);
let win = gBrowser.contentWindow;
let selectedPane = win.history.state;
let doc = win.document;
let selectedAdvancedTab = aAdvancedTab && doc.getElementById("advancedPrefs").selectedTab.id;
gBrowser.removeCurrentTab();
deferred.resolve({selectedPane: selectedPane, selectedAdvancedTab: selectedAdvancedTab});
});
}, true);
return deferred.promise;
}
function openPreferencesViaHash(aPane) {
let deferred = Promise.defer();
gBrowser.selectedTab = gBrowser.addTab("about:preferences" + (aPane ? "#" + aPane : ""));

View File

@ -117,3 +117,25 @@ function waitForEvent(aSubject, aEventName, aTimeoutMs, aTarget) {
return eventDeferred.promise.then(cleanup, cleanup);
}
function openPreferencesViaOpenPreferencesAPI(aPane, aAdvancedTab, aOptions) {
let deferred = Promise.defer();
gBrowser.selectedTab = gBrowser.addTab("about:blank");
openPreferences(aPane, aAdvancedTab ? {advancedTab: aAdvancedTab} : undefined);
let newTabBrowser = gBrowser.selectedBrowser;
newTabBrowser.addEventListener("Initialized", function PrefInit() {
newTabBrowser.removeEventListener("Initialized", PrefInit, true);
newTabBrowser.contentWindow.addEventListener("load", function prefLoad() {
newTabBrowser.contentWindow.removeEventListener("load", prefLoad);
let win = gBrowser.contentWindow;
let selectedPane = win.history.state;
let doc = win.document;
let selectedAdvancedTab = aAdvancedTab && doc.getElementById("advancedPrefs").selectedTab.id;
if (!aOptions || !aOptions.leaveOpen)
gBrowser.removeCurrentTab();
deferred.resolve({selectedPane: selectedPane, selectedAdvancedTab: selectedAdvancedTab});
});
}, true);
return deferred.promise;
}

View File

@ -60,12 +60,87 @@ var gMainPane = {
this.updateBrowserStartupLastSession();
#ifdef MOZ_DEV_EDITION
let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
let listener = gMainPane.separateProfileModeChange.bind(gMainPane);
separateProfileModeCheckbox.addEventListener("command", listener);
let getStartedLink = document.getElementById("getStarted");
let syncListener = gMainPane.onGetStarted.bind(gMainPane);
getStartedLink.addEventListener("click", syncListener);
Cu.import("resource://gre/modules/osfile.jsm");
let uAppData = OS.Constants.Path.userApplicationDataDir;
let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
OS.File.stat(ignoreSeparateProfile).then(() => separateProfileModeCheckbox.checked = false,
() => separateProfileModeCheckbox.checked = true);
#endif
// Notify observers that the UI is now ready
Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService)
.notifyObservers(window, "main-pane-loaded", null);
},
#ifdef MOZ_DEV_EDITION
separateProfileModeChange: function ()
{
function quitApp() {
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestartNotSameProfile);
}
function revertCheckbox(error) {
separateProfileModeCheckbox.checked = !separateProfileModeCheckbox.checked;
if (error) {
Cu.reportError("Failed to toggle separate profile mode: " + error);
}
}
let separateProfileModeCheckbox = document.getElementById("separateProfileMode");
let brandName = document.getElementById("bundleBrand").getString("brandShortName");
let bundle = document.getElementById("bundlePreferences");
let msg = bundle.getFormattedString(separateProfileModeCheckbox.checked ?
"featureEnableRequiresRestart" : "featureDisableRequiresRestart",
[brandName]);
let title = bundle.getFormattedString("shouldRestartTitle", [brandName]);
let shouldProceed = Services.prompt.confirm(window, title, msg)
if (shouldProceed) {
let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"]
.createInstance(Ci.nsISupportsPRBool);
Services.obs.notifyObservers(cancelQuit, "quit-application-requested",
"restart");
shouldProceed = !cancelQuit.data;
if (shouldProceed) {
Cu.import("resource://gre/modules/osfile.jsm");
let uAppData = OS.Constants.Path.userApplicationDataDir;
let ignoreSeparateProfile = OS.Path.join(uAppData, "ignore-dev-edition-profile");
if (separateProfileModeCheckbox.checked) {
OS.File.remove(ignoreSeparateProfile).then(quitApp, revertCheckbox);
} else {
OS.File.writeAtomic(ignoreSeparateProfile, new Uint8Array()).then(quitApp, revertCheckbox);
}
}
}
// Revert the checkbox in case we didn't quit
revertCheckbox();
},
onGetStarted: function (aEvent) {
const Cc = Components.classes, Ci = Components.interfaces;
let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
.getService(Ci.nsIWindowMediator);
let win = wm.getMostRecentWindow("navigator:browser");
if (win) {
let accountsTab = win.gBrowser.addTab("about:accounts?action=migrateToDevEdition");
win.gBrowser.selectedTab = accountsTab;
}
},
#endif
// HOME PAGE
/*

View File

@ -77,17 +77,28 @@
<groupbox id="startupGroup">
<caption label="&startup.label;"/>
#ifdef MOZ_DEV_EDITION
<vbox id="separateProfileBox">
<checkbox id="separateProfileMode"
label="&separateProfileMode.label;"/>
<hbox align="center" class="indent">
<label id="useFirefoxSync">&useFirefoxSync.label;</label>
<label id="getStarted" class="text-link">&getStarted.label;</label>
</hbox>
</vbox>
#endif
#ifdef HAVE_SHELL_SERVICE
<vbox id="defaultBrowserBox">
<hbox align="center">
<checkbox id="alwaysCheckDefault" preference="browser.shell.checkDefaultBrowser"
label="&alwaysCheckDefault.label;" accesskey="&alwaysCheckDefault2.accesskey;"/>
label="&alwaysCheckDefault2.label;" accesskey="&alwaysCheckDefault2.accesskey;"/>
</hbox>
<deck id="setDefaultPane">
<hbox align="center" class="indent">
<label id="isNotDefaultLabel" flex="1">&isNotDefault.label;</label>
<button id="setDefaultButton"
label="&setAsMyDefaultBrowser.label;" accesskey="&setAsMyDefaultBrowser.accesskey;"
label="&setAsMyDefaultBrowser2.label;" accesskey="&setAsMyDefaultBrowser2.accesskey;"
oncommand="gMainPane.setDefaultBrowser();"
preference="pref.general.disable_button.default_browser"/>
</hbox>

View File

@ -594,7 +594,7 @@ let SessionStoreInternal = {
// manager message, so the target will be a <xul:browser>.
var browser = aMessage.target;
var win = browser.ownerDocument.defaultView;
let tab = this._getTabForBrowser(browser);
let tab = win.gBrowser.getTabForBrowser(browser);
if (!tab) {
// Ignore messages from <browser> elements that are not tabs.
return;
@ -2971,24 +2971,6 @@ let SessionStoreInternal = {
return window;
},
/**
* Gets the tab for the given browser. This should be marginally better
* than using tabbrowser's getTabForContentWindow. This assumes the browser
* is the linkedBrowser of a tab, not a dangling browser.
*
* @param aBrowser
* The browser from which to get the tab.
*/
_getTabForBrowser: function ssi_getTabForBrowser(aBrowser) {
let window = aBrowser.ownerDocument.defaultView;
for (let i = 0; i < window.gBrowser.tabs.length; i++) {
let tab = window.gBrowser.tabs[i];
if (tab.linkedBrowser == aBrowser)
return tab;
}
return undefined;
},
/**
* Whether or not to resume session, if not recovering from a crash.
* @returns bool

View File

@ -31,10 +31,13 @@
<!ENTITY alwaysAsk.label "Always ask me where to save files">
<!ENTITY alwaysAsk.accesskey "A">
<!ENTITY alwaysCheckDefault.label "Always check to see if &brandShortName; is the default browser on startup">
<!ENTITY alwaysCheckDefault2.label "Always check if &brandShortName; is your default browser">
<!ENTITY alwaysCheckDefault2.accesskey "w">
<!ENTITY setAsMyDefaultBrowser.label "Make &brandShortName; My Default Browser">
<!ENTITY setAsMyDefaultBrowser.accesskey "D">
<!ENTITY setAsMyDefaultBrowser2.label "Make Default">
<!ENTITY setAsMyDefaultBrowser2.accesskey "D">
<!ENTITY isDefault.label "&brandShortName; is currently your default browser">
<!ENTITY isNotDefault.label "&brandShortName; is not your default browser">
<!ENTITY separateProfileMode.label "Allow &brandShortName; and Firefox to run at the same time">
<!ENTITY useFirefoxSync.label "Tip: This uses separate profiles. Use Sync to share data between them.">
<!ENTITY getStarted.label "Start using Sync…">

View File

@ -55,6 +55,11 @@ label.small {
}
/* General Pane */
#useFirefoxSync,
#getStarted {
font-size: 90%;
}
#isNotDefaultLabel {
font-weight: bold;
}

View File

@ -169,6 +169,11 @@ caption {
/* General Pane */
#useFirefoxSync,
#getStarted {
font-size: 90%;
}
#isNotDefaultLabel {
font-weight: bold;
}

View File

@ -110,6 +110,15 @@ treecol {
/* General Pane */
#useFirefoxSync {
font-size: 90%;
-moz-margin-end: 8px !important;
}
#getStarted {
font-size: 90%;
}
#isNotDefaultLabel {
font-weight: bold;
}

View File

@ -55,6 +55,11 @@ label.small {
/* General Pane */
#useFirefoxSync,
#getStarted {
font-size: 90%;
}
#isNotDefaultLabel {
font-weight: bold;
}

View File

@ -625,7 +625,7 @@ class ReftestOptions(OptionParser):
# Certain paths do not make sense when we're cross compiling Fennec. This
# logic is cribbed from the example in
# python/mozbuild/mozbuild/mach_commands.py.
defaults['appname'] = build_obj.get_binary_path() if \
defaults['app'] = build_obj.get_binary_path() if \
build_obj and build_obj.substs['MOZ_BUILD_APP'] != 'mobile/android' else None
self.add_option("--extra-profile-file",

View File

@ -104,6 +104,7 @@ skip-if = android_version == "10"
[testJNI]
# [testMozPay] # see bug 945675
[testNetworkManager]
[testOfflinePage]
[testOrderedBroadcast]
[testOSLocale]
[testResourceSubstitutions]

View File

@ -0,0 +1,11 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.tests;
public class testOfflinePage extends JavascriptTest {
public testOfflinePage() {
super("testOfflinePage.js");
}
}

View File

@ -0,0 +1,107 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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";
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Messaging.jsm");
function ok(passed, text) {
do_report_result(passed, text, Components.stack.caller, false);
}
function is(lhs, rhs, text) {
do_report_result(lhs === rhs, text, Components.stack.caller, false);
}
// The chrome window
let chromeWin;
// Track the <browser> where the tests are happening
let browser;
// The proxy setting
let proxyPrefValue;
const kUniqueURI = Services.io.newURI("http://mochi.test:8888/tests/robocop/video_controls.html", null, null);
add_test(function setup_browser() {
// Tests always connect to localhost, and per bug 87717, localhost is now
// reachable in offline mode. To avoid this, disable any proxy.
proxyPrefValue = Services.prefs.getIntPref("network.proxy.type");
Services.prefs.setIntPref("network.proxy.type", 0);
// Clear network cache.
Cc["@mozilla.org/netwerk/cache-storage-service;1"].getService(Ci.nsICacheStorageService).clear();
chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
let BrowserApp = chromeWin.BrowserApp;
do_register_cleanup(function cleanup() {
BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
Services.io.offline = false;
});
do_test_pending();
// Add a new tab with a blank page so we can better control the real page load and the offline state
browser = BrowserApp.addTab("about:blank", { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
// Go offline, expecting the error page.
Services.io.offline = true;
// Load our test web page
browser.addEventListener("DOMContentLoaded", errorListener, true);
browser.loadURI(kUniqueURI.spec, null, null)
});
//------------------------------------------------------------------------------
// listen to loading the neterror page. (offline mode)
function errorListener() {
if (browser.contentWindow.location == "about:blank") {
do_print("got about:blank, which is expected once, so return");
return;
}
browser.removeEventListener("DOMContentLoaded", errorListener, true);
ok(Services.io.offline, "Services.io.offline is true.");
// This is an error page.
is(browser.contentDocument.documentURI.substring(0, 27), "about:neterror?e=netOffline", "Document URI is the error page.");
// But location bar should show the original request.
is(browser.contentWindow.location.href, kUniqueURI.spec, "Docshell URI is the original URI.");
Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
// Now press the "Try Again" button, with offline mode off.
Services.io.offline = false;
browser.addEventListener("DOMContentLoaded", reloadListener, true);
ok(browser.contentDocument.getElementById("errorTryAgain"), "The error page has got a #errorTryAgain element");
browser.contentDocument.getElementById("errorTryAgain").click();
}
//------------------------------------------------------------------------------
// listen to reload of neterror.
function reloadListener() {
browser.removeEventListener("DOMContentLoaded", reloadListener, true);
ok(!Services.io.offline, "Services.io.offline is false.");
// This is not an error page.
is(browser.contentDocument.documentURI, kUniqueURI.spec, "Document URI is not the offline-error page, but the original URI.");
do_test_finished();
run_next_test();
}
run_next_test();

View File

@ -159,6 +159,38 @@ let Bookmarks = Object.freeze({
}
let item = yield insertBookmark(insertInfo, parent);
// Notify onItemAdded to listeners.
let observers = PlacesUtils.bookmarks.getObservers();
// We need the itemId to notify, though once the switch to guids is
// complete we may stop using it.
let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
let itemId = yield PlacesUtils.promiseItemId(item.guid);
notify(observers, "onItemAdded", [ itemId, parent._id, item.index,
item.type, uri, item.title || null,
toPRTime(item.dateAdded), item.guid,
item.parentGuid ]);
// If a keyword is defined, notify onItemChanged for it.
if (item.keyword) {
notify(observers, "onItemChanged", [ itemId, "keyword", false,
item.keyword,
toPRTime(item.lastModified),
item.type, parent._id, item.guid,
item.parentGuid ]);
}
// If it's a tag, notify OnItemChanged to all bookmarks for this URL.
let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
if (isTagging) {
for (let entry of (yield fetchBookmarksByURL(item))) {
notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
toPRTime(entry.lastModified),
entry.type, entry._parentId,
entry.guid, entry.parentGuid ]);
}
}
// Remove non-enumerable properties.
return Object.assign({}, item);
}.bind(this));
@ -275,6 +307,59 @@ let Bookmarks = Object.freeze({
updateFrecency(db, [updatedItem.url]).then(null, Cu.reportError);
}
// Notify onItemChanged to listeners.
let observers = PlacesUtils.bookmarks.getObservers();
// For lastModified, we only care about the original input, since we
// should not notify implciit lastModified changes.
if (info.hasOwnProperty("lastModified") &&
updateInfo.hasOwnProperty("lastModified") &&
item.lastModified != updatedItem.lastModified) {
notify(observers, "onItemChanged", [ updatedItem._id, "lastModified",
false,
`${toPRTime(updatedItem.lastModified)}`,
toPRTime(updatedItem.lastModified),
updatedItem.type,
updatedItem._parentId,
updatedItem.guid,
updatedItem.parentGuid ]);
}
if (updateInfo.hasOwnProperty("title")) {
notify(observers, "onItemChanged", [ updatedItem._id, "title",
false, updatedItem.title,
toPRTime(updatedItem.lastModified),
updatedItem.type,
updatedItem._parentId,
updatedItem.guid,
updatedItem.parentGuid ]);
}
if (updateInfo.hasOwnProperty("url")) {
notify(observers, "onItemChanged", [ updatedItem._id, "uri",
false, updatedItem.url.href,
toPRTime(updatedItem.lastModified),
updatedItem.type,
updatedItem._parentId,
updatedItem.guid,
updatedItem.parentGuid ]);
}
if (updateInfo.hasOwnProperty("keyword")) {
notify(observers, "onItemChanged", [ updatedItem._id, "keyword",
false, updatedItem.keyword,
toPRTime(updatedItem.lastModified),
updatedItem.type,
updatedItem._parentId,
updatedItem.guid,
updatedItem.parentGuid ]);
}
// If the item was move, notify onItemMoved.
if (item.parentGuid != updatedItem.parentGuid ||
item.index != updatedItem.index) {
notify(observers, "onItemMoved", [ updatedItem._id, item._parentId,
item.index, updatedItem._parentId,
updatedItem.index, updatedItem.type,
updatedItem.guid, item.parentGuid,
updatedItem.newParentGuid ]);
}
// Remove non-enumerable properties.
return Object.assign({}, updatedItem);
}.bind(this));
@ -313,8 +398,25 @@ let Bookmarks = Object.freeze({
if (!item._parentId || item._parentId == PlacesUtils.placesRootId)
throw new Error("It's not possible to remove Places root folders.");
let isUntagging = item._grandParentId == PlacesUtils.bookmarks.tagsFolderId;
item = yield removeBookmark(item, isUntagging);
item = yield removeBookmark(item);
// Notify onItemRemoved to listeners.
let observers = PlacesUtils.bookmarks.getObservers();
let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
notify(observers, "onItemRemoved", [ item._id, item._parentId, item.index,
item.type, uri, item.guid,
item.parentGuid ]);
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
if (isUntagging) {
for (let entry of (yield fetchBookmarksByURL(item))) {
notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
toPRTime(entry.lastModified),
entry.type, entry._parentId,
entry.guid, entry.parentGuid ]);
}
}
// Remove non-enumerable properties.
return Object.assign({}, item);
});
@ -335,15 +437,16 @@ let Bookmarks = Object.freeze({
let rows = yield db.executeCached(
`WITH RECURSIVE
descendants(did) AS (
SELECT folder_id FROM moz_bookmarks_roots
WHERE root_name IN ("toolbar", "menu", "unfiled")
SELECT id FROM moz_bookmarks
WHERE parent IN (SELECT folder_id FROM moz_bookmarks_roots
WHERE root_name IN ("toolbar", "menu", "unfiled"))
UNION ALL
SELECT id FROM moz_bookmarks
JOIN descendants ON parent = did
)
SELECT b.id AS _id, b.parent AS _parentId, b.position AS 'index',
b.type, url, b.guid, p.guid AS parentGuid, b.dateAdded,
b.lastModified, b.title, NULL AS _grandParentId,
b.lastModified, b.title, p.parent AS _grandParentId,
NULL AS _childCount, NULL AS keyword
FROM moz_bookmarks b
JOIN moz_bookmarks p ON p.id = b.parent
@ -355,8 +458,9 @@ let Bookmarks = Object.freeze({
yield db.executeCached(
`WITH RECURSIVE
descendants(did) AS (
SELECT folder_id FROM moz_bookmarks_roots
WHERE root_name IN ("toolbar", "menu", "unfiled")
SELECT id FROM moz_bookmarks
WHERE parent IN (SELECT folder_id FROM moz_bookmarks_roots
WHERE root_name IN ("toolbar", "menu", "unfiled"))
UNION ALL
SELECT id FROM moz_bookmarks
JOIN descendants ON parent = did
@ -380,9 +484,28 @@ let Bookmarks = Object.freeze({
let urls = [for (item of items) if (item.url) item.url];
updateFrecency(db, urls).then(null, Cu.reportError);
// TODO: send notifications
// Send onItemRemoved notifications to listeners.
// TODO (Bug 1087580): this should send a single clear bookmarks
// notification rather than notifying for each bookmark.
// Notify listeners in reverse order to serve children before parents.
let observers = PlacesUtils.bookmarks.getObservers();
for (let item of items.reverse()) {
let uri = item.hasOwnProperty("url") ? toURI(item.url) : null;
notify(observers, "onItemRemoved", [ item._id, item._parentId,
item.index, item.type, uri,
item.guid, item.parentGuid ]);
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
if (isUntagging) {
for (let entry of (yield fetchBookmarksByURL(item))) {
notify(observers, "onItemChanged", [ entry._id, "tags", false, "",
toPRTime(entry.lastModified),
entry.type, entry._parentId,
entry.guid, entry.parentGuid ]);
}
}
}
});
}),
@ -582,6 +705,24 @@ let Bookmarks = Object.freeze({
////////////////////////////////////////////////////////////////////////////////
// Globals.
/**
* Sends a bookmarks notification through the given observers.
*
* @param observers
* array of nsINavBookmarkObserver objects.
* @param notification
* the notification name.
* @param args
* array of arguments to pass to the notification.
*/
function notify(observers, notification, args) {
for (let observer of observers) {
try {
observer[notification](...args);
} catch (ex) {}
}
}
XPCOMUtils.defineLazyGetter(this, "DBConnPromised",
() => new Promise((resolve, reject) => {
Sqlite.wrapStorageConnection({ connection: PlacesUtils.history.DBConnection } )
@ -706,9 +847,6 @@ function* updateBookmark(info, item, newParent) {
function* insertBookmark(item, parent) {
let db = yield DBConnPromised;
let isTaggingURL = item.hasOwnProperty("url") &&
parent._parentId == PlacesUtils.bookmarks.tagsFolderId;
// If a guid was not provided, generate one, so we won't need to fetch the
// bookmark just after having created it.
if (!item.hasOwnProperty("guid"))
@ -752,19 +890,12 @@ function* insertBookmark(item, parent) {
});
// If not a tag recalculate frecency...
if (item.type == Bookmarks.TYPE_BOOKMARK && !isTaggingURL) {
let isTagging = parent._parentId == PlacesUtils.tagsFolderId;
if (item.type == Bookmarks.TYPE_BOOKMARK && !isTagging) {
// ...though we don't wait for the calculation.
updateFrecency(db, [item.url]).then(null, Cu.reportError);
}
// Notify onItemAdded
// TODO
// If it's a tag notify OnItemChanged to all bookmarks for this URL.
if (isTaggingURL) {
// TODO
}
// Don't return an empty title to the caller.
if (item.hasOwnProperty("title") && item.title === null)
delete item.title;
@ -858,14 +989,17 @@ function* fetchBookmarksByKeyword(info) {
function* removeBookmark(item) {
let db = yield DBConnPromised;
let isUntagging = item._grandParentId == PlacesUtils.bookmarks.tagsFolderId;
// Remove annotations first. If this is a tag, we can avoid paying that cost.
if (!isUntagging) {
PlacesUtils.annotations.removeItemAnnotations(item._id);
}
let isUntagging = item._grandParentId == PlacesUtils.tagsFolderId;
yield db.executeTransaction(function* transaction() {
// Remove annotations first. If it's a tag, we can avoid paying that cost.
if (!isUntagging) {
// We don't go through the annotations service for this cause otherwise
// we'd get a pointless onItemChanged notification and it would also
// set lastModified to an unexpected value.
yield removeAnnotationsForItem(db, item._id);
}
// Remove the bookmark from the database.
yield db.executeCached(
`DELETE FROM moz_bookmarks WHERE guid = :guid`, { guid: item.guid });
@ -889,14 +1023,6 @@ function* removeBookmark(item) {
updateFrecency(db, [item.url]).then(null, Cu.reportError);
}
// Notify onItemRemoved.
// TODO
// If we are untagging a bookmark, notify OnItemChanged.
if (isUntagging) {
// TODO
}
return item;
}
@ -953,6 +1079,15 @@ function removeSameValueProperties(dest, src) {
}
}
/**
* Converts an URL object to an nsIURI.
*
* @param url
* the URL object to convert.
* @return nsIURI for the given URL.
*/
function toURI(url) NetUtil.newURI(url.href);
/**
* Reverse a host based on the moz_places algorithm, that is reverse the host
* string and add a trialing period. For example "google.com" becomes
@ -1203,6 +1338,27 @@ let removeOrphanAnnotations = Task.async(function* (db) {
`);
});
/**
* Removes annotations for a given item.
*
* @param db
* the Sqlite.jsm connection handle.
* @param itemId
* internal id of the item for which to remove annotations.
*/
let removeAnnotationsForItem = Task.async(function* (db, itemId) {
yield db.executeCached(
`DELETE FROM moz_items_annos
WHERE item_id = :id
`, { id: itemId });
yield db.executeCached(
`DELETE FROM moz_anno_attributes
WHERE id IN (SELECT n.id from moz_anno_attributes n
LEFT JOIN moz_items_annos a ON a.anno_attribute_id = n.id
WHERE a.id ISNULL)
`);
});
/**
* Updates lastModified for all the ancestors of a given folder GUID.
*

View File

@ -223,7 +223,7 @@ interface nsINavBookmarkObserver : nsISupports
* folders. A URI in history can be contained in one or more such folders.
*/
[scriptable, uuid(4C309044-B6DA-4511-AF57-E8940DB00045)]
[scriptable, uuid(b0f9a80a-d7f0-4421-8513-444125f0d828)]
interface nsINavBookmarksService : nsISupports
{
/**
@ -544,6 +544,12 @@ interface nsINavBookmarksService : nsISupports
*/
void removeObserver(in nsINavBookmarkObserver observer);
/**
* Gets an array of registered nsINavBookmarkObserver objects.
*/
void getObservers([optional] out unsigned long count,
[retval, array, size_is(count)] out nsINavBookmarkObserver observers);
/**
* Runs the passed callback inside of a database transaction.
* Use this when a lot of things are about to change, for example

View File

@ -2670,6 +2670,47 @@ nsNavBookmarks::RemoveObserver(nsINavBookmarkObserver* aObserver)
return mObservers.RemoveWeakElement(aObserver);
}
NS_IMETHODIMP
nsNavBookmarks::GetObservers(uint32_t* _count,
nsINavBookmarkObserver*** _observers)
{
NS_ENSURE_ARG_POINTER(_count);
NS_ENSURE_ARG_POINTER(_observers);
*_count = 0;
*_observers = nullptr;
if (!mCanNotify)
return NS_OK;
nsCOMArray<nsINavBookmarkObserver> observers;
// First add the category cache observers.
mCacheObservers.GetEntries(observers);
// Then add the other observers.
for (uint32_t i = 0; i < mObservers.Length(); ++i) {
const nsCOMPtr<nsINavBookmarkObserver> &observer = mObservers.ElementAt(i);
// Skip nullified weak observers.
if (observer)
observers.AppendElement(observer);
}
if (observers.Count() == 0)
return NS_OK;
*_observers = static_cast<nsINavBookmarkObserver**>
(nsMemory::Alloc(observers.Count() * sizeof(nsINavBookmarkObserver*)));
NS_ENSURE_TRUE(*_observers, NS_ERROR_OUT_OF_MEMORY);
*_count = observers.Count();
for (uint32_t i = 0; i < *_count; ++i) {
NS_ADDREF((*_observers)[i] = observers[i]);
}
return NS_OK;
}
void
nsNavBookmarks::NotifyItemVisited(const ItemVisitData& aData)
{

View File

@ -78,6 +78,22 @@ add_task(function* test_eraseEverything() {
Assert.equal(rows.length, 0);
});
add_task(function* test_eraseEverything_roots() {
yield PlacesUtils.bookmarks.eraseEverything();
// Ensure the roots have not been removed.
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
Assert.ok(yield PlacesUtils.bookmarks.fetch(unfiledGuid));
let toolbarGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.toolbarFolderId);
Assert.ok(yield PlacesUtils.bookmarks.fetch(toolbarGuid));
let menuGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.bookmarksMenuFolderId);
Assert.ok(yield PlacesUtils.bookmarks.fetch(menuGuid));
let tagsGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.tagsFolderId);
Assert.ok(yield PlacesUtils.bookmarks.fetch(tagsGuid));
let rootGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.placesRootId);
Assert.ok(yield PlacesUtils.bookmarks.fetch(rootGuid));
});
function run_test() {
run_next_test();
}

View File

@ -0,0 +1,364 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
add_task(function* insert_separator_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let observer = expectNotifications();
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
parentGuid: unfiledGuid});
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemAdded",
arguments: [ itemId, parentId, bm.index, bm.type,
null, null, bm.dateAdded,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* insert_folder_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let observer = expectNotifications();
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: unfiledGuid,
title: "a folder" });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemAdded",
arguments: [ itemId, parentId, bm.index, bm.type,
null, bm.title, bm.dateAdded,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* insert_folder_notitle_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let observer = expectNotifications();
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: unfiledGuid });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemAdded",
arguments: [ itemId, parentId, bm.index, bm.type,
null, null, bm.dateAdded,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* insert_bookmark_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let observer = expectNotifications();
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://example.com/"),
title: "a bookmark" });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemAdded",
arguments: [ itemId, parentId, bm.index, bm.type,
bm.url, bm.title, bm.dateAdded,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* insert_bookmark_notitle_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let observer = expectNotifications();
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://example.com/") });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemAdded",
arguments: [ itemId, parentId, bm.index, bm.type,
bm.url, null, bm.dateAdded,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* insert_bookmark_keyword_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let observer = expectNotifications();
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://example.com/"),
keyword: "kw" });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemAdded",
arguments: [ itemId, parentId, bm.index, bm.type,
bm.url, null, bm.dateAdded,
bm.guid, bm.parentGuid ] },
{ name: "onItemChanged",
arguments: [ itemId, "keyword", false, bm.keyword,
bm.lastModified, bm.type, parentId,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* insert_bookmark_tag_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://tag.example.com/") });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
let tagsGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.tagsFolderId);
let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: tagsGuid,
title: "tag" });
let observer = expectNotifications();
let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: tagFolder.guid,
url: new URL("http://tag.example.com/") });
let tagId = yield PlacesUtils.promiseItemId(tag.guid);
let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid);
observer.check([ { name: "onItemAdded",
arguments: [ tagId, tagParentId, tag.index, tag.type,
tag.url, null, tag.dateAdded,
tag.guid, tag.parentGuid ] },
{ name: "onItemChanged",
arguments: [ tagId, "tags", false, "",
tag.lastModified, tag.type, tagParentId,
tag.guid, tag.parentGuid ] },
{ name: "onItemChanged",
arguments: [ itemId, "tags", false, "",
bm.lastModified, bm.type, parentId,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* update_bookmark_lastModified() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://lastmod.example.com/") });
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
lastModified: new Date() });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemChanged",
arguments: [ itemId, "lastModified", false,
`${bm.lastModified * 1000}`, bm.lastModified,
bm.type, parentId, bm.guid, bm.parentGuid ] }
]);
});
add_task(function* update_bookmark_title() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://title.example.com/") });
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
title: "new title" });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemChanged",
arguments: [ itemId, "title", false, bm.title,
bm.lastModified, bm.type, parentId, bm.guid,
bm.parentGuid ] }
]);
});
add_task(function* update_bookmark_uri() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://url.example.com/") });
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
url: "http://mozilla.org/" });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemChanged",
arguments: [ itemId, "uri", false, bm.url.href,
bm.lastModified, bm.type, parentId, bm.guid,
bm.parentGuid ] }
]);
});
add_task(function* update_bookmark_keyword() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://keyword.example.com/") });
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.update({ guid: bm.guid,
keyword: "kw" });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
observer.check([ { name: "onItemChanged",
arguments: [ itemId, "keyword", false, bm.keyword,
bm.lastModified, bm.type, parentId, bm.guid,
bm.parentGuid ] }
]);
});
add_task(function* remove_bookmark() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://remove.example.com/") });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.remove(bm.guid);
// TODO (Bug 653910): onItemAnnotationRemoved notified even if there were no
// annotations.
observer.check([ { name: "onItemRemoved",
arguments: [ itemId, parentId, bm.index, bm.type, bm.url,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* remove_folder() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: unfiledGuid });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
let observer = expectNotifications();
bm = yield PlacesUtils.bookmarks.remove(bm.guid);
observer.check([ { name: "onItemRemoved",
arguments: [ itemId, parentId, bm.index, bm.type, null,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* remove_bookmark_tag_notification() {
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: unfiledGuid,
url: new URL("http://untag.example.com/") });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
let tagsGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.tagsFolderId);
let tagFolder = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: tagsGuid,
title: "tag" });
let tag = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: tagFolder.guid,
url: new URL("http://untag.example.com/") });
let tagId = yield PlacesUtils.promiseItemId(tag.guid);
let tagParentId = yield PlacesUtils.promiseItemId(tag.parentGuid);
let observer = expectNotifications();
let removed = yield PlacesUtils.bookmarks.remove(tag.guid);
observer.check([ { name: "onItemRemoved",
arguments: [ tagId, tagParentId, tag.index, tag.type,
tag.url, tag.guid, tag.parentGuid ] },
{ name: "onItemChanged",
arguments: [ itemId, "tags", false, "",
bm.lastModified, bm.type, parentId,
bm.guid, bm.parentGuid ] }
]);
});
add_task(function* eraseEverything_notification() {
// Let's start from a clean situation.
yield PlacesUtils.bookmarks.eraseEverything();
let unfiledGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.unfiledBookmarksFolderId);
let folder1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: unfiledGuid });
let folder1Id = yield PlacesUtils.promiseItemId(folder1.guid);
let folder1ParentId = yield PlacesUtils.promiseItemId(folder1.parentGuid);
let bm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: folder1.guid,
url: new URL("http://example.com/") });
let itemId = yield PlacesUtils.promiseItemId(bm.guid);
let parentId = yield PlacesUtils.promiseItemId(bm.parentGuid);
let folder2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_FOLDER,
parentGuid: unfiledGuid });
let folder2Id = yield PlacesUtils.promiseItemId(folder2.guid);
let folder2ParentId = yield PlacesUtils.promiseItemId(folder2.parentGuid);
let toolbarGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.toolbarFolderId);
let toolbarBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: toolbarGuid,
url: new URL("http://example.com/") });
let toolbarBmId = yield PlacesUtils.promiseItemId(toolbarBm.guid);
let toolbarBmParentId = yield PlacesUtils.promiseItemId(toolbarBm.parentGuid);
let menuGuid = yield PlacesUtils.promiseItemGuid(PlacesUtils.bookmarksMenuFolderId);
let menuBm = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: menuGuid,
url: new URL("http://example.com/") });
let menuBmId = yield PlacesUtils.promiseItemId(menuBm.guid);
let menuBmParentId = yield PlacesUtils.promiseItemId(menuBm.parentGuid);
let observer = expectNotifications();
let removed = yield PlacesUtils.bookmarks.eraseEverything();
observer.check([ { name: "onItemRemoved",
arguments: [ menuBmId, menuBmParentId,
menuBm.index, menuBm.type,
menuBm.url, menuBm.guid,
menuBm.parentGuid ] },
{ name: "onItemRemoved",
arguments: [ toolbarBmId, toolbarBmParentId,
toolbarBm.index, toolbarBm.type,
toolbarBm.url, toolbarBm.guid,
toolbarBm.parentGuid ] },
{ name: "onItemRemoved",
arguments: [ folder2Id, folder2ParentId, folder2.index,
folder2.type, null, folder2.guid,
folder2.parentGuid ] },
{ name: "onItemRemoved",
arguments: [ itemId, parentId, bm.index, bm.type,
bm.url, bm.guid, bm.parentGuid ] },
{ name: "onItemRemoved",
arguments: [ folder1Id, folder1ParentId, folder1.index,
folder1.type, null, folder1.guid,
folder1.parentGuid ] }
]);
});
function expectNotifications() {
let notifications = [];
let observer = new Proxy(NavBookmarkObserver, {
get(target, name) {
if (name == "check") {
PlacesUtils.bookmarks.removeObserver(observer);
return expectedNotifications =>
Assert.deepEqual(notifications, expectedNotifications);
}
if (name.startsWith("onItem")) {
return () => {
let args = Array.from(arguments, arg => {
if (arg && arg instanceof Ci.nsIURI)
return new URL(arg.spec);
if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
return new Date(parseInt(arg/1000));
return arg;
});
notifications.push({ name: name, arguments: args });
}
}
if (name in target)
return target[name];
return undefined;
}
});
PlacesUtils.bookmarks.addObserver(observer, false);
return observer;
}
function run_test() {
run_next_test();
}

View File

@ -32,6 +32,7 @@ skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_bookmarks_eraseEverything.js]
[test_bookmarks_fetch.js]
[test_bookmarks_insert.js]
[test_bookmarks_notifications.js]
[test_bookmarks_remove.js]
[test_bookmarks_update.js]
[test_changeBookmarkURI.js]

View File

@ -1,49 +1,57 @@
/* 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/. */
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
// Get services.
let bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
getService(Ci.nsINavBookmarksService);
let os = Cc["@mozilla.org/observer-service;1"].
getService(Ci.nsIObserverService);
let gDummyCreated = false;
let gDummyAdded = false;
let observer = {
observe: function(subject, topic, data) {
if (topic == "dummy-observer-created")
gDummyCreated = true;
else if (topic == "dummy-observer-item-added")
gDummyAdded = true;
},
QueryInterface: XPCOMUtils.generateQI([
Ci.nsIObserver,
Ci.nsISupportsWeakReference,
])
};
function verify() {
do_check_true(gDummyCreated);
do_check_true(gDummyAdded);
do_test_finished();
function run_test() {
run_next_test()
}
// main
function run_test() {
add_task(function* test_observers() {
do_load_manifest("nsDummyObserver.manifest");
os.addObserver(observer, "dummy-observer-created", true);
os.addObserver(observer, "dummy-observer-item-added", true);
let dummyCreated = false;
let dummyReceivedOnItemAdded = false;
Services.obs.addObserver(function created() {
Services.obs.removeObserver(created, "dummy-observer-created");
dummyCreated = true;
}, "dummy-observer-created", false);
Services.obs.addObserver(function added() {
Services.obs.removeObserver(added, "dummy-observer-item-added");
dummyReceivedOnItemAdded = true;
}, "dummy-observer-item-added", false);
let initialObservers = PlacesUtils.bookmarks.getObservers();
// Add a common observer, it should be invoked after the category observer.
let notificationsPromised = new Promise((resolve, reject) => {
PlacesUtils.bookmarks.addObserver( {
__proto__: NavBookmarkObserver.prototype,
onItemAdded() {
let observers = PlacesUtils.bookmarks.getObservers();
Assert.equal(observers.length, initialObservers.length + 1);
// Check the common observer is the last one.
for (let i = 0; i < initialObservers.length; ++i) {
Assert.equal(initialObservers[i], observers[i]);
}
PlacesUtils.bookmarks.removeObserver(this);
observers = PlacesUtils.bookmarks.getObservers();
Assert.equal(observers.length, initialObservers.length);
// Check the category observer has been invoked before this one.
Assert.ok(dummyCreated);
Assert.ok(dummyReceivedOnItemAdded);
resolve();
}
}, false);
});
// Add a bookmark
bs.insertBookmark(bs.unfiledBookmarksFolder, uri("http://typed.mozilla.org"),
bs.DEFAULT_INDEX, "bookmark");
PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
uri("http://typed.mozilla.org"),
PlacesUtils.bookmarks.DEFAULT_INDEX,
"bookmark");
do_test_pending();
do_timeout(1000, verify);
}
yield notificationsPromised;
});

View File

@ -8,40 +8,29 @@
let Cr = Components.results;
/**
* Print some debug message to the console. All arguments will be printed,
* separated by spaces.
*
* @param [arg0, arg1, arg2, ...]
* Any number of arguments to print out
* @usage _("Hello World") -> prints "Hello World"
* @usage _(1, 2, 3) -> prints "1 2 3"
*/
let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" "));
_("Make an array of services to test, each specifying a class id, interface,",
"and an array of function names that don't throw when passed nulls");
// Make an array of services to test, each specifying a class id, interface
// and an array of function names that don't throw when passed nulls
let testServices = [
["browser/nav-history-service;1", "nsINavHistoryService",
["queryStringToQueries", "removePagesByTimeframe", "removePagesFromHost",
"removeVisitsByTimeframe"]],
["browser/nav-bookmarks-service;1","nsINavBookmarksService",
["createFolder"]],
["createFolder", "getObservers"]],
["browser/livemark-service;2","mozIAsyncLivemarks", ["reloadLivemarks"]],
["browser/annotation-service;1","nsIAnnotationService", []],
["browser/favicon-service;1","nsIFaviconService", []],
["browser/tagging-service;1","nsITaggingService", []],
];
_(testServices.join("\n"));
do_print(testServices.join("\n"));
function run_test()
{
testServices.forEach(function([cid, iface, nothrow]) {
_("Running test with", cid, iface, nothrow);
for (let [cid, iface, nothrow] of testServices) {
do_print(`Running test with ${cid} ${iface} ${nothrow}`);
let s = Cc["@mozilla.org/" + cid].getService(Ci[iface]);
let okName = function(name) {
_("Checking if function is okay to test:", name);
do_print(`Checking if function is okay to test: ${name}`);
let func = s[name];
let mesg = "";
@ -52,60 +41,58 @@ function run_test()
else if (name == "QueryInterface")
mesg = "Ignore QI!";
if (mesg == "")
return true;
if (mesg) {
do_print(`${mesg} Skipping: ${name}`);
return false;
}
_(mesg, "Skipping:", name);
return false;
return true;
}
_("Generating an array of functions to test service:", s);
[i for (i in s) if (okName(i))].sort().forEach(function(n) {
_();
_("Testing " + iface + " function with null args:", n);
do_print(`Generating an array of functions to test service: ${s}`);
for (let n of [i for (i in s) if (okName(i))].sort()) {
do_print(`\nTesting ${iface} function with null args: ${n}`);
let func = s[n];
let num = func.length;
_("Generating array of nulls for #args:", num);
let args = [];
for (let i = num; --i >= 0; )
args.push(null);
do_print(`Generating array of nulls for #args: ${num}`);
let args = Array(num).fill(null);
let tryAgain = true;
while (tryAgain == true) {
try {
_("Calling with args:", JSON.stringify(args));
do_print(`Calling with args: ${JSON.stringify(args)}`);
func.apply(s, args);
_("The function didn't throw! Is it one of the nothrow?", nothrow);
do_check_neq(nothrow.indexOf(n), -1);
do_print(`The function did not throw! Is it one of the nothrow? ${nothrow}`);
Assert.notEqual(nothrow.indexOf(n), -1);
_("Must have been an expected nothrow, so no need to try again");
do_print("Must have been an expected nothrow, so no need to try again");
tryAgain = false;
}
catch(ex if ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
_("Caught an expected exception:", ex.name);
do_print(`Caught an expected exception: ${ex.name}`);
_("Moving on to the next test..");
do_print("Moving on to the next test..");
tryAgain = false;
}
catch(ex if ex.result == Cr.NS_ERROR_XPC_NEED_OUT_OBJECT) {
let pos = Number(ex.message.match(/object arg (\d+)/)[1]);
_("Function call expects an out object at", pos);
do_print(`Function call expects an out object at ${pos}`);
args[pos] = {};
}
catch(ex if ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
_("Method not implemented exception:", ex.name);
do_print(`Method not implemented exception: ${ex.name}`);
_("Moving on to the next test..");
do_print("Moving on to the next test..");
tryAgain = false;
}
catch(ex) {
_("Caught some unexpected exception.. dumping");
_([[i, ex[i]] for (i in ex)].join("\n"));
do_check_true(false);
do_print("Caught some unexpected exception.. dumping");
do_print([[i, ex[i]] for (i in ex)].join("\n"));
throw ex;
}
}
});
});
}
}
}

View File

@ -143,7 +143,8 @@ nsAppStartup::nsAppStartup() :
mInterrupted(false),
mIsSafeModeNecessary(false),
mStartupCrashTrackingEnded(false),
mRestartTouchEnvironment(false)
mRestartTouchEnvironment(false),
mRestartNotSameProfile(false)
{ }
@ -287,6 +288,8 @@ nsAppStartup::Run(void)
retval = NS_SUCCESS_RESTART_METRO_APP;
} else if (mRestart) {
retval = NS_SUCCESS_RESTART_APP;
} else if (mRestartNotSameProfile) {
retval = NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE;
}
return retval;
@ -386,7 +389,12 @@ nsAppStartup::Quit(uint32_t aMode)
gRestartMode = (aMode & 0xF0);
}
if (mRestart || mRestartTouchEnvironment) {
if (!mRestartNotSameProfile) {
mRestartNotSameProfile = (aMode & eRestartNotSameProfile) != 0;
gRestartMode = (aMode & 0xF0);
}
if (mRestart || mRestartTouchEnvironment || mRestartNotSameProfile) {
// Mark the next startup as a restart.
PR_SetEnv("MOZ_APP_RESTART=1");
@ -456,7 +464,7 @@ nsAppStartup::Quit(uint32_t aMode)
NS_NAMED_LITERAL_STRING(shutdownStr, "shutdown");
NS_NAMED_LITERAL_STRING(restartStr, "restart");
obsService->NotifyObservers(nullptr, "quit-application",
(mRestart || mRestartTouchEnvironment) ?
(mRestart || mRestartTouchEnvironment || mRestartNotSameProfile) ?
restartStr.get() : shutdownStr.get());
}

View File

@ -62,7 +62,8 @@ private:
bool mInterrupted; // Was startup interrupted by an interactive prompt?
bool mIsSafeModeNecessary; // Whether safe mode is necessary
bool mStartupCrashTrackingEnded; // Whether startup crash tracking has already ended
bool mRestartTouchEnvironment; // Quit (eRestartTouchEnvironment)
bool mRestartTouchEnvironment; // Quit (eRestartTouchEnvironment)
bool mRestartNotSameProfile; // Quit(eRestartNotSameProfile)
#if defined(XP_WIN)
//Interaction with OS-provided profiling probes

View File

@ -36,6 +36,11 @@ interface nsIAppStartup : nsISupports
* This return code indicates that the application should be
* restarted in metro because quit was called with the
* eRestartTouchEnviroment flag.
* @returnCode NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE
* This return code indicates that the application should be
* restarted without necessarily using the same profile because
* quit was called with the eRestartNotSameProfile flag.
*/
void run();
@ -134,6 +139,13 @@ interface nsIAppStartup : nsISupports
*/
const uint32_t eRestartTouchEnvironment = 0x80;
/**
* Restart the application after quitting. The application will be
* restarted with an empty command line and the normal profile selection
* process will take place on startup.
*/
const uint32_t eRestartNotSameProfile = 0x100;
/**
* Exit the event loop, and shut down the app.
*

View File

@ -102,6 +102,7 @@ function acceptDialog()
gDialogParams.objects.insertElementAt(profileLock.nsIProfileLock, 0, false);
gProfileService.selectedProfile = selectedProfile.profile;
gProfileService.defaultProfile = selectedProfile.profile;
updateStartupPrefs();
gDialogParams.SetInt(0, 1);

View File

@ -29,6 +29,12 @@ interface nsIToolkitProfileService : nsISupports
* browser if no other profile is specified at runtime). This is the profile
* marked with Default=1 in profiles.ini and is usually the same as
* selectedProfile, except on Developer Edition.
*
* Developer Edition uses a profile named "dev-edition-default" as the
* default profile (which it creates if it doesn't exist), unless a special
* empty file named "ignore-dev-edition-profile" is present next to
* profiles.ini. In that case Developer Edition behaves the same as any
* other build of Firefox.
*/
attribute nsIToolkitProfile defaultProfile;

View File

@ -424,6 +424,22 @@ nsToolkitProfileService::Init()
nsToolkitProfile* currentProfile = nullptr;
#ifdef MOZ_DEV_EDITION
nsCOMPtr<nsIFile> ignoreSeparateProfile;
rv = mAppData->Clone(getter_AddRefs(ignoreSeparateProfile));
if (NS_FAILED(rv))
return rv;
rv = ignoreSeparateProfile->AppendNative(NS_LITERAL_CSTRING("ignore-dev-edition-profile"));
if (NS_FAILED(rv))
return rv;
bool shouldIgnoreSeparateProfile;
rv = ignoreSeparateProfile->Exists(&shouldIgnoreSeparateProfile);
if (NS_FAILED(rv))
return rv;
#endif
unsigned int c = 0;
bool foundAuroraDefault = false;
for (c = 0; true; ++c) {
@ -485,8 +501,9 @@ nsToolkitProfileService::Init()
this->SetDefaultProfile(currentProfile);
}
#ifdef MOZ_DEV_EDITION
// Use the dev-edition-default profile if this is an Aurora build.
if (name.EqualsLiteral("dev-edition-default")) {
// Use the dev-edition-default profile if this is an Aurora build and
// ignore-dev-edition-profile is not present.
if (name.EqualsLiteral("dev-edition-default") && !shouldIgnoreSeparateProfile) {
mChosen = currentProfile;
foundAuroraDefault = true;
}
@ -498,7 +515,7 @@ nsToolkitProfileService::Init()
// on webapprt.
bool isFirefox = strcmp(gAppData->ID,
"{ec8030f7-c20a-464f-9b0e-13a3a9e97384}") == 0;
if (!foundAuroraDefault && isFirefox) {
if (!foundAuroraDefault && isFirefox && !shouldIgnoreSeparateProfile) {
// If a single profile exists, it may not be already marked as default.
// Do it now to avoid problems when we create the dev-edition-default profile.
if (!mChosen && mFirst && !mFirst->mNext)

View File

@ -4176,7 +4176,9 @@ XREMain::XRE_main(int argc, char* argv[], const nsXREAppData* aAppData)
// Check for an application initiated restart. This is one that
// corresponds to nsIAppStartup.quit(eRestart)
if (rv == NS_SUCCESS_RESTART_APP || rv == NS_SUCCESS_RESTART_METRO_APP) {
if (rv == NS_SUCCESS_RESTART_APP
|| rv == NS_SUCCESS_RESTART_METRO_APP
|| rv == NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE) {
appInitiatedRestart = true;
// We have an application restart don't do any shutdown checks here
@ -4209,10 +4211,12 @@ XREMain::XRE_main(int argc, char* argv[], const nsXREAppData* aAppData)
if (appInitiatedRestart) {
RestoreStateForAppInitiatedRestart();
// Ensure that these environment variables are set:
SaveFileToEnvIfUnset("XRE_PROFILE_PATH", mProfD);
SaveFileToEnvIfUnset("XRE_PROFILE_LOCAL_PATH", mProfLD);
SaveWordToEnvIfUnset("XRE_PROFILE_NAME", mProfileName);
if (rv != NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE) {
// Ensure that these environment variables are set:
SaveFileToEnvIfUnset("XRE_PROFILE_PATH", mProfD);
SaveFileToEnvIfUnset("XRE_PROFILE_LOCAL_PATH", mProfLD);
SaveWordToEnvIfUnset("XRE_PROFILE_NAME", mProfileName);
}
#ifdef MOZ_WIDGET_GTK
MOZ_gdk_display_close(mGdkDisplay);

View File

@ -906,6 +906,7 @@
* case in which nsIAppStartup::Quit was called with the eRestart flag. */
ERROR(NS_SUCCESS_RESTART_APP, SUCCESS(1)),
ERROR(NS_SUCCESS_RESTART_METRO_APP, SUCCESS(2)),
ERROR(NS_SUCCESS_RESTART_APP_NOT_SAME_PROFILE, SUCCESS(3)),
ERROR(NS_SUCCESS_UNORM_NOTFOUND, SUCCESS(17)),