Merge fx-team to m-c. a=merge
|
@ -1764,8 +1764,10 @@ pref("media.gmp-gmpopenh264.provider.enabled", true);
|
|||
|
||||
pref("browser.apps.URL", "https://marketplace.firefox.com/discovery/");
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
pref("browser.polaris.enabled", false);
|
||||
pref("privacy.trackingprotection.ui.enabled", false);
|
||||
#endif
|
||||
|
||||
// Temporary pref to allow printing in e10s windows on some platforms.
|
||||
#ifdef UNIX_BUT_NOT_MAC
|
||||
|
|
|
@ -250,6 +250,8 @@ let LoopCallsInternal = {
|
|||
respData.calls.forEach((callData) => {
|
||||
if (!this.callsData.inUse) {
|
||||
callData.sessionType = sessionType;
|
||||
// XXX Bug 1090209 will transiton into a better window id.
|
||||
callData.windowId = callData.callId;
|
||||
this._startCall(callData, "incoming");
|
||||
} else {
|
||||
this._returnBusy(callData);
|
||||
|
@ -277,7 +279,7 @@ let LoopCallsInternal = {
|
|||
null,
|
||||
// No title, let the page set that, to avoid flickering.
|
||||
"",
|
||||
"about:loopconversation#" + conversationType + "/" + callData.callId);
|
||||
"about:loopconversation#" + conversationType + "/" + callData.windowId);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -295,7 +297,7 @@ let LoopCallsInternal = {
|
|||
contact: contact,
|
||||
callType: callType,
|
||||
// XXX Really we shouldn't be using random numbers, bug 1090209 will fix this.
|
||||
callId: Math.floor((Math.random() * 100000000))
|
||||
windowId: Math.floor((Math.random() * 100000000))
|
||||
};
|
||||
|
||||
this._startCall(callData, "outgoing");
|
||||
|
@ -339,17 +341,17 @@ this.LoopCalls = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Returns the callData for a specific loopCallId
|
||||
* Returns the callData for a specific conversation window id.
|
||||
*
|
||||
* The data was retrieved from the LoopServer via a GET/calls/<version> request
|
||||
* triggered by an incoming message from the LoopPushServer.
|
||||
*
|
||||
* @param {int} loopCallId
|
||||
* @param {Number} conversationWindowId
|
||||
* @return {callData} The callData or undefined if error.
|
||||
*/
|
||||
getCallData: function(loopCallId) {
|
||||
getCallData: function(conversationWindowId) {
|
||||
if (LoopCallsInternal.callsData.data &&
|
||||
LoopCallsInternal.callsData.data.callId == loopCallId) {
|
||||
LoopCallsInternal.callsData.data.windowId == conversationWindowId) {
|
||||
return LoopCallsInternal.callsData.data;
|
||||
} else {
|
||||
return undefined;
|
||||
|
@ -357,15 +359,15 @@ this.LoopCalls = {
|
|||
},
|
||||
|
||||
/**
|
||||
* Releases the callData for a specific loopCallId
|
||||
* Releases the callData for a specific conversation window id.
|
||||
*
|
||||
* The result of this call will be a free call session slot.
|
||||
*
|
||||
* @param {int} loopCallId
|
||||
* @param {Number} conversationWindowId
|
||||
*/
|
||||
releaseCallData: function(loopCallId) {
|
||||
releaseCallData: function(conversationWindowId) {
|
||||
if (LoopCallsInternal.callsData.data &&
|
||||
LoopCallsInternal.callsData.data.callId == loopCallId) {
|
||||
LoopCallsInternal.callsData.data.windowId == conversationWindowId) {
|
||||
LoopCallsInternal.callsData.data = undefined;
|
||||
LoopCallsInternal.callsData.inUse = false;
|
||||
}
|
||||
|
|
|
@ -5,339 +5,222 @@
|
|||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService",
|
||||
"resource:///modules/loop/MozLoopService.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "LOOP_SESSION_TYPE",
|
||||
"resource:///modules/loop/MozLoopService.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopPushHandler",
|
||||
"resource:///modules/loop/MozLoopPushHandler.jsm");
|
||||
const {MozLoopService, LOOP_SESSION_TYPE} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {});
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
XPCOMUtils.defineLazyGetter(this, "eventEmitter", function() {
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {});
|
||||
return new EventEmitter();
|
||||
});
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["LoopRooms", "roomsPushNotification"];
|
||||
|
||||
let gRoomsListFetched = false;
|
||||
let gRooms = new Map();
|
||||
let gCallbacks = new Map();
|
||||
|
||||
/**
|
||||
* Callback used to indicate changes to rooms data on the LoopServer.
|
||||
*
|
||||
* @param {Object} version Version number assigned to this change set.
|
||||
* @param {Object} channelID Notification channel identifier.
|
||||
*
|
||||
*/
|
||||
const roomsPushNotification = function(version, channelID) {
|
||||
return LoopRoomsInternal.onNotification(version, channelID);
|
||||
};
|
||||
|
||||
let LoopRoomsInternal = {
|
||||
getAll: function(callback) {
|
||||
Task.spawn(function*() {
|
||||
yield MozLoopService.register();
|
||||
|
||||
if (gRoomsListFetched) {
|
||||
callback(null, [...gRooms.values()]);
|
||||
return;
|
||||
}
|
||||
// Fetch the rooms from the server.
|
||||
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
|
||||
LOOP_SESSION_TYPE.GUEST;
|
||||
let rooms = yield this.requestRoomList(sessionType);
|
||||
// Add each room to our in-memory Map using a locally unique
|
||||
// identifier.
|
||||
for (let room of rooms) {
|
||||
let id = MozLoopService.generateLocalID();
|
||||
room.localRoomId = id;
|
||||
// Next, request the detailed information for each room.
|
||||
// If the request fails the room data will not be added to the map.
|
||||
try {
|
||||
let details = yield this.requestRoomDetails(room.roomToken, sessionType);
|
||||
for (let attr in details) {
|
||||
room[attr] = details[attr]
|
||||
}
|
||||
delete room.currSize; //This attribute will be eliminated in the next revision.
|
||||
gRooms.set(id, room);
|
||||
}
|
||||
catch (error) {MozLoopService.log.warn(
|
||||
"failed GETing room details for roomToken = " + room.roomToken + ": ", error)}
|
||||
}
|
||||
callback(null, [...gRooms.values()]);
|
||||
return;
|
||||
}.bind(this)).catch((error) => {MozLoopService.log.error("getAll error:", error);
|
||||
callback(error)});
|
||||
return;
|
||||
},
|
||||
|
||||
getRoomData: function(localRoomId, callback) {
|
||||
if (gRooms.has(localRoomId)) {
|
||||
callback(null, gRooms.get(localRoomId));
|
||||
} else {
|
||||
callback(new Error("Room data not found or not fetched yet for room with ID " + localRoomId));
|
||||
}
|
||||
return;
|
||||
},
|
||||
// Since the LoopRoomsInternal.rooms map as defined below is a local cache of
|
||||
// room objects that are retrieved from the server, this is list may become out
|
||||
// of date. The Push server may notify us of this event, which will set the global
|
||||
// 'dirty' flag to TRUE.
|
||||
let gDirty = true;
|
||||
|
||||
/**
|
||||
* Request list of all rooms associated with this account.
|
||||
* Extend a `target` object with the properties defined in `source`.
|
||||
*
|
||||
* @param {String} sessionType Indicates which hawkRequest endpoint to use.
|
||||
*
|
||||
* @returns {Promise} room list
|
||||
* @param {Object} target The target object to receive properties defined in `source`
|
||||
* @param {Object} source The source object to copy properties from
|
||||
*/
|
||||
requestRoomList: function(sessionType) {
|
||||
return MozLoopService.hawkRequest(sessionType, "/rooms", "GET")
|
||||
.then(response => {
|
||||
let roomsList = JSON.parse(response.body);
|
||||
if (!Array.isArray(roomsList)) {
|
||||
// Force a reject in the returned promise.
|
||||
// To be caught by the caller using the returned Promise.
|
||||
throw new Error("Missing array of rooms in response.");
|
||||
const extend = function(target, source) {
|
||||
for (let key of Object.getOwnPropertyNames(source)) {
|
||||
target[key] = source[key];
|
||||
}
|
||||
return roomsList;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Request information about a specific room from the server.
|
||||
*
|
||||
* @param {Object} token Room identifier returned from the LoopServer.
|
||||
* @param {String} sessionType Indicates which hawkRequest endpoint to use.
|
||||
*
|
||||
* @returns {Promise} room details
|
||||
*/
|
||||
requestRoomDetails: function(token, sessionType) {
|
||||
return MozLoopService.hawkRequest(sessionType, "/rooms/" + token, "GET")
|
||||
.then(response => JSON.parse(response.body));
|
||||
},
|
||||
|
||||
/**
|
||||
* Callback used to indicate changes to rooms data on the LoopServer.
|
||||
*
|
||||
* @param {Object} version Version number assigned to this change set.
|
||||
* @param {Object} channelID Notification channel identifier.
|
||||
*
|
||||
*/
|
||||
onNotification: function(version, channelID) {
|
||||
return;
|
||||
},
|
||||
|
||||
createRoom: function(props, callback) {
|
||||
// Always create a basic room record and launch the window, attaching
|
||||
// the localRoomId. Later errors will be returned via the registered callback.
|
||||
let localRoomId = MozLoopService.generateLocalID((id) => {gRooms.has(id)})
|
||||
let room = {localRoomId : localRoomId};
|
||||
for (let prop in props) {
|
||||
room[prop] = props[prop]
|
||||
}
|
||||
|
||||
gRooms.set(localRoomId, room);
|
||||
this.addCallback(localRoomId, "RoomCreated", callback);
|
||||
MozLoopService.openChatWindow(null, "", "about:loopconversation#room/" + localRoomId);
|
||||
|
||||
if (!"roomName" in props ||
|
||||
!"expiresIn" in props ||
|
||||
!"roomOwner" in props ||
|
||||
!"maxSize" in props) {
|
||||
this.postCallback(localRoomId, "RoomCreated",
|
||||
new Error("missing required room create property"));
|
||||
return localRoomId;
|
||||
}
|
||||
|
||||
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
|
||||
LOOP_SESSION_TYPE.GUEST;
|
||||
|
||||
MozLoopService.hawkRequest(sessionType, "/rooms", "POST", props).then(
|
||||
(response) => {
|
||||
let data = JSON.parse(response.body);
|
||||
for (let attr in data) {
|
||||
room[attr] = data[attr]
|
||||
}
|
||||
delete room.expiresIn; //Do not keep this value - it is a request to the server
|
||||
this.postCallback(localRoomId, "RoomCreated", null, room);
|
||||
},
|
||||
(error) => {
|
||||
this.postCallback(localRoomId, "RoomCreated", error);
|
||||
});
|
||||
|
||||
return localRoomId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Send an update to the callbacks registered for a specific localRoomId
|
||||
* for a callback type.
|
||||
*
|
||||
* The result set is always saved. Then each
|
||||
* callback function that has been registered when this function is
|
||||
* called will be called with the result set. Any new callback that
|
||||
* is regsitered via addCallback will receive a copy of the last
|
||||
* saved result set when registered. This allows the posting operation
|
||||
* to complete before the callback is registered in an asynchronous
|
||||
* operation.
|
||||
*
|
||||
* Callbacsk must be of the form:
|
||||
* function (error, success) {...}
|
||||
*
|
||||
* @param {String} localRoomId Local room identifier.
|
||||
* @param {String} callbackName callback type
|
||||
* @param {?Error} error result or null.
|
||||
* @param {?Object} success result if error argument is null.
|
||||
*/
|
||||
postCallback: function(localRoomId, callbackName, error, success) {
|
||||
let roomCallbacks = gCallbacks.get(localRoomId);
|
||||
if (!roomCallbacks) {
|
||||
// No callbacks have been registered or results posted for this room.
|
||||
// Initialize a record for this room and callbackName, saving the
|
||||
// result set.
|
||||
gCallbacks.set(localRoomId, new Map([[
|
||||
callbackName,
|
||||
{ callbackList: [], result: { error: error, success: success } }]]));
|
||||
return;
|
||||
}
|
||||
|
||||
let namedCallback = roomCallbacks.get(callbackName);
|
||||
// A callback of this name has not been registered.
|
||||
if (!namedCallback) {
|
||||
roomCallbacks.set(
|
||||
callbackName,
|
||||
{callbackList: [], result: {error: error, success: success}});
|
||||
return;
|
||||
}
|
||||
|
||||
// Record the latest result set.
|
||||
namedCallback.result = {error: error, success: success};
|
||||
|
||||
// Call each registerd callback passing the new result posted.
|
||||
namedCallback.callbackList.forEach((callback) => {
|
||||
callback(error, success);
|
||||
});
|
||||
},
|
||||
|
||||
addCallback: function(localRoomId, callbackName, callback) {
|
||||
let roomCallbacks = gCallbacks.get(localRoomId);
|
||||
if (!roomCallbacks) {
|
||||
// No callbacks have been registered or results posted for this room.
|
||||
// Initialize a record for this room and callbackName.
|
||||
gCallbacks.set(localRoomId, new Map([[
|
||||
callbackName,
|
||||
{callbackList: [callback]}]]));
|
||||
return;
|
||||
}
|
||||
|
||||
let namedCallback = roomCallbacks.get(callbackName);
|
||||
// A callback of this name has not been registered.
|
||||
if (!namedCallback) {
|
||||
roomCallbacks.set(
|
||||
callbackName,
|
||||
{callbackList: [callback]});
|
||||
return;
|
||||
}
|
||||
|
||||
// Add this callback if not already in the array
|
||||
if (namedCallback.callbackList.indexOf(callback) >= 0) {
|
||||
return;
|
||||
}
|
||||
namedCallback.callbackList.push(callback);
|
||||
|
||||
// If a result has been posted for this callback
|
||||
// send it using this new callback function.
|
||||
let result = namedCallback.result;
|
||||
if (result) {
|
||||
callback(result.error, result.success);
|
||||
}
|
||||
},
|
||||
|
||||
deleteCallback: function(localRoomId, callbackName, callback) {
|
||||
let roomCallbacks = gCallbacks.get(localRoomId);
|
||||
if (!roomCallbacks) {
|
||||
return;
|
||||
}
|
||||
|
||||
let namedCallback = roomCallbacks.get(callbackName);
|
||||
if (!namedCallback) {
|
||||
return;
|
||||
}
|
||||
|
||||
let i = namedCallback.callbackList.indexOf(callback);
|
||||
if (i >= 0) {
|
||||
namedCallback.callbackList.splice(i, 1);
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
return target;
|
||||
};
|
||||
Object.freeze(LoopRoomsInternal);
|
||||
|
||||
/**
|
||||
* The LoopRooms class.
|
||||
* The Rooms class.
|
||||
*
|
||||
* Each method that is a member of this class requires the last argument to be a
|
||||
* callback Function. MozLoopAPI will cause things to break if this invariant is
|
||||
* violated. You'll notice this as well in the documentation for each method.
|
||||
*/
|
||||
this.LoopRooms = {
|
||||
let LoopRoomsInternal = {
|
||||
rooms: new Map(),
|
||||
|
||||
/**
|
||||
* Fetch a list of rooms that the currently registered user is a member of.
|
||||
*
|
||||
* @param {String} [version] If set, we will fetch a list of changed rooms since
|
||||
* `version`. Optional.
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of rooms, if it was fetched successfully.
|
||||
*/
|
||||
getAll: function(callback) {
|
||||
return LoopRoomsInternal.getAll(callback);
|
||||
getAll: function(version = null, callback) {
|
||||
if (!callback) {
|
||||
callback = version;
|
||||
version = null;
|
||||
}
|
||||
|
||||
Task.spawn(function* () {
|
||||
yield MozLoopService.register();
|
||||
|
||||
if (!gDirty) {
|
||||
callback(null, [...this.rooms.values()]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the rooms from the server.
|
||||
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
|
||||
LOOP_SESSION_TYPE.GUEST;
|
||||
let url = "/rooms" + (version ? "?version=" + encodeURIComponent(version) : "");
|
||||
let response = yield MozLoopService.hawkRequest(sessionType, url, "GET");
|
||||
let roomsList = JSON.parse(response.body);
|
||||
if (!Array.isArray(roomsList)) {
|
||||
throw new Error("Missing array of rooms in response.");
|
||||
}
|
||||
|
||||
// Next, request the detailed information for each room. If the request
|
||||
// fails the room data will not be added to the map.
|
||||
for (let room of roomsList) {
|
||||
this.rooms.set(room.roomToken, room);
|
||||
yield LoopRooms.promise("get", room.roomToken);
|
||||
}
|
||||
|
||||
// Set the 'dirty' flag back to FALSE, since the list is as fresh as can be now.
|
||||
gDirty = false;
|
||||
callback(null, [...this.rooms.values()]);
|
||||
}.bind(this)).catch(error => {
|
||||
callback(error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the current stored version of the data for the indicated room.
|
||||
* Request information about a specific room from the server. It will be
|
||||
* returned from the cache if it's already in it.
|
||||
*
|
||||
* @param {String} localRoomId Local room identifier
|
||||
* @param {String} roomToken Room identifier
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the list of rooms, if it was fetched successfully.
|
||||
*/
|
||||
getRoomData: function(localRoomId, callback) {
|
||||
return LoopRoomsInternal.getRoomData(localRoomId, callback);
|
||||
get: function(roomToken, callback) {
|
||||
let room = this.rooms.has(roomToken) ? this.rooms.get(roomToken) : {};
|
||||
// Check if we need to make a request to the server to collect more room data.
|
||||
if (!room || gDirty || !("participants" in room)) {
|
||||
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
|
||||
LOOP_SESSION_TYPE.GUEST;
|
||||
MozLoopService.hawkRequest(sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
|
||||
.then(response => {
|
||||
let eventName = ("roomToken" in room) ? "add" : "update";
|
||||
extend(room, JSON.parse(response.body));
|
||||
// Remove the `currSize` for posterity.
|
||||
if ("currSize" in room) {
|
||||
delete room.currSize;
|
||||
}
|
||||
this.rooms.set(roomToken, room);
|
||||
|
||||
eventEmitter.emit(eventName, room);
|
||||
callback(null, room);
|
||||
}, err => callback(err)).catch(err => callback(err));
|
||||
} else {
|
||||
callback(null, room);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a room. Will both open a chat window for the new room
|
||||
* and perform an exchange with the LoopServer to create the room.
|
||||
* for a callback type. Callback must be of the form:
|
||||
* function (error, success) {...}
|
||||
* Create a room.
|
||||
*
|
||||
* @param {Object} room properties to be sent to the LoopServer
|
||||
* @param {Function} callback Must be of the form: function (error, success) {...}
|
||||
*
|
||||
* @returns {String} localRoomId assigned to this new room.
|
||||
* @param {Object} room Properties to be sent to the LoopServer
|
||||
* @param {Function} callback Function that will be invoked once the operation
|
||||
* finished. The first argument passed will be an
|
||||
* `Error` object or `null`. The second argument will
|
||||
* be the room, if it was created successfully.
|
||||
*/
|
||||
createRoom: function(roomProps, callback) {
|
||||
return LoopRoomsInternal.createRoom(roomProps, callback);
|
||||
create: function(room, callback) {
|
||||
if (!("roomName" in room) || !("expiresIn" in room) ||
|
||||
!("roomOwner" in room) || !("maxSize" in room)) {
|
||||
callback(new Error("Missing required property to create a room"));
|
||||
return;
|
||||
}
|
||||
|
||||
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
|
||||
LOOP_SESSION_TYPE.GUEST;
|
||||
|
||||
MozLoopService.hawkRequest(sessionType, "/rooms", "POST", room)
|
||||
.then(response => {
|
||||
let data = JSON.parse(response.body);
|
||||
extend(room, data);
|
||||
// Do not keep this value - it is a request to the server.
|
||||
delete room.expiresIn;
|
||||
this.rooms.set(room.roomToken, room);
|
||||
|
||||
eventEmitter.emit("add", room);
|
||||
callback(null, room);
|
||||
}, error => callback(error)).catch(error => callback(error));
|
||||
},
|
||||
|
||||
/**
|
||||
* Register a callback of a specified type with a localRoomId.
|
||||
* Callback used to indicate changes to rooms data on the LoopServer.
|
||||
*
|
||||
* @param {String} localRoomId Local room identifier.
|
||||
* @param {String} callbackName callback type
|
||||
* @param {Function} callback Must be of the form: function (error, success) {...}
|
||||
* @param {String} version Version number assigned to this change set.
|
||||
* @param {String} channelID Notification channel identifier.
|
||||
*/
|
||||
addCallback: function(localRoomId, callbackName, callback) {
|
||||
return LoopRoomsInternal.addCallback(localRoomId, callbackName, callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* Un-register and delete a callback of a specified type for a localRoomId.
|
||||
*
|
||||
* @param {String} localRoomId Local room identifier.
|
||||
* @param {String} callbackName callback type
|
||||
* @param {Function} callback Previously passed to addCallback().
|
||||
*/
|
||||
deleteCallback: function(localRoomId, callbackName, callback) {
|
||||
return LoopRoomsInternal.deleteCallback(localRoomId, callbackName, callback);
|
||||
onNotification: function(version, channelID) {
|
||||
gDirty = true;
|
||||
this.getAll(version, () => {});
|
||||
},
|
||||
};
|
||||
Object.freeze(LoopRooms);
|
||||
Object.freeze(LoopRoomsInternal);
|
||||
|
||||
/**
|
||||
* Public Loop Rooms API.
|
||||
*
|
||||
* LoopRooms implements the EventEmitter interface by exposing three methods -
|
||||
* `on`, `once` and `off` - to subscribe to events.
|
||||
* At this point the following events may be subscribed to:
|
||||
* - 'add': A new room object was successfully added to the data store.
|
||||
* - 'remove': A room was successfully removed from the data store.
|
||||
* - 'update': A room object was successfully updated with changed
|
||||
* properties in the data store.
|
||||
*
|
||||
* See the internal code for the API documentation.
|
||||
*/
|
||||
this.LoopRooms = {
|
||||
getAll: function(version, callback) {
|
||||
return LoopRoomsInternal.getAll(version, callback);
|
||||
},
|
||||
|
||||
get: function(roomToken, callback) {
|
||||
return LoopRoomsInternal.get(roomToken, callback);
|
||||
},
|
||||
|
||||
create: function(options, callback) {
|
||||
return LoopRoomsInternal.create(options, callback);
|
||||
},
|
||||
|
||||
promise: function(method, ...params) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this[method](...params, (error, result) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
on: (...params) => eventEmitter.on(...params),
|
||||
|
||||
once: (...params) => eventEmitter.once(...params),
|
||||
|
||||
off: (...params) => eventEmitter.off(...params)
|
||||
};
|
||||
Object.freeze(this.LoopRooms);
|
||||
|
|
|
@ -75,6 +75,14 @@ const cloneValueInto = function(value, targetWindow) {
|
|||
return value;
|
||||
}
|
||||
|
||||
// Strip Function properties, since they can not be cloned across boundaries
|
||||
// like this.
|
||||
for (let prop of Object.getOwnPropertyNames(value)) {
|
||||
if (typeof value[prop] == "function") {
|
||||
delete value[prop];
|
||||
}
|
||||
}
|
||||
|
||||
// Inspect for an error this way, because the Error object is special.
|
||||
if (value.constructor.name == "Error") {
|
||||
return cloneErrorObject(value, targetWindow);
|
||||
|
@ -176,8 +184,10 @@ function injectLoopAPI(targetWindow) {
|
|||
}
|
||||
|
||||
// We have to clone the error property since it may be an Error object.
|
||||
if (error.hasOwnProperty("toString")) {
|
||||
delete error.toString;
|
||||
}
|
||||
errors[type] = Cu.cloneInto(error, targetWindow);
|
||||
|
||||
}
|
||||
return Cu.cloneInto(errors, targetWindow);
|
||||
},
|
||||
|
@ -196,34 +206,34 @@ function injectLoopAPI(targetWindow) {
|
|||
},
|
||||
|
||||
/**
|
||||
* Returns the callData for a specific callDataId
|
||||
* Returns the callData for a specific conversation window id.
|
||||
*
|
||||
* The data was retrieved from the LoopServer via a GET/calls/<version> request
|
||||
* triggered by an incoming message from the LoopPushServer.
|
||||
*
|
||||
* @param {int} loopCallId
|
||||
* @param {Number} conversationWindowId
|
||||
* @returns {callData} The callData or undefined if error.
|
||||
*/
|
||||
getCallData: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(loopCallId) {
|
||||
return Cu.cloneInto(LoopCalls.getCallData(loopCallId), targetWindow);
|
||||
value: function(conversationWindowId) {
|
||||
return Cu.cloneInto(LoopCalls.getCallData(conversationWindowId), targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Releases the callData for a specific loopCallId
|
||||
* Releases the callData for a specific conversation window id.
|
||||
*
|
||||
* The result of this call will be a free call session slot.
|
||||
*
|
||||
* @param {int} loopCallId
|
||||
* @param {Number} conversationWindowId
|
||||
*/
|
||||
releaseCallData: {
|
||||
enumerable: true,
|
||||
writable: true,
|
||||
value: function(loopCallId) {
|
||||
LoopCalls.releaseCallData(loopCallId);
|
||||
value: function(conversationWindowId) {
|
||||
LoopCalls.releaseCallData(conversationWindowId);
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -352,7 +352,7 @@ loop.conversation = (function(mozL10n) {
|
|||
setupIncomingCall: function() {
|
||||
navigator.mozLoop.startAlerting();
|
||||
|
||||
var callData = navigator.mozLoop.getCallData(this.props.conversation.get("callId"));
|
||||
var callData = navigator.mozLoop.getCallData(this.props.conversation.get("windowId"));
|
||||
if (!callData) {
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
// this by better "call failed" UI.
|
||||
|
@ -374,7 +374,7 @@ loop.conversation = (function(mozL10n) {
|
|||
* Moves the call to the end state
|
||||
*/
|
||||
endCall: function() {
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("callId"));
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
|
@ -475,7 +475,7 @@ loop.conversation = (function(mozL10n) {
|
|||
*/
|
||||
_declineCall: function() {
|
||||
this._websocket.decline();
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("callId"));
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
|
||||
this._websocket.close();
|
||||
// Having a timeout here lets the logging for the websocket complete and be
|
||||
// displayed on the console if both are on.
|
||||
|
@ -618,10 +618,10 @@ loop.conversation = (function(mozL10n) {
|
|||
{sdk: window.OT} // Model dependencies
|
||||
);
|
||||
|
||||
// Obtain the callId and pass it through
|
||||
// Obtain the windowId and pass it through
|
||||
var helper = new loop.shared.utils.Helper();
|
||||
var locationHash = helper.locationData().hash;
|
||||
var callId;
|
||||
var windowId;
|
||||
var outgoing;
|
||||
var localRoomStore;
|
||||
|
||||
|
@ -633,7 +633,7 @@ loop.conversation = (function(mozL10n) {
|
|||
|
||||
var hash = locationHash.match(/#incoming\/(.*)/);
|
||||
if (hash) {
|
||||
callId = hash[1];
|
||||
windowId = hash[1];
|
||||
outgoing = false;
|
||||
} else if (hash = locationHash.match(/#room\/(.*)/)) {
|
||||
localRoomStore = new loop.store.LocalRoomStore({
|
||||
|
@ -643,16 +643,16 @@ loop.conversation = (function(mozL10n) {
|
|||
} else {
|
||||
hash = locationHash.match(/#outgoing\/(.*)/);
|
||||
if (hash) {
|
||||
callId = hash[1];
|
||||
windowId = hash[1];
|
||||
outgoing = true;
|
||||
}
|
||||
}
|
||||
|
||||
conversation.set({callId: callId});
|
||||
conversation.set({windowId: windowId});
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
// Handle direct close of dialog box via [x] control.
|
||||
navigator.mozLoop.releaseCallData(callId);
|
||||
navigator.mozLoop.releaseCallData(windowId);
|
||||
});
|
||||
|
||||
React.renderComponent(AppControllerView({
|
||||
|
@ -671,7 +671,7 @@ loop.conversation = (function(mozL10n) {
|
|||
}
|
||||
|
||||
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
|
||||
callId: callId,
|
||||
windowId: windowId,
|
||||
outgoing: outgoing
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -352,7 +352,7 @@ loop.conversation = (function(mozL10n) {
|
|||
setupIncomingCall: function() {
|
||||
navigator.mozLoop.startAlerting();
|
||||
|
||||
var callData = navigator.mozLoop.getCallData(this.props.conversation.get("callId"));
|
||||
var callData = navigator.mozLoop.getCallData(this.props.conversation.get("windowId"));
|
||||
if (!callData) {
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
// this by better "call failed" UI.
|
||||
|
@ -374,7 +374,7 @@ loop.conversation = (function(mozL10n) {
|
|||
* Moves the call to the end state
|
||||
*/
|
||||
endCall: function() {
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("callId"));
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
|
||||
this.setState({callStatus: "end"});
|
||||
},
|
||||
|
||||
|
@ -475,7 +475,7 @@ loop.conversation = (function(mozL10n) {
|
|||
*/
|
||||
_declineCall: function() {
|
||||
this._websocket.decline();
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("callId"));
|
||||
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
|
||||
this._websocket.close();
|
||||
// Having a timeout here lets the logging for the websocket complete and be
|
||||
// displayed on the console if both are on.
|
||||
|
@ -618,10 +618,10 @@ loop.conversation = (function(mozL10n) {
|
|||
{sdk: window.OT} // Model dependencies
|
||||
);
|
||||
|
||||
// Obtain the callId and pass it through
|
||||
// Obtain the windowId and pass it through
|
||||
var helper = new loop.shared.utils.Helper();
|
||||
var locationHash = helper.locationData().hash;
|
||||
var callId;
|
||||
var windowId;
|
||||
var outgoing;
|
||||
var localRoomStore;
|
||||
|
||||
|
@ -633,7 +633,7 @@ loop.conversation = (function(mozL10n) {
|
|||
|
||||
var hash = locationHash.match(/#incoming\/(.*)/);
|
||||
if (hash) {
|
||||
callId = hash[1];
|
||||
windowId = hash[1];
|
||||
outgoing = false;
|
||||
} else if (hash = locationHash.match(/#room\/(.*)/)) {
|
||||
localRoomStore = new loop.store.LocalRoomStore({
|
||||
|
@ -643,16 +643,16 @@ loop.conversation = (function(mozL10n) {
|
|||
} else {
|
||||
hash = locationHash.match(/#outgoing\/(.*)/);
|
||||
if (hash) {
|
||||
callId = hash[1];
|
||||
windowId = hash[1];
|
||||
outgoing = true;
|
||||
}
|
||||
}
|
||||
|
||||
conversation.set({callId: callId});
|
||||
conversation.set({windowId: windowId});
|
||||
|
||||
window.addEventListener("unload", function(event) {
|
||||
// Handle direct close of dialog box via [x] control.
|
||||
navigator.mozLoop.releaseCallData(callId);
|
||||
navigator.mozLoop.releaseCallData(windowId);
|
||||
});
|
||||
|
||||
React.renderComponent(<AppControllerView
|
||||
|
@ -671,7 +671,7 @@ loop.conversation = (function(mozL10n) {
|
|||
}
|
||||
|
||||
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
|
||||
callId: callId,
|
||||
windowId: windowId,
|
||||
outgoing: outgoing
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ loop.shared.actions = (function() {
|
|||
*/
|
||||
GatherCallData: Action.define("gatherCallData", {
|
||||
// Specify the callId for an incoming call.
|
||||
callId: [String, null],
|
||||
windowId: [String, null],
|
||||
outgoing: Boolean
|
||||
}),
|
||||
|
||||
|
|
|
@ -55,6 +55,8 @@ loop.store.ConversationStore = (function() {
|
|||
|
||||
var ConversationStore = Backbone.Model.extend({
|
||||
defaults: {
|
||||
// The id of the window. Currently used for getting the window id.
|
||||
windowId: undefined,
|
||||
// The current state of the call
|
||||
callState: CALL_STATES.INIT,
|
||||
// The reason if a call was terminated
|
||||
|
@ -200,7 +202,7 @@ loop.store.ConversationStore = (function() {
|
|||
return;
|
||||
}
|
||||
|
||||
var callData = navigator.mozLoop.getCallData(actionData.callId);
|
||||
var callData = navigator.mozLoop.getCallData(actionData.windowId);
|
||||
if (!callData) {
|
||||
console.error("Failed to get the call data");
|
||||
this.set({callState: CALL_STATES.TERMINATED});
|
||||
|
@ -210,7 +212,7 @@ loop.store.ConversationStore = (function() {
|
|||
this.set({
|
||||
contact: callData.contact,
|
||||
outgoing: actionData.outgoing,
|
||||
callId: actionData.callId,
|
||||
windowId: actionData.windowId,
|
||||
callType: callData.callType,
|
||||
callState: CALL_STATES.GATHER
|
||||
});
|
||||
|
@ -407,11 +409,7 @@ loop.store.ConversationStore = (function() {
|
|||
delete this._websocket;
|
||||
}
|
||||
|
||||
// XXX: The internal callId is different from
|
||||
// this.get("callId"), see bug 1084228 for more info.
|
||||
var locationHash = new loop.shared.utils.Helper().locationData().hash;
|
||||
var callId = locationHash.match(/\#outgoing\/(.*)/)[1];
|
||||
navigator.mozLoop.releaseCallData(callId);
|
||||
navigator.mozLoop.releaseCallData(this.get("windowId"));
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -22,6 +22,7 @@ loop.shared.models = (function(l10n) {
|
|||
sessionToken: undefined, // OT session token
|
||||
sessionType: undefined, // Hawk session type
|
||||
apiKey: undefined, // OT api key
|
||||
windowId: undefined, // The window id
|
||||
callId: undefined, // The callId on the server
|
||||
progressURL: undefined, // The websocket url to use for progress
|
||||
websocketToken: undefined, // The token to use for websocket auth, this is
|
||||
|
|
|
@ -158,7 +158,7 @@ describe("loop.conversation", function() {
|
|||
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
|
||||
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
|
||||
new loop.shared.actions.GatherCallData({
|
||||
callId: "42",
|
||||
windowId: "42",
|
||||
outgoing: false
|
||||
}));
|
||||
});
|
||||
|
@ -175,7 +175,7 @@ describe("loop.conversation", function() {
|
|||
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
|
||||
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
|
||||
new loop.shared.actions.GatherCallData({
|
||||
callId: "24",
|
||||
windowId: "24",
|
||||
outgoing: true
|
||||
}));
|
||||
});
|
||||
|
@ -276,7 +276,7 @@ describe("loop.conversation", function() {
|
|||
conversation = new loop.shared.models.ConversationModel({}, {
|
||||
sdk: {}
|
||||
});
|
||||
conversation.set({callId: 42});
|
||||
conversation.set({windowId: 42});
|
||||
sandbox.stub(conversation, "setOutgoingSessionData");
|
||||
});
|
||||
|
||||
|
@ -547,8 +547,10 @@ describe("loop.conversation", function() {
|
|||
decline: sinon.stub(),
|
||||
close: sinon.stub()
|
||||
};
|
||||
conversation.set({
|
||||
windowId: "8699"
|
||||
});
|
||||
conversation.setIncomingSessionData({
|
||||
callId: 8699,
|
||||
websocketToken: 123
|
||||
});
|
||||
});
|
||||
|
@ -571,7 +573,7 @@ describe("loop.conversation", function() {
|
|||
icView.decline();
|
||||
|
||||
sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, 8699);
|
||||
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "8699");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -610,7 +612,7 @@ describe("loop.conversation", function() {
|
|||
|
||||
sinon.assert.calledTwice(conversation.get);
|
||||
sinon.assert.calledWithExactly(conversation.get, "callToken");
|
||||
sinon.assert.calledWithExactly(conversation.get, "callId");
|
||||
sinon.assert.calledWithExactly(conversation.get, "windowId");
|
||||
});
|
||||
|
||||
it("should trigger error handling in case of error", function() {
|
||||
|
|
|
@ -125,11 +125,7 @@ describe("loop.store.ConversationStore", function () {
|
|||
describe("#connectionFailure", function() {
|
||||
beforeEach(function() {
|
||||
store._websocket = fakeWebsocket;
|
||||
sandbox.stub(loop.shared.utils.Helper.prototype, "locationData")
|
||||
.returns({
|
||||
hash: "#outgoing/42",
|
||||
pathname: ""
|
||||
});
|
||||
store.set({windowId: "42"});
|
||||
});
|
||||
|
||||
it("should disconnect the session", function() {
|
||||
|
@ -246,7 +242,7 @@ describe("loop.store.ConversationStore", function () {
|
|||
it("should set the state to 'gather'", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.GatherCallData({
|
||||
callId: "76543218",
|
||||
windowId: "76543218",
|
||||
outgoing: true
|
||||
}));
|
||||
|
||||
|
@ -256,18 +252,18 @@ describe("loop.store.ConversationStore", function () {
|
|||
it("should save the basic call information", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.GatherCallData({
|
||||
callId: "123456",
|
||||
windowId: "123456",
|
||||
outgoing: true
|
||||
}));
|
||||
|
||||
expect(store.get("callId")).eql("123456");
|
||||
expect(store.get("windowId")).eql("123456");
|
||||
expect(store.get("outgoing")).eql(true);
|
||||
});
|
||||
|
||||
it("should save the basic information from the mozLoop api", function() {
|
||||
dispatcher.dispatch(
|
||||
new sharedActions.GatherCallData({
|
||||
callId: "123456",
|
||||
windowId: "123456",
|
||||
outgoing: true
|
||||
}));
|
||||
|
||||
|
@ -280,7 +276,7 @@ describe("loop.store.ConversationStore", function () {
|
|||
|
||||
beforeEach(function() {
|
||||
outgoingCallData = {
|
||||
callId: "123456",
|
||||
windowId: "123456",
|
||||
outgoing: true
|
||||
};
|
||||
});
|
||||
|
@ -499,11 +495,7 @@ describe("loop.store.ConversationStore", function () {
|
|||
close: wsCloseSpy
|
||||
};
|
||||
store.set({callState: CALL_STATES.ONGOING});
|
||||
sandbox.stub(loop.shared.utils.Helper.prototype, "locationData")
|
||||
.returns({
|
||||
hash: "#outgoing/42",
|
||||
pathname: ""
|
||||
});
|
||||
store.set({windowId: "42"});
|
||||
});
|
||||
|
||||
it("should disconnect the session", function() {
|
||||
|
@ -549,11 +541,7 @@ describe("loop.store.ConversationStore", function () {
|
|||
close: wsCloseSpy
|
||||
};
|
||||
store.set({callState: CALL_STATES.ONGOING});
|
||||
sandbox.stub(loop.shared.utils.Helper.prototype, "locationData")
|
||||
.returns({
|
||||
hash: "#outgoing/42",
|
||||
pathname: ""
|
||||
});
|
||||
store.set({windowId: "42"});
|
||||
});
|
||||
|
||||
it("should disconnect the session", function() {
|
||||
|
@ -587,11 +575,7 @@ describe("loop.store.ConversationStore", function () {
|
|||
store._websocket = fakeWebsocket;
|
||||
|
||||
store.set({callState: CALL_STATES.CONNECTING});
|
||||
sandbox.stub(loop.shared.utils.Helper.prototype, "locationData")
|
||||
.returns({
|
||||
hash: "#outgoing/42",
|
||||
pathname: ""
|
||||
});
|
||||
store.set({windowId: "42"});
|
||||
});
|
||||
|
||||
it("should disconnect the session", function() {
|
||||
|
|
|
@ -45,7 +45,7 @@ describe("loop.Dispatcher", function () {
|
|||
|
||||
beforeEach(function() {
|
||||
gatherAction = new sharedActions.GatherCallData({
|
||||
callId: "42",
|
||||
windowId: "42",
|
||||
outgoing: false
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,175 @@
|
|||
/* 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/. */
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource:///modules/loop/LoopRooms.jsm");
|
||||
|
||||
const kRooms = new Map([
|
||||
["_nxD4V4FflQ", {
|
||||
roomToken: "_nxD4V4FflQ",
|
||||
roomName: "First Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
|
||||
maxSize: 2,
|
||||
currSize: 0,
|
||||
ctime: 1405517546
|
||||
}],
|
||||
["QzBbvGmIZWU", {
|
||||
roomToken: "QzBbvGmIZWU",
|
||||
roomName: "Second Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU",
|
||||
maxSize: 2,
|
||||
currSize: 0,
|
||||
ctime: 140551741
|
||||
}],
|
||||
["3jKS_Els9IU", {
|
||||
roomToken: "3jKS_Els9IU",
|
||||
roomName: "Third Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/3jKS_Els9IU",
|
||||
maxSize: 3,
|
||||
clientMaxSize: 2,
|
||||
currSize: 1,
|
||||
ctime: 1405518241
|
||||
}]
|
||||
]);
|
||||
|
||||
let roomDetail = {
|
||||
roomName: "First Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
|
||||
roomOwner: "Alexis",
|
||||
maxSize: 2,
|
||||
clientMaxSize: 2,
|
||||
creationTime: 1405517546,
|
||||
expiresAt: 1405534180,
|
||||
participants: [{
|
||||
displayName: "Alexis",
|
||||
account: "alexis@example.com",
|
||||
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
|
||||
}, {
|
||||
displayName: "Adam",
|
||||
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
|
||||
}]
|
||||
};
|
||||
|
||||
const kCreateRoomProps = {
|
||||
roomName: "UX Discussion",
|
||||
expiresIn: 5,
|
||||
roomOwner: "Alexis",
|
||||
maxSize: 2
|
||||
};
|
||||
|
||||
const kCreateRoomData = {
|
||||
roomToken: "_nxD4V4FflQ",
|
||||
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
|
||||
expiresAt: 1405534180
|
||||
};
|
||||
|
||||
add_task(function* setup_server() {
|
||||
loopServer.registerPathHandler("/registration", (req, res) => {
|
||||
res.setStatusLine(null, 200, "OK");
|
||||
res.processAsync();
|
||||
res.finish();
|
||||
});
|
||||
|
||||
loopServer.registerPathHandler("/rooms", (req, res) => {
|
||||
res.setStatusLine(null, 200, "OK");
|
||||
|
||||
if (req.method == "POST") {
|
||||
Assert.ok(req.bodyInputStream, "POST request should have a payload");
|
||||
let body = CommonUtils.readBytesFromInputStream(req.bodyInputStream);
|
||||
let data = JSON.parse(body);
|
||||
Assert.deepEqual(data, kCreateRoomProps);
|
||||
|
||||
res.write(JSON.stringify(kCreateRoomData));
|
||||
} else {
|
||||
res.write(JSON.stringify([...kRooms.values()]));
|
||||
}
|
||||
|
||||
res.processAsync();
|
||||
res.finish();
|
||||
});
|
||||
|
||||
function returnRoomDetails(res, roomName) {
|
||||
roomDetail.roomName = roomName;
|
||||
res.setStatusLine(null, 200, "OK");
|
||||
res.write(JSON.stringify(roomDetail));
|
||||
res.processAsync();
|
||||
res.finish();
|
||||
}
|
||||
|
||||
// Add a request handler for each room in the list.
|
||||
[...kRooms.values()].forEach(function(room) {
|
||||
loopServer.registerPathHandler("/rooms/" + encodeURIComponent(room.roomToken), (req, res) => {
|
||||
returnRoomDetails(res, room.roomName);
|
||||
});
|
||||
});
|
||||
|
||||
loopServer.registerPathHandler("/rooms/error401", (req, res) => {
|
||||
res.setStatusLine(null, 401, "Not Found");
|
||||
res.processAsync();
|
||||
res.finish();
|
||||
});
|
||||
|
||||
loopServer.registerPathHandler("/rooms/errorMalformed", (req, res) => {
|
||||
res.setStatusLine(null, 200, "OK");
|
||||
res.write("{\"some\": \"Syntax Error!\"}}}}}}");
|
||||
res.processAsync();
|
||||
res.finish();
|
||||
});
|
||||
});
|
||||
|
||||
const normalizeRoom = function(room) {
|
||||
delete room.currSize;
|
||||
if (!("participants" in room)) {
|
||||
let name = room.roomName;
|
||||
for (let key of Object.getOwnPropertyNames(roomDetail)) {
|
||||
room[key] = roomDetail[key];
|
||||
}
|
||||
room.roomName = name;
|
||||
}
|
||||
return room;
|
||||
};
|
||||
|
||||
const compareRooms = function(room1, room2) {
|
||||
Assert.deepEqual(normalizeRoom(room1), normalizeRoom(room2));
|
||||
};
|
||||
|
||||
add_task(function* test_getAllRooms() {
|
||||
yield MozLoopService.register(mockPushHandler);
|
||||
|
||||
let rooms = yield LoopRooms.promise("getAll");
|
||||
Assert.equal(rooms.length, 3);
|
||||
for (let room of rooms) {
|
||||
compareRooms(kRooms.get(room.roomToken), room);
|
||||
}
|
||||
});
|
||||
|
||||
add_task(function* test_getRoom() {
|
||||
yield MozLoopService.register(mockPushHandler);
|
||||
|
||||
let roomToken = "_nxD4V4FflQ";
|
||||
let room = yield LoopRooms.promise("get", roomToken);
|
||||
Assert.deepEqual(room, kRooms.get(roomToken));
|
||||
});
|
||||
|
||||
add_task(function* test_errorStates() {
|
||||
yield Assert.rejects(LoopRooms.promise("get", "error401"), /Not Found/, "Fetching a non-existent room should fail");
|
||||
yield Assert.rejects(LoopRooms.promise("get", "errorMalformed"), /SyntaxError/, "Wrong message format should reject");
|
||||
});
|
||||
|
||||
add_task(function* test_createRoom() {
|
||||
let eventCalled = false;
|
||||
LoopRooms.once("add", (e, room) => {
|
||||
compareRooms(room, kCreateRoomProps);
|
||||
eventCalled = true;
|
||||
});
|
||||
let room = yield LoopRooms.promise("create", kCreateRoomProps);
|
||||
compareRooms(room, kCreateRoomProps);
|
||||
Assert.ok(eventCalled, "Event should have fired");
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
setupFakeLoopServer();
|
||||
|
||||
run_next_test();
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Chat",
|
||||
"resource:///modules/Chat.jsm");
|
||||
let hasTheseProps = function(a, b) {
|
||||
for (let prop in a) {
|
||||
if (a[prop] != b[prop]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let openChatOrig = Chat.open;
|
||||
|
||||
add_test(function test_openRoomsWindow() {
|
||||
let roomProps = {roomName: "UX Discussion",
|
||||
expiresIn: 5,
|
||||
roomOwner: "Alexis",
|
||||
maxSize: 2}
|
||||
|
||||
let roomData = {roomToken: "_nxD4V4FflQ",
|
||||
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
|
||||
expiresAt: 1405534180}
|
||||
|
||||
loopServer.registerPathHandler("/rooms", (request, response) => {
|
||||
if (!request.bodyInputStream) {
|
||||
do_throw("empty request body");
|
||||
}
|
||||
let body = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
let data = JSON.parse(body);
|
||||
do_check_true(hasTheseProps(roomProps, data));
|
||||
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
response.write(JSON.stringify(roomData));
|
||||
response.processAsync();
|
||||
response.finish();
|
||||
});
|
||||
|
||||
MozLoopService.register(mockPushHandler).then(() => {
|
||||
let opened = false;
|
||||
let created = false;
|
||||
let urlPieces = [];
|
||||
|
||||
Chat.open = function(contentWindow, origin, title, url) {
|
||||
urlPieces = url.split('/');
|
||||
do_check_eq(urlPieces[0], "about:loopconversation#room");
|
||||
opened = true;
|
||||
};
|
||||
|
||||
let returnedID = LoopRooms.createRoom(roomProps, (error, data) => {
|
||||
do_check_false(error);
|
||||
do_check_true(data);
|
||||
do_check_true(hasTheseProps(roomData, data));
|
||||
do_check_eq(data.localRoomId, urlPieces[1]);
|
||||
created = true;
|
||||
});
|
||||
|
||||
waitForCondition(function() created && opened).then(() => {
|
||||
do_check_true(opened, "should open a chat window");
|
||||
do_check_eq(returnedID, urlPieces[1]);
|
||||
|
||||
// Verify that a delayed callback, when attached,
|
||||
// received the same data.
|
||||
LoopRooms.addCallback(
|
||||
urlPieces[1], "RoomCreated",
|
||||
(error, data) => {
|
||||
do_check_false(error);
|
||||
do_check_true(data);
|
||||
do_check_true(hasTheseProps(roomData, data));
|
||||
do_check_eq(data.localRoomId, urlPieces[1]);
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}, () => {
|
||||
do_throw("should have opened a chat window");
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
function run_test()
|
||||
{
|
||||
setupFakeLoopServer();
|
||||
mockPushHandler.registrationPushURL = kEndPointUrl;
|
||||
|
||||
loopServer.registerPathHandler("/registration", (request, response) => {
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
response.processAsync();
|
||||
response.finish();
|
||||
});
|
||||
|
||||
do_register_cleanup(function() {
|
||||
// Revert original Chat.open implementation
|
||||
Chat.open = openChatOrig;
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
|
@ -1,132 +0,0 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Chat",
|
||||
"resource:///modules/Chat.jsm");
|
||||
let hasTheseProps = function(a, b) {
|
||||
for (let prop in a) {
|
||||
if (a[prop] != b[prop]) {
|
||||
do_print("hasTheseProps fail: prop = " + prop);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
let openChatOrig = Chat.open;
|
||||
|
||||
add_test(function test_getAllRooms() {
|
||||
|
||||
let roomList = [
|
||||
{ roomToken: "_nxD4V4FflQ",
|
||||
roomName: "First Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
|
||||
maxSize: 2,
|
||||
currSize: 0,
|
||||
ctime: 1405517546 },
|
||||
{ roomToken: "QzBbvGmIZWU",
|
||||
roomName: "Second Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/QzBbvGmIZWU",
|
||||
maxSize: 2,
|
||||
currSize: 0,
|
||||
ctime: 140551741 },
|
||||
{ roomToken: "3jKS_Els9IU",
|
||||
roomName: "Third Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/3jKS_Els9IU",
|
||||
maxSize: 3,
|
||||
clientMaxSize: 2,
|
||||
currSize: 1,
|
||||
ctime: 1405518241 }
|
||||
]
|
||||
|
||||
let roomDetail = {
|
||||
roomName: "First Room Name",
|
||||
roomUrl: "http://localhost:3000/rooms/_nxD4V4FflQ",
|
||||
roomOwner: "Alexis",
|
||||
maxSize: 2,
|
||||
clientMaxSize: 2,
|
||||
creationTime: 1405517546,
|
||||
expiresAt: 1405534180,
|
||||
participants: [
|
||||
{ displayName: "Alexis", account: "alexis@example.com", roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
|
||||
{ displayName: "Adam", roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
|
||||
]
|
||||
}
|
||||
|
||||
loopServer.registerPathHandler("/rooms", (request, response) => {
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
response.write(JSON.stringify(roomList));
|
||||
response.processAsync();
|
||||
response.finish();
|
||||
});
|
||||
|
||||
let returnRoomDetails = function(response, roomName) {
|
||||
roomDetail.roomName = roomName;
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
response.write(JSON.stringify(roomDetail));
|
||||
response.processAsync();
|
||||
response.finish();
|
||||
}
|
||||
|
||||
loopServer.registerPathHandler("/rooms/_nxD4V4FflQ", (request, response) => {
|
||||
returnRoomDetails(response, "First Room Name");
|
||||
});
|
||||
|
||||
loopServer.registerPathHandler("/rooms/QzBbvGmIZWU", (request, response) => {
|
||||
returnRoomDetails(response, "Second Room Name");
|
||||
});
|
||||
|
||||
loopServer.registerPathHandler("/rooms/3jKS_Els9IU", (request, response) => {
|
||||
returnRoomDetails(response, "Third Room Name");
|
||||
});
|
||||
|
||||
MozLoopService.register().then(() => {
|
||||
|
||||
LoopRooms.getAll((error, rooms) => {
|
||||
do_check_false(error);
|
||||
do_check_true(rooms);
|
||||
do_check_eq(rooms.length, 3);
|
||||
do_check_eq(rooms[0].roomName, "First Room Name");
|
||||
do_check_eq(rooms[1].roomName, "Second Room Name");
|
||||
do_check_eq(rooms[2].roomName, "Third Room Name");
|
||||
|
||||
let room = rooms[0];
|
||||
do_check_true(room.localRoomId);
|
||||
do_check_false(room.currSize);
|
||||
delete roomList[0].currSize;
|
||||
do_check_true(hasTheseProps(roomList[0], room));
|
||||
delete roomDetail.roomName;
|
||||
delete room.participants;
|
||||
delete roomDetail.participants;
|
||||
do_check_true(hasTheseProps(roomDetail, room));
|
||||
|
||||
LoopRooms.getRoomData(room.localRoomId, (error, roomData) => {
|
||||
do_check_false(error);
|
||||
do_check_true(hasTheseProps(room, roomData));
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
setupFakeLoopServer();
|
||||
mockPushHandler.registrationPushURL = kEndPointUrl;
|
||||
|
||||
loopServer.registerPathHandler("/registration", (request, response) => {
|
||||
response.setStatusLine(null, 200, "OK");
|
||||
response.processAsync();
|
||||
response.finish();
|
||||
});
|
||||
|
||||
do_register_cleanup(function() {
|
||||
// Revert original Chat.open implementation
|
||||
Chat.open = openChatOrig;
|
||||
});
|
||||
|
||||
run_next_test();
|
||||
}
|
|
@ -6,6 +6,7 @@ skip-if = toolkit == 'gonk'
|
|||
|
||||
[test_loopapi_hawk_request.js]
|
||||
[test_looppush_initialize.js]
|
||||
[test_looprooms.js]
|
||||
[test_loopservice_directcall.js]
|
||||
[test_loopservice_dnd.js]
|
||||
[test_loopservice_expiry.js]
|
||||
|
@ -21,5 +22,3 @@ skip-if = toolkit == 'gonk'
|
|||
[test_loopservice_token_send.js]
|
||||
[test_loopservice_token_validation.js]
|
||||
[test_loopservice_busy.js]
|
||||
[test_rooms_getdata.js]
|
||||
[test_rooms_create.js]
|
||||
|
|
|
@ -401,6 +401,7 @@ BrowserGlue.prototype = {
|
|||
Services.obs.removeObserver(this, "browser-search-service");
|
||||
this._syncSearchEngines();
|
||||
break;
|
||||
#ifdef NIGHTLY_BUILD
|
||||
case "nsPref:changed":
|
||||
if (data == POLARIS_ENABLED) {
|
||||
let enabled = Services.prefs.getBoolPref(POLARIS_ENABLED);
|
||||
|
@ -408,6 +409,7 @@ BrowserGlue.prototype = {
|
|||
Services.prefs.setBoolPref("privacy.trackingprotection.enabled", enabled);
|
||||
Services.prefs.setBoolPref("privacy.trackingprotection.ui.enabled", enabled);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -14,6 +14,24 @@ var gPrivacyPane = {
|
|||
*/
|
||||
_shouldPromptForRestart: true,
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
/**
|
||||
* Show the Tracking Protection UI depending on the
|
||||
* privacy.trackingprotection.ui.enabled pref, and linkify its Learn More link
|
||||
*/
|
||||
_initTrackingProtection: function () {
|
||||
if (!Services.prefs.getBoolPref("privacy.trackingprotection.ui.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let link = document.getElementById("trackingProtectionLearnMore");
|
||||
let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "tracking-protection";
|
||||
link.setAttribute("href", url);
|
||||
|
||||
document.getElementById("trackingprotectionbox").hidden = false;
|
||||
},
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Sets up the UI for the number of days of history to keep, and updates the
|
||||
* label of the "Clear Now..." button.
|
||||
|
@ -31,6 +49,9 @@ var gPrivacyPane = {
|
|||
this.updateHistoryModePane();
|
||||
this.updatePrivacyMicroControls();
|
||||
this.initAutoStartPrivateBrowsingReverter();
|
||||
#ifdef NIGHTLY_BUILD
|
||||
this._initTrackingProtection();
|
||||
#endif
|
||||
|
||||
setEventListener("browser.urlbar.default.behavior", "change",
|
||||
document.getElementById('browser.urlbar.autocomplete.enabled')
|
||||
|
|
|
@ -13,6 +13,9 @@
|
|||
<preference id="privacy.donottrackheader.enabled"
|
||||
name="privacy.donottrackheader.enabled"
|
||||
type="bool"/>
|
||||
<preference id="privacy.trackingprotection.enabled"
|
||||
name="privacy.trackingprotection.enabled"
|
||||
type="bool"/>
|
||||
|
||||
<!-- XXX button prefs -->
|
||||
<preference id="pref.privacy.disable_button.cookie_exceptions"
|
||||
|
@ -71,6 +74,19 @@
|
|||
<!-- Tracking -->
|
||||
<groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" align="start">
|
||||
<caption><label>&tracking.label;</label></caption>
|
||||
<vbox id="trackingprotectionbox" hidden="true">
|
||||
<hbox align="center">
|
||||
<checkbox id="trackingProtection"
|
||||
preference="privacy.trackingprotection.enabled"
|
||||
accesskey="&trackingProtection.accesskey;"
|
||||
label="&trackingProtection.label;" />
|
||||
<image id="trackingProtectionImage" src="chrome://browser/skin/bad-content-blocked-16.png"/>
|
||||
</hbox>
|
||||
<label id="trackingProtectionLearnMore"
|
||||
class="text-link"
|
||||
value="&trackingProtectionLearnMore.label;"/>
|
||||
<separator/>
|
||||
</vbox>
|
||||
<checkbox id="privacyDoNotTrackCheckbox"
|
||||
label="&dntTrackingNotOkay.label2;"
|
||||
accesskey="&dntTrackingNotOkay.accesskey;"
|
||||
|
|
|
@ -17,6 +17,24 @@ var gPrivacyPane = {
|
|||
*/
|
||||
_shouldPromptForRestart: true,
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
/**
|
||||
* Show the Tracking Protection UI depending on the
|
||||
* privacy.trackingprotection.ui.enabled pref, and linkify its Learn More link
|
||||
*/
|
||||
_initTrackingProtection: function () {
|
||||
if (!Services.prefs.getBoolPref("privacy.trackingprotection.ui.enabled")) {
|
||||
return;
|
||||
}
|
||||
|
||||
let link = document.getElementById("trackingProtectionLearnMore");
|
||||
let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "tracking-protection";
|
||||
link.setAttribute("href", url);
|
||||
|
||||
document.getElementById("trackingprotectionbox").hidden = false;
|
||||
},
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Sets up the UI for the number of days of history to keep, and updates the
|
||||
* label of the "Clear Now..." button.
|
||||
|
@ -28,6 +46,9 @@ var gPrivacyPane = {
|
|||
this.updateHistoryModePane();
|
||||
this.updatePrivacyMicroControls();
|
||||
this.initAutoStartPrivateBrowsingReverter();
|
||||
#ifdef NIGHTLY_BUILD
|
||||
this._initTrackingProtection();
|
||||
#endif
|
||||
},
|
||||
|
||||
// HISTORY MODE
|
||||
|
|
|
@ -26,6 +26,9 @@
|
|||
<preference id="privacy.donottrackheader.enabled"
|
||||
name="privacy.donottrackheader.enabled"
|
||||
type="bool"/>
|
||||
<preference id="privacy.trackingprotection.enabled"
|
||||
name="privacy.trackingprotection.enabled"
|
||||
type="bool"/>
|
||||
|
||||
<!-- XXX button prefs -->
|
||||
<preference id="pref.privacy.disable_button.cookie_exceptions"
|
||||
|
@ -81,6 +84,19 @@
|
|||
<!-- Tracking -->
|
||||
<groupbox id="trackingGroup" align="start">
|
||||
<caption label="&tracking.label;"/>
|
||||
<vbox id="trackingprotectionbox" hidden="true">
|
||||
<hbox align="center">
|
||||
<checkbox id="trackingProtection"
|
||||
preference="privacy.trackingprotection.enabled"
|
||||
accesskey="&trackingProtection.accesskey;"
|
||||
label="&trackingProtection.label;" />
|
||||
<image id="trackingProtectionImage" src="chrome://browser/skin/bad-content-blocked-16.png"/>
|
||||
</hbox>
|
||||
<label id="trackingProtectionLearnMore"
|
||||
class="text-link"
|
||||
value="&trackingProtectionLearnMore.label;"/>
|
||||
<separator/>
|
||||
</vbox>
|
||||
<checkbox id="privacyDoNotTrackCheckbox"
|
||||
label="&dntTrackingNotOkay.label2;"
|
||||
accesskey="&dntTrackingNotOkay.accesskey;"
|
||||
|
|
|
@ -23,12 +23,24 @@ function* testPrefs(test) {
|
|||
}
|
||||
}
|
||||
|
||||
function isNightly() {
|
||||
return Services.appinfo.version.contains("a1");
|
||||
}
|
||||
|
||||
add_task(function* test_default_values() {
|
||||
if (!isNightly()) {
|
||||
ok(true, "Skipping test, not Nightly")
|
||||
return;
|
||||
}
|
||||
Assert.ok(!Services.prefs.getBoolPref(POLARIS_ENABLED), POLARIS_ENABLED + " is disabled by default.");
|
||||
Assert.ok(!Services.prefs.getBoolPref(PREF_TPUI), PREF_TPUI + "is disabled by default.");
|
||||
});
|
||||
|
||||
add_task(function* test_changing_pref_changes_tracking() {
|
||||
if (!isNightly()) {
|
||||
ok(true, "Skipping test, not Nightly")
|
||||
return;
|
||||
}
|
||||
function* testPref(pref) {
|
||||
Services.prefs.setBoolPref(POLARIS_ENABLED, true);
|
||||
yield assertPref(pref, true);
|
||||
|
@ -41,6 +53,10 @@ add_task(function* test_changing_pref_changes_tracking() {
|
|||
});
|
||||
|
||||
add_task(function* test_prefs_can_be_changed_individually() {
|
||||
if (!isNightly()) {
|
||||
ok(true, "Skipping test, not Nightly")
|
||||
return;
|
||||
}
|
||||
function* testPref(pref) {
|
||||
Services.prefs.setBoolPref(POLARIS_ENABLED, true);
|
||||
yield assertPref(pref, true);
|
||||
|
|
|
@ -44,8 +44,14 @@ const TIMELINE_BLUEPRINT = {
|
|||
stroke: "hsl(39,82%,49%)",
|
||||
label: L10N.getStr("timeline.label.paint")
|
||||
},
|
||||
"ConsoleTime": {
|
||||
"DOMEvent": {
|
||||
group: 3,
|
||||
fill: "hsl(219,82%,69%)",
|
||||
stroke: "hsl(219,82%,69%)",
|
||||
label: L10N.getStr("timeline.label.domevent")
|
||||
},
|
||||
"ConsoleTime": {
|
||||
group: 4,
|
||||
fill: "hsl(0,0%,80%)",
|
||||
stroke: "hsl(0,0%,60%)",
|
||||
label: L10N.getStr("timeline.label.consoleTime")
|
||||
|
|
|
@ -17,6 +17,12 @@ setDefaultBrowserNotNow.accesskey = N
|
|||
setDefaultBrowserNever.label = Don't ask me again
|
||||
setDefaultBrowserNever.accesskey = D
|
||||
|
||||
# LOCALIZATION NOTE (setDefaultBrowserTitle, setDefaultBrowserMessage, setDefaultBrowserDontAsk):
|
||||
# These strings are used as an alternative to the ones above, in a modal dialog.
|
||||
# %S will be replaced by brandShortName
|
||||
setDefaultBrowserTitle=Default Browser
|
||||
setDefaultBrowserMessage=%S is not currently set as your default browser. Would you like to make it your default browser?
|
||||
setDefaultBrowserDontAsk=Always perform this check when starting %S.
|
||||
|
||||
desktopBackgroundLeafNameWin=Desktop Background.bmp
|
||||
DesktopBackgroundDownloading=Saving Picture…
|
||||
|
|
Двоичные данные
browser/themes/linux/Toolbar-inverted.png
До Ширина: | Высота: | Размер: 11 KiB После Ширина: | Высота: | Размер: 12 KiB |
Двоичные данные
browser/themes/linux/Toolbar.png
До Ширина: | Высота: | Размер: 14 KiB После Ширина: | Высота: | Размер: 16 KiB |
Двоичные данные
browser/themes/linux/downloads/download-glow.png
До Ширина: | Высота: | Размер: 445 B |
|
@ -32,7 +32,11 @@ toolbar[brighttext] #downloads-button[cui-areatype="toolbar"]:not([attention]) >
|
|||
}
|
||||
|
||||
#downloads-button[cui-areatype="toolbar"][attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button[cui-areatype="toolbar"][attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
#downloads-button[cui-areatype="menu-panel"][attention] {
|
||||
|
@ -54,7 +58,11 @@ toolbar[brighttext] #downloads-button:not([counter]):not([attention]) > #downloa
|
|||
}
|
||||
|
||||
#downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
/*** Download notifications ***/
|
||||
|
|
|
@ -108,7 +108,6 @@ browser.jar:
|
|||
skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
|
||||
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
|
||||
skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
|
||||
skin/classic/browser/downloads/download-glow.png (downloads/download-glow.png)
|
||||
skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
|
||||
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
|
||||
skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)
|
||||
|
|
Двоичные данные
browser/themes/osx/Toolbar-inverted.png
До Ширина: | Высота: | Размер: 27 KiB После Ширина: | Высота: | Размер: 29 KiB |
Двоичные данные
browser/themes/osx/Toolbar-inverted@2x.png
До Ширина: | Высота: | Размер: 65 KiB После Ширина: | Высота: | Размер: 72 KiB |
Двоичные данные
browser/themes/osx/Toolbar-yosemite.png
До Ширина: | Высота: | Размер: 17 KiB После Ширина: | Высота: | Размер: 18 KiB |
Двоичные данные
browser/themes/osx/Toolbar-yosemite@2x.png
До Ширина: | Высота: | Размер: 39 KiB После Ширина: | Высота: | Размер: 43 KiB |
Двоичные данные
browser/themes/osx/Toolbar.png
До Ширина: | Высота: | Размер: 26 KiB После Ширина: | Высота: | Размер: 28 KiB |
Двоичные данные
browser/themes/osx/Toolbar@2x.png
До Ширина: | Высота: | Размер: 70 KiB После Ширина: | Высота: | Размер: 78 KiB |
Двоичные данные
browser/themes/osx/downloads/download-glow.png
До Ширина: | Высота: | Размер: 676 B |
Двоичные данные
browser/themes/osx/downloads/download-glow@2x.png
До Ширина: | Высота: | Размер: 1.2 KiB |
|
@ -34,7 +34,11 @@ toolbar[brighttext] #downloads-indicator-icon {
|
|||
}
|
||||
|
||||
#downloads-button[attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"), 36, 198, 54, 180);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button[attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 36, 198, 54, 180);
|
||||
}
|
||||
|
||||
#downloads-button[cui-areatype="menu-panel"][attention] {
|
||||
|
@ -56,7 +60,11 @@ toolbar[brighttext] #downloads-button:not([counter]):not([attention]) > #downloa
|
|||
}
|
||||
|
||||
#downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"), 36, 198, 54, 180);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 36, 198, 54, 180);
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
|
@ -79,7 +87,11 @@ toolbar[brighttext] #downloads-button:not([counter]):not([attention]) > #downloa
|
|||
}
|
||||
|
||||
#downloads-button[attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow@2x.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar@2x.png"), 72, 396, 108, 360);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button[attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted@2x.png"), 72, 396, 108, 360);
|
||||
}
|
||||
|
||||
#downloads-button[cui-areatype="menu-panel"][attention] {
|
||||
|
@ -87,7 +99,11 @@ toolbar[brighttext] #downloads-button:not([counter]):not([attention]) > #downloa
|
|||
}
|
||||
|
||||
#downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow@2x.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar@2x.png"), 72, 396, 108, 360);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted@2x.png"), 72, 396, 108, 360);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -182,8 +182,6 @@ browser.jar:
|
|||
skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
|
||||
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
|
||||
skin/classic/browser/downloads/buttons@2x.png (downloads/buttons@2x.png)
|
||||
skin/classic/browser/downloads/download-glow.png (downloads/download-glow.png)
|
||||
skin/classic/browser/downloads/download-glow@2x.png (downloads/download-glow@2x.png)
|
||||
skin/classic/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
|
||||
skin/classic/browser/downloads/download-glow-menuPanel@2x.png (downloads/download-glow-menuPanel@2x.png)
|
||||
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
|
||||
|
|
Двоичные данные
browser/themes/windows/Toolbar-XP.png
До Ширина: | Высота: | Размер: 16 KiB После Ширина: | Высота: | Размер: 17 KiB |
Двоичные данные
browser/themes/windows/Toolbar-aero.png
До Ширина: | Высота: | Размер: 15 KiB После Ширина: | Высота: | Размер: 17 KiB |
Двоичные данные
browser/themes/windows/Toolbar-inverted.png
До Ширина: | Высота: | Размер: 11 KiB После Ширина: | Высота: | Размер: 12 KiB |
Двоичные данные
browser/themes/windows/Toolbar-lunaSilver.png
До Ширина: | Высота: | Размер: 15 KiB После Ширина: | Высота: | Размер: 17 KiB |
Двоичные данные
browser/themes/windows/Toolbar.png
До Ширина: | Высота: | Размер: 6.0 KiB После Ширина: | Высота: | Размер: 6.5 KiB |
|
@ -32,7 +32,11 @@ toolbar[brighttext] #downloads-button:not([attention]) > #downloads-indicator-an
|
|||
}
|
||||
|
||||
#downloads-button[attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button[attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
#downloads-button[cui-areatype="menu-panel"][attention] {
|
||||
|
@ -44,10 +48,6 @@ toolbar[brighttext] #downloads-button:not([attention]) > #downloads-indicator-an
|
|||
@media (-moz-os-version: windows-vista),
|
||||
(-moz-os-version: windows-win7) {
|
||||
%endif
|
||||
#downloads-button[attention] > #downloads-indicator-anchor > #downloads-indicator-icon {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow-XPVista7.png");
|
||||
}
|
||||
|
||||
#downloads-button[cui-areatype="menu-panel"][attention] {
|
||||
list-style-image: url("chrome://browser/skin/downloads/download-glow-menuPanel-XPVista7.png");
|
||||
}
|
||||
|
@ -69,7 +69,11 @@ toolbar[brighttext] #downloads-button:not([counter]):not([attention]) > #downloa
|
|||
}
|
||||
|
||||
#downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: url("chrome://browser/skin/downloads/download-glow.png");
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
toolbar[brighttext] #downloads-button:not([counter])[attention] > #downloads-indicator-anchor > #downloads-indicator-progress-area > #downloads-indicator-counter {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/Toolbar-inverted.png"), 18, 198, 36, 180);
|
||||
}
|
||||
|
||||
/*** Download notifications ***/
|
||||
|
|
|
@ -132,7 +132,6 @@ browser.jar:
|
|||
* skin/classic/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay.css)
|
||||
skin/classic/browser/downloads/buttons.png (downloads/buttons.png)
|
||||
skin/classic/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
|
||||
skin/classic/browser/downloads/download-glow-XPVista7.png (downloads/download-glow-XPVista7.png)
|
||||
skin/classic/browser/downloads/download-glow-menuPanel-XPVista7.png (downloads/download-glow-menuPanel-XPVista7.png)
|
||||
skin/classic/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
|
||||
skin/classic/browser/downloads/download-notification-start.png (downloads/download-notification-start.png)
|
||||
|
@ -564,8 +563,6 @@ browser.jar:
|
|||
* skin/classic/aero/browser/downloads/allDownloadsViewOverlay.css (downloads/allDownloadsViewOverlay-aero.css)
|
||||
skin/classic/aero/browser/downloads/buttons.png (downloads/buttons-aero.png)
|
||||
skin/classic/aero/browser/downloads/contentAreaDownloadsView.css (downloads/contentAreaDownloadsView.css)
|
||||
skin/classic/aero/browser/downloads/download-glow.png (downloads/download-glow.png)
|
||||
skin/classic/aero/browser/downloads/download-glow-XPVista7.png (downloads/download-glow-XPVista7.png)
|
||||
skin/classic/aero/browser/downloads/download-glow-menuPanel.png (downloads/download-glow-menuPanel.png)
|
||||
skin/classic/aero/browser/downloads/download-glow-menuPanel-XPVista7.png (downloads/download-glow-menuPanel-XPVista7.png)
|
||||
skin/classic/aero/browser/downloads/download-notification-finish.png (downloads/download-notification-finish.png)
|
||||
|
|
|
@ -2885,6 +2885,11 @@ nsDocShell::PopProfileTimelineMarkers(JSContext* aCx,
|
|||
if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
|
||||
bool hasSeenEnd = false;
|
||||
|
||||
// DOM events can be nested, so we must take care when searching
|
||||
// for the matching end. It doesn't hurt to apply this logic to
|
||||
// all event types.
|
||||
uint32_t markerDepth = 0;
|
||||
|
||||
// The assumption is that the devtools timeline flushes markers frequently
|
||||
// enough for the amount of markers to always be small enough that the
|
||||
// nested for loop isn't going to be a performance problem.
|
||||
|
@ -2898,11 +2903,18 @@ nsDocShell::PopProfileTimelineMarkers(JSContext* aCx,
|
|||
hasSeenPaintedLayer = true;
|
||||
}
|
||||
|
||||
bool isSameMarkerType = strcmp(startMarkerName, endMarkerName) == 0;
|
||||
if (strcmp(startMarkerName, endMarkerName) != 0) {
|
||||
continue;
|
||||
}
|
||||
bool isPaint = strcmp(startMarkerName, "Paint") == 0;
|
||||
|
||||
// Pair start and end markers.
|
||||
if (endPayload->GetMetaData() == TRACING_INTERVAL_END && isSameMarkerType) {
|
||||
if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
|
||||
++markerDepth;
|
||||
} else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
|
||||
if (markerDepth > 0) {
|
||||
--markerDepth;
|
||||
} else {
|
||||
// But ignore paint start/end if no layer has been painted.
|
||||
if (!isPaint || (isPaint && hasSeenPaintedLayer)) {
|
||||
mozilla::dom::ProfileTimelineMarker marker;
|
||||
|
@ -2918,6 +2930,7 @@ nsDocShell::PopProfileTimelineMarkers(JSContext* aCx,
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we did not see the corresponding END, keep the START.
|
||||
if (!hasSeenEnd) {
|
||||
|
|
|
@ -35,6 +35,7 @@ support-files =
|
|||
file_bug1046022.html
|
||||
print_postdata.sjs
|
||||
test-form_sjis.html
|
||||
timelineMarkers-04.html
|
||||
|
||||
[browser_bug134911.js]
|
||||
skip-if = e10s # Bug ?????? - BrowserSetForcedCharacterSet() in browser.js references docShell
|
||||
|
@ -98,3 +99,7 @@ skip-if = e10s
|
|||
[browser_timelineMarkers-01.js]
|
||||
[browser_timelineMarkers-02.js]
|
||||
skip-if = e10s
|
||||
[browser_timelineMarkers-03.js]
|
||||
skip-if = e10s
|
||||
[browser_timelineMarkers-04.js]
|
||||
skip-if = e10s
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the docShell profile timeline API returns the right
|
||||
// markers for DOM events.
|
||||
|
||||
let TESTS = [{
|
||||
desc: "Event dispatch with single handler",
|
||||
setup: function() {
|
||||
content.document.body.addEventListener("dog",
|
||||
function(e) { console.log("hi"); },
|
||||
true);
|
||||
content.document.body.dispatchEvent(new Event("dog"));
|
||||
},
|
||||
check: function(markers) {
|
||||
is(markers.length, 1, "Got 1 marker");
|
||||
}
|
||||
}, {
|
||||
desc: "Event dispatch with a second handler",
|
||||
setup: function() {
|
||||
content.document.body.addEventListener("dog",
|
||||
function(e) { console.log("hi"); },
|
||||
false);
|
||||
content.document.body.dispatchEvent(new Event("dog"));
|
||||
},
|
||||
check: function(markers) {
|
||||
is(markers.length, 2, "Got 2 markers");
|
||||
}
|
||||
}, {
|
||||
desc: "Event dispatch on a new document",
|
||||
setup: function() {
|
||||
let doc = content.document.implementation.createHTMLDocument("doc");
|
||||
let p = doc.createElement("p");
|
||||
p.innerHTML = "inside";
|
||||
doc.body.appendChild(p);
|
||||
|
||||
p.addEventListener("zebra", function(e) {console.log("hi");});
|
||||
p.dispatchEvent(new Event("zebra"));
|
||||
},
|
||||
check: function(markers) {
|
||||
is(markers.length, 1, "Got 1 marker");
|
||||
}
|
||||
}, {
|
||||
desc: "Event dispatch on window",
|
||||
setup: function() {
|
||||
let doc = content.window.addEventListener("aardvark", function(e) {
|
||||
console.log("I like ants!");
|
||||
});
|
||||
|
||||
content.window.dispatchEvent(new Event("aardvark"));
|
||||
},
|
||||
check: function(markers) {
|
||||
is(markers.length, 1, "Got 1 marker");
|
||||
}
|
||||
}];
|
||||
|
||||
let test = Task.async(function*() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
yield openUrl("data:text/html;charset=utf-8,Test page");
|
||||
|
||||
let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
|
||||
info("Start recording");
|
||||
docShell.recordProfileTimelineMarkers = true;
|
||||
|
||||
for (let {desc, setup, check} of TESTS) {
|
||||
|
||||
info("Running test: " + desc);
|
||||
|
||||
info("Flushing the previous markers if any");
|
||||
docShell.popProfileTimelineMarkers();
|
||||
|
||||
info("Running the test setup function");
|
||||
let onMarkers = waitForMarkers(docShell);
|
||||
setup();
|
||||
info("Waiting for new markers on the docShell");
|
||||
let markers = yield onMarkers;
|
||||
|
||||
info("Running the test check function");
|
||||
check(markers.filter(m => m.name == "DOMEvent"));
|
||||
}
|
||||
|
||||
info("Stop recording");
|
||||
docShell.recordProfileTimelineMarkers = false;
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function openUrl(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
window.focus();
|
||||
|
||||
let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
|
||||
let linkedBrowser = tab.linkedBrowser;
|
||||
|
||||
linkedBrowser.addEventListener("load", function onload() {
|
||||
linkedBrowser.removeEventListener("load", onload, true);
|
||||
resolve(tab);
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForMarkers(docshell) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let waitIterationCount = 0;
|
||||
let maxWaitIterationCount = 10; // Wait for 2sec maximum
|
||||
|
||||
let interval = setInterval(() => {
|
||||
let markers = docshell.popProfileTimelineMarkers();
|
||||
if (markers.length > 0) {
|
||||
clearInterval(interval);
|
||||
resolve(markers);
|
||||
}
|
||||
if (waitIterationCount > maxWaitIterationCount) {
|
||||
clearInterval(interval);
|
||||
resolve([]);
|
||||
}
|
||||
waitIterationCount++;
|
||||
}, 200);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the docShell profile timeline API returns the right
|
||||
// markers for XMLHttpRequest events.
|
||||
|
||||
let TESTS = [{
|
||||
desc: "Event dispatch from XMLHttpRequest",
|
||||
setup: function() {
|
||||
content.dispatchEvent(new Event("dog"));
|
||||
},
|
||||
check: function(markers) {
|
||||
// One subtlety here is that we have five events: the event we
|
||||
// inject in "setup", plus the four state transition events. The
|
||||
// first state transition is reported synchronously and so should
|
||||
// show up as a nested marker.
|
||||
is(markers.length, 5, "Got 5 markers");
|
||||
}
|
||||
}];
|
||||
|
||||
let test = Task.async(function*() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
const testDir = "http://mochi.test:8888/browser/docshell/test/browser/";
|
||||
const testName = "timelineMarkers-04.html";
|
||||
|
||||
yield openUrl(testDir + testName);
|
||||
|
||||
let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIWebNavigation)
|
||||
.QueryInterface(Ci.nsIDocShell);
|
||||
|
||||
info("Start recording");
|
||||
docShell.recordProfileTimelineMarkers = true;
|
||||
|
||||
for (let {desc, setup, check} of TESTS) {
|
||||
|
||||
info("Running test: " + desc);
|
||||
|
||||
info("Flushing the previous markers if any");
|
||||
docShell.popProfileTimelineMarkers();
|
||||
|
||||
info("Running the test setup function");
|
||||
let onMarkers = waitForMarkers(docShell);
|
||||
setup();
|
||||
info("Waiting for new markers on the docShell");
|
||||
let markers = yield onMarkers;
|
||||
|
||||
info("Running the test check function");
|
||||
check(markers.filter(m => m.name == "DOMEvent"));
|
||||
}
|
||||
|
||||
info("Stop recording");
|
||||
docShell.recordProfileTimelineMarkers = false;
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
finish();
|
||||
});
|
||||
|
||||
function openUrl(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
window.focus();
|
||||
|
||||
let tab = window.gBrowser.selectedTab = window.gBrowser.addTab(url);
|
||||
let linkedBrowser = tab.linkedBrowser;
|
||||
|
||||
linkedBrowser.addEventListener("load", function onload() {
|
||||
linkedBrowser.removeEventListener("load", onload, true);
|
||||
resolve(tab);
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
function waitForMarkers(docshell) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
let waitIterationCount = 0;
|
||||
let maxWaitIterationCount = 10; // Wait for 2sec maximum
|
||||
|
||||
let interval = setInterval(() => {
|
||||
let markers = docshell.popProfileTimelineMarkers();
|
||||
if (markers.length > 0) {
|
||||
clearInterval(interval);
|
||||
resolve(markers);
|
||||
}
|
||||
if (waitIterationCount > maxWaitIterationCount) {
|
||||
clearInterval(interval);
|
||||
resolve([]);
|
||||
}
|
||||
waitIterationCount++;
|
||||
}, 200);
|
||||
});
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"></meta>
|
||||
<title>markers test</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Test page</p>
|
||||
|
||||
<script>
|
||||
function do_xhr() {
|
||||
const theURL = "timelineMarkers-04.html";
|
||||
|
||||
xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = function() {
|
||||
// Nothing.
|
||||
};
|
||||
xhr.open("get", theURL, true);
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
window.addEventListener("dog", do_xhr, true);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
#include "mozilla/AddonPathService.h"
|
||||
#include "mozilla/BasicEvents.h"
|
||||
#include "mozilla/CycleCollectedJSRuntime.h"
|
||||
#include "mozilla/DOMEventTargetHelper.h"
|
||||
#include "mozilla/EventDispatcher.h"
|
||||
#include "mozilla/EventListenerManager.h"
|
||||
#ifdef MOZ_B2G
|
||||
|
@ -26,6 +27,7 @@
|
|||
#include "nsCOMArray.h"
|
||||
#include "nsCOMPtr.h"
|
||||
#include "nsContentUtils.h"
|
||||
#include "nsDocShell.h"
|
||||
#include "nsDOMCID.h"
|
||||
#include "nsError.h"
|
||||
#include "nsGkAtoms.h"
|
||||
|
@ -977,6 +979,47 @@ EventListenerManager::HandleEventSubType(Listener* aListener,
|
|||
return result;
|
||||
}
|
||||
|
||||
nsIDocShell*
|
||||
EventListenerManager::GetDocShellForTarget()
|
||||
{
|
||||
nsCOMPtr<nsINode> node(do_QueryInterface(mTarget));
|
||||
nsIDocument* doc = nullptr;
|
||||
nsIDocShell* docShell = nullptr;
|
||||
|
||||
if (node) {
|
||||
doc = node->OwnerDoc();
|
||||
if (!doc->GetDocShell()) {
|
||||
bool ignore;
|
||||
nsCOMPtr<nsPIDOMWindow> window =
|
||||
do_QueryInterface(doc->GetScriptHandlingObject(ignore));
|
||||
if (window) {
|
||||
doc = window->GetExtantDoc();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
nsCOMPtr<nsPIDOMWindow> window = GetTargetAsInnerWindow();
|
||||
if (window) {
|
||||
doc = window->GetExtantDoc();
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc) {
|
||||
nsCOMPtr<DOMEventTargetHelper> helper(do_QueryInterface(mTarget));
|
||||
if (helper) {
|
||||
nsPIDOMWindow* window = helper->GetOwner();
|
||||
if (window) {
|
||||
doc = window->GetExtantDoc();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (doc) {
|
||||
docShell = doc->GetDocShell();
|
||||
}
|
||||
|
||||
return docShell;
|
||||
}
|
||||
|
||||
/**
|
||||
* Causes a check for event listeners and processing by them if they exist.
|
||||
* @param an event listener
|
||||
|
@ -1028,10 +1071,28 @@ EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
|
|||
}
|
||||
}
|
||||
|
||||
// Maybe add a marker to the docshell's timeline, but only
|
||||
// bother with all the logic if some docshell is recording.
|
||||
nsCOMPtr<nsIDocShell> docShell;
|
||||
if (mIsMainThreadELM &&
|
||||
nsDocShell::gProfileTimelineRecordingsCount > 0 &&
|
||||
listener->mListenerType != Listener::eNativeListener) {
|
||||
docShell = GetDocShellForTarget();
|
||||
if (docShell) {
|
||||
nsDocShell* ds = static_cast<nsDocShell*>(docShell.get());
|
||||
ds->AddProfileTimelineMarker("DOMEvent", TRACING_INTERVAL_START);
|
||||
}
|
||||
}
|
||||
|
||||
if (NS_FAILED(HandleEventSubType(listener, *aDOMEvent,
|
||||
aCurrentTarget))) {
|
||||
aEvent->mFlags.mExceptionHasBeenRisen = true;
|
||||
}
|
||||
|
||||
if (docShell) {
|
||||
nsDocShell* ds = static_cast<nsDocShell*>(docShell.get());
|
||||
ds->AddProfileTimelineMarker("DOMEvent", TRACING_INTERVAL_END);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#include "nsIDOMEventListener.h"
|
||||
#include "nsTObserverArray.h"
|
||||
|
||||
class nsIDocShell;
|
||||
class nsIDOMEvent;
|
||||
class nsIEventListenerInfo;
|
||||
class nsIScriptContext;
|
||||
|
@ -420,6 +421,8 @@ protected:
|
|||
nsIDOMEvent* aDOMEvent,
|
||||
dom::EventTarget* aCurrentTarget);
|
||||
|
||||
nsIDocShell* GetDocShellForTarget();
|
||||
|
||||
/**
|
||||
* Compile the "inline" event listener for aListener. The
|
||||
* body of the listener can be provided in aBody; if this is null we
|
||||
|
|
|
@ -130,6 +130,7 @@ include('/ipc/chromium/chromium-config.mozbuild')
|
|||
|
||||
FINAL_LIBRARY = 'xul'
|
||||
LOCAL_INCLUDES += [
|
||||
'/docshell/base',
|
||||
'/dom/base',
|
||||
'/dom/html',
|
||||
'/dom/settings',
|
||||
|
|
|
@ -2539,7 +2539,8 @@ public class BrowserApp extends GeckoApp
|
|||
@Override
|
||||
public void openOptionsMenu() {
|
||||
// Disable menu access (for hardware buttons) when the software menu button is inaccessible.
|
||||
if (mBrowserToolbar.isEditing()) {
|
||||
// Note that the software button is always accessible on new tablet.
|
||||
if (mBrowserToolbar.isEditing() && !NewTabletUI.isEnabled(this)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ import android.util.Log;
|
|||
class ChromeCast implements GeckoMediaPlayer {
|
||||
private static final boolean SHOW_DEBUG = false;
|
||||
|
||||
static final String MIRROR_RECEIVER_APP_ID = "5F72F863";
|
||||
static final String MIRROR_RECEIVER_APP_ID = "08FF1091";
|
||||
|
||||
private final Context context;
|
||||
private final RouteInfo route;
|
||||
|
|
|
@ -188,8 +188,10 @@ public class TabStripItemView extends ThemedLinearLayout
|
|||
// The surrounding tab strip dividers need to be hidden
|
||||
// when a tab item enters pressed state.
|
||||
View parent = (View) getParent();
|
||||
if (parent != null) {
|
||||
parent.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
void updateFromTab(Tab tab) {
|
||||
if (tab == null) {
|
||||
|
|
|
@ -13,6 +13,8 @@ import android.graphics.drawable.Drawable;
|
|||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.ViewTreeObserver.OnPreDrawListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -73,9 +75,21 @@ public class TabStripView extends TwoWayView {
|
|||
final int selected = getPositionForSelectedTab();
|
||||
if (selected != -1) {
|
||||
updateSelectedStyle(selected);
|
||||
ensurePositionIsVisible(selected);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensurePositionIsVisible(final int position) {
|
||||
getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
smoothScrollToPosition(position);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int getCheckedIndex(int childCount) {
|
||||
final int checkedIndex = getCheckedItemPosition() - getFirstVisiblePosition();
|
||||
if (checkedIndex < 0 || checkedIndex > childCount - 1) {
|
||||
|
|
|
@ -10,6 +10,7 @@ import org.mozilla.gecko.animation.PropertyAnimator;
|
|||
import org.mozilla.gecko.animation.ViewHelper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
/**
|
||||
|
@ -20,8 +21,18 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
|
|||
|
||||
private static final int FORWARD_ANIMATION_DURATION = 450;
|
||||
|
||||
private enum ForwardButtonState {
|
||||
HIDDEN,
|
||||
DISPLAYED,
|
||||
TRANSITIONING,
|
||||
}
|
||||
|
||||
private final int forwardButtonTranslationWidth;
|
||||
|
||||
private ForwardButtonState forwardButtonState;
|
||||
|
||||
private boolean backButtonWasEnabledOnStartEditing;
|
||||
|
||||
public BrowserToolbarNewTablet(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
|
@ -32,6 +43,19 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
|
|||
// so translate it for start of the expansion animation; future
|
||||
// iterations translate it to this position when hiding and will already be set up.
|
||||
ViewHelper.setTranslationX(forwardButton, -forwardButtonTranslationWidth);
|
||||
|
||||
// TODO: Move this to *TabletBase when old tablet is removed.
|
||||
// We don't want users clicking the forward button in transitions, but we don't want it to
|
||||
// look disabled to avoid flickering complications (e.g. disabled in editing mode), so undo
|
||||
// the work of the super class' constructor.
|
||||
setButtonEnabled(forwardButton, true);
|
||||
|
||||
updateForwardButtonState(ForwardButtonState.HIDDEN);
|
||||
}
|
||||
|
||||
private void updateForwardButtonState(final ForwardButtonState state) {
|
||||
forwardButtonState = state;
|
||||
forwardButton.setEnabled(forwardButtonState == ForwardButtonState.DISPLAYED);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -52,14 +76,11 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
|
|||
@Override
|
||||
protected void animateForwardButton(final ForwardButtonAnimation animation) {
|
||||
final boolean willShowForward = (animation == ForwardButtonAnimation.SHOW);
|
||||
|
||||
// If we're not in the appropriate state to start a particular animation,
|
||||
// then we must be in the opposite state and do not need to animate.
|
||||
final float forwardOffset = ViewHelper.getTranslationX(forwardButton);
|
||||
if ((forwardOffset >= 0 && willShowForward) ||
|
||||
forwardOffset < 0 && !willShowForward) {
|
||||
if ((forwardButtonState != ForwardButtonState.HIDDEN && willShowForward) ||
|
||||
(forwardButtonState != ForwardButtonState.DISPLAYED && !willShowForward)) {
|
||||
return;
|
||||
}
|
||||
updateForwardButtonState(ForwardButtonState.TRANSITIONING);
|
||||
|
||||
// We want the forward button to show immediately when switching tabs
|
||||
final PropertyAnimator forwardAnim =
|
||||
|
@ -88,6 +109,7 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
|
|||
|
||||
@Override
|
||||
public void onPropertyAnimationEnd() {
|
||||
final ForwardButtonState newForwardButtonState;
|
||||
if (willShowForward) {
|
||||
// Increase the margins to ensure the text does not run outside the View.
|
||||
MarginLayoutParams layoutParams =
|
||||
|
@ -96,9 +118,14 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
|
|||
|
||||
layoutParams = (MarginLayoutParams) urlEditLayout.getLayoutParams();
|
||||
layoutParams.leftMargin = forwardButtonTranslationWidth;
|
||||
|
||||
newForwardButtonState = ForwardButtonState.DISPLAYED;
|
||||
} else {
|
||||
newForwardButtonState = ForwardButtonState.HIDDEN;
|
||||
}
|
||||
|
||||
urlDisplayLayout.finishForwardAnimation();
|
||||
updateForwardButtonState(newForwardButtonState);
|
||||
|
||||
requestLayout();
|
||||
}
|
||||
|
@ -133,4 +160,38 @@ class BrowserToolbarNewTablet extends BrowserToolbarTabletBase {
|
|||
public void triggerTabsPanelTransition(final PropertyAnimator animator, final boolean areTabsShown) {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startEditing(final String url, final PropertyAnimator animator) {
|
||||
// We already know the forward button state - no need to store it here.
|
||||
backButtonWasEnabledOnStartEditing = backButton.isEnabled();
|
||||
|
||||
setButtonEnabled(backButton, false);
|
||||
setButtonEnabled(forwardButton, false);
|
||||
|
||||
super.startEditing(url, animator);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String commitEdit() {
|
||||
stopEditingNewTablet();
|
||||
return super.commitEdit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String cancelEdit() {
|
||||
stopEditingNewTablet();
|
||||
|
||||
setButtonEnabled(backButton, backButtonWasEnabledOnStartEditing);
|
||||
updateForwardButtonState(forwardButtonState);
|
||||
|
||||
return super.cancelEdit();
|
||||
}
|
||||
|
||||
private void stopEditingNewTablet() {
|
||||
// Undo the changes caused by calling setButtonEnabled in startEditing.
|
||||
// Note that this should be called first so the enabled state of the
|
||||
// forward button is set to the proper value.
|
||||
setButtonEnabled(forwardButton, true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.mozilla.gecko.toolbar;
|
|||
|
||||
import java.util.Arrays;
|
||||
|
||||
import org.mozilla.gecko.NewTabletUI;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tab;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
|
@ -103,6 +104,7 @@ abstract class BrowserToolbarTabletBase extends BrowserToolbar {
|
|||
setButtonEnabled(backButton, canDoBack(tab));
|
||||
|
||||
final boolean isForwardEnabled = canDoForward(tab);
|
||||
if (!NewTabletUI.isEnabled(getContext())) {
|
||||
if (forwardButton.isEnabled() != isForwardEnabled) {
|
||||
// Save the state on the forward button so that we can skip animations
|
||||
// when there's nothing to change
|
||||
|
@ -110,6 +112,12 @@ abstract class BrowserToolbarTabletBase extends BrowserToolbar {
|
|||
animateForwardButton(
|
||||
isForwardEnabled ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
|
||||
}
|
||||
} else {
|
||||
// I don't know the implications of changing this code on old tablet
|
||||
// (and no one is going to thoroughly test it) so duplicate the code.
|
||||
animateForwardButton(
|
||||
isForwardEnabled ? ForwardButtonAnimation.SHOW : ForwardButtonAnimation.HIDE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -109,6 +109,7 @@ this.HawkClient.prototype = {
|
|||
code: restResponse.status,
|
||||
errno: restResponse.status
|
||||
};
|
||||
errorObj.toString = function() this.code + ": " + this.message;
|
||||
let retryAfter = restResponse.headers && restResponse.headers["retry-after"];
|
||||
retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter;
|
||||
if (retryAfter) {
|
||||
|
|
|
@ -209,7 +209,12 @@ let MemoryActor = protocol.ActorClass({
|
|||
* An object of the form:
|
||||
*
|
||||
* {
|
||||
* allocations: [<index into "frames" below> ...],
|
||||
* allocations: [<index into "frames" below>, ...],
|
||||
* allocationsTimestamps: [
|
||||
* <timestamp for allocations[0]>,
|
||||
* <timestamp for allocations[1]>,
|
||||
* ...
|
||||
* ],
|
||||
* frames: [
|
||||
* {
|
||||
* line: <line number for this frame>,
|
||||
|
@ -217,7 +222,7 @@ let MemoryActor = protocol.ActorClass({
|
|||
* source: <filename string for this frame>,
|
||||
* functionDisplayName: <this frame's inferred function name function or null>,
|
||||
* parent: <index into "frames">
|
||||
* }
|
||||
* },
|
||||
* ...
|
||||
* ],
|
||||
* counts: [
|
||||
|
@ -228,6 +233,8 @@ let MemoryActor = protocol.ActorClass({
|
|||
* ]
|
||||
* }
|
||||
*
|
||||
* The timestamps' unit is microseconds since the epoch.
|
||||
*
|
||||
* Subsequent `getAllocations` request within the same recording and
|
||||
* tab navigation will always place the same stack frames at the same
|
||||
* indices as previous `getAllocations` requests in the same
|
||||
|
@ -255,10 +262,11 @@ let MemoryActor = protocol.ActorClass({
|
|||
getAllocations: method(expectState("attached", function() {
|
||||
const allocations = this.dbg.memory.drainAllocationsLog()
|
||||
const packet = {
|
||||
allocations: []
|
||||
allocations: [],
|
||||
allocationsTimestamps: []
|
||||
};
|
||||
|
||||
for (let { frame: stack } of allocations) {
|
||||
for (let { frame: stack, timestamp } of allocations) {
|
||||
if (stack && Cu.isDeadWrapper(stack)) {
|
||||
continue;
|
||||
}
|
||||
|
@ -275,6 +283,7 @@ let MemoryActor = protocol.ActorClass({
|
|||
this._countFrame(waived);
|
||||
|
||||
packet.allocations.push(this._framesToIndices.get(waived));
|
||||
packet.allocationsTimestamps.push(timestamp);
|
||||
}
|
||||
|
||||
// Now that we are guaranteed to have a form for every frame, we know the
|
||||
|
|
|
@ -1581,6 +1581,13 @@ Object.defineProperty(BrowserTabActor.prototype, "docShell", {
|
|||
|
||||
Object.defineProperty(BrowserTabActor.prototype, "title", {
|
||||
get: function() {
|
||||
// On Fennec, we can check the session store data for zombie tabs
|
||||
if (this._browser.__SS_restore) {
|
||||
let sessionStore = this._browser.__SS_data;
|
||||
// Get the last selected entry
|
||||
let entry = sessionStore.entries[sessionStore.index - 1];
|
||||
return entry.title;
|
||||
}
|
||||
let title = this.contentDocument.title || this._browser.contentTitle;
|
||||
// If contentTitle is empty (e.g. on a not-yet-restored tab), but there is a
|
||||
// tabbrowser (i.e. desktop Firefox, but not Fennec), we can use the label
|
||||
|
@ -1597,6 +1604,24 @@ Object.defineProperty(BrowserTabActor.prototype, "title", {
|
|||
configurable: false
|
||||
});
|
||||
|
||||
Object.defineProperty(BrowserTabActor.prototype, "url", {
|
||||
get: function() {
|
||||
// On Fennec, we can check the session store data for zombie tabs
|
||||
if (this._browser.__SS_restore) {
|
||||
let sessionStore = this._browser.__SS_data;
|
||||
// Get the last selected entry
|
||||
let entry = sessionStore.entries[sessionStore.index - 1];
|
||||
return entry.url;
|
||||
}
|
||||
if (this.webNavigation.currentURI) {
|
||||
return this.webNavigation.currentURI.spec;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
Object.defineProperty(BrowserTabActor.prototype, "browser", {
|
||||
get: function() {
|
||||
return this._browser;
|
||||
|
|
|
@ -79,6 +79,7 @@ skip-if = buildapp == 'mulet'
|
|||
[test_memory_allocations_02.html]
|
||||
[test_memory_allocations_03.html]
|
||||
[test_memory_allocations_04.html]
|
||||
[test_memory_allocations_05.html]
|
||||
[test_memory_attach_01.html]
|
||||
[test_memory_attach_02.html]
|
||||
[test_memory_census.html]
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<!--
|
||||
Bug 1068144 - Test getting the timestamps for allocations.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Memory monitoring actor test</title>
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
|
||||
</head>
|
||||
<body>
|
||||
<pre id="test">
|
||||
<script src="memory-helpers.js" type="application/javascript;version=1.8"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
Task.spawn(function* () {
|
||||
var { memory, client } = yield startServerAndGetSelectedTabMemory();
|
||||
yield memory.attach();
|
||||
|
||||
var allocs = [];
|
||||
function allocator() {
|
||||
allocs.push(new Object);
|
||||
}
|
||||
|
||||
// Using setTimeout results in wildly varying delays that make it hard to
|
||||
// test our timestamps and results in intermittent failures. Instead, we
|
||||
// actually spin an empty loop for a whole millisecond.
|
||||
function actuallyWaitOneWholeMillisecond() {
|
||||
var start = window.performance.now();
|
||||
while (window.performance.now() - start < 1.000) ;
|
||||
}
|
||||
|
||||
yield memory.startRecordingAllocations();
|
||||
|
||||
allocator();
|
||||
actuallyWaitOneWholeMillisecond();
|
||||
allocator();
|
||||
actuallyWaitOneWholeMillisecond();
|
||||
allocator();
|
||||
|
||||
var response = yield memory.getAllocations();
|
||||
yield memory.stopRecordingAllocations();
|
||||
|
||||
ok(response.allocationsTimestamps, "The response should have timestamps.");
|
||||
is(response.allocationsTimestamps.length, response.allocations.length,
|
||||
"There should be a timestamp for every allocation.");
|
||||
|
||||
var allocatorIndices = response.allocations
|
||||
.map(function (a, idx) {
|
||||
var frame = response.frames[a];
|
||||
if (frame && frame.functionDisplayName === "allocator") {
|
||||
return idx;
|
||||
}
|
||||
})
|
||||
.filter(function (idx) {
|
||||
return idx !== undefined;
|
||||
});
|
||||
|
||||
is(allocatorIndices.length, 3, "Should have our 3 allocations from the `allocator` timeouts.");
|
||||
|
||||
var lastTimestamp;
|
||||
for (var i = 0; i < 3; i++) {
|
||||
var timestamp = response.allocationsTimestamps[allocatorIndices[i]];
|
||||
info("timestamp", timestamp);
|
||||
ok(timestamp, "We should have a timestamp for the `allocator` allocation.");
|
||||
|
||||
if (lastTimestamp) {
|
||||
var delta = timestamp - lastTimestamp;
|
||||
info("delta since last timestamp", delta);
|
||||
ok(delta >= 1000 /* 1 ms */,
|
||||
"The timestamp should be about 1 ms after the last timestamp.");
|
||||
}
|
||||
|
||||
lastTimestamp = timestamp;
|
||||
}
|
||||
|
||||
yield memory.detach();
|
||||
destroyServerAndFinish(client);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</pre>
|
||||
</body>
|
||||
</html>
|