Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-11-03 16:47:18 -05:00
commit 4af93d7500
84 changed files with 2342 additions and 1130 deletions

View File

@ -14,10 +14,11 @@ let DevEdition = {
styleSheetLocation: "chrome://browser/skin/devedition.css",
styleSheet: null,
defaultThemeID: "{972ce4c6-7e08-4474-a285-3208198ce6fd}",
init: function () {
this._updateDevtoolsThemeAttribute();
this._updateStyleSheet();
this._updateStyleSheetFromPrefs();
// Listen for changes to all prefs except for complete themes.
// No need for this since changing a complete theme requires a
@ -25,14 +26,27 @@ let DevEdition = {
Services.prefs.addObserver(this._lwThemePrefName, this, false);
Services.prefs.addObserver(this._prefName, this, false);
Services.prefs.addObserver(this._devtoolsThemePrefName, this, false);
Services.obs.addObserver(this, "lightweight-theme-styling-update", false);
},
observe: function (subject, topic, data) {
if (topic == "lightweight-theme-styling-update") {
let newTheme = JSON.parse(data);
if (!newTheme || newTheme.id === this.defaultThemeID) {
// A lightweight theme has been unapplied, so just re-read prefs.
this._updateStyleSheetFromPrefs();
} else {
// A lightweight theme has been applied, but the pref may not be
// set yet if this happened from customize menu or addons page.
this._toggleStyleSheet(false);
}
}
if (topic == "nsPref:changed") {
if (data == this._devtoolsThemePrefName) {
this._updateDevtoolsThemeAttribute();
} else {
this._updateStyleSheet();
this._updateStyleSheetFromPrefs();
}
}
},
@ -42,11 +56,10 @@ let DevEdition = {
// to change colors based on the selected devtools theme.
document.documentElement.setAttribute("devtoolstheme",
Services.prefs.getCharPref(this._devtoolsThemePrefName));
this._updateStyleSheetFromPrefs();
},
_updateStyleSheet: function() {
// Only try to apply the dev edition theme if it is preffered
// on and there are no other themes applied.
_updateStyleSheetFromPrefs: function() {
let lightweightThemeSelected = false;
try {
lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
@ -57,21 +70,37 @@ let DevEdition = {
defaultThemeSelected = Services.prefs.getCharPref(this._themePrefName) == "classic/1.0";
} catch(e) {}
let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
!lightweightThemeSelected && defaultThemeSelected;
let devtoolsIsDark = false;
try {
devtoolsIsDark = Services.prefs.getCharPref(this._devtoolsThemePrefName) == "dark";
} catch(e) {}
let deveditionThemeEnabled = Services.prefs.getBoolPref(this._prefName) &&
!lightweightThemeSelected && defaultThemeSelected && devtoolsIsDark;
this._toggleStyleSheet(deveditionThemeEnabled);
},
handleEvent: function(e) {
if (e.type === "load") {
this.styleSheet.removeEventListener("load", this);
gBrowser.tabContainer._positionPinnedTabs();
ToolbarIconColor.inferFromText();
}
},
_toggleStyleSheet: function(deveditionThemeEnabled) {
if (deveditionThemeEnabled && !this.styleSheet) {
let styleSheetAttr = `href="${this.styleSheetLocation}" type="text/css"`;
let styleSheet = this.styleSheet = document.createProcessingInstruction(
this.styleSheet = document.createProcessingInstruction(
'xml-stylesheet', styleSheetAttr);
this.styleSheet.addEventListener("load", function onLoad() {
styleSheet.removeEventListener("load", onLoad);
ToolbarIconColor.inferFromText();
});
this.styleSheet.addEventListener("load", this);
document.insertBefore(this.styleSheet, document.documentElement);
} else if (!deveditionThemeEnabled && this.styleSheet) {
this.styleSheet.removeEventListener("load", this);
this.styleSheet.remove();
this.styleSheet = null;
gBrowser.tabContainer._positionPinnedTabs();
ToolbarIconColor.inferFromText();
}
},
@ -80,6 +109,10 @@ let DevEdition = {
Services.prefs.removeObserver(this._lwThemePrefName, this);
Services.prefs.removeObserver(this._prefName, this);
Services.prefs.removeObserver(this._devtoolsThemePrefName, this);
Services.obs.removeObserver(this, "lightweight-theme-styling-update", false);
if (this.styleSheet) {
this.styleSheet.removeEventListener("load", this);
}
this.styleSheet = null;
}
};

View File

@ -108,7 +108,7 @@ support-files =
[browser_aboutHealthReport.js]
skip-if = os == "linux" # Bug 924307
[browser_aboutHome.js]
skip-if = e10s # Bug ?????? - no about:home support yet
skip-if = e10s # Bug 1093153 - no about:home support yet
[browser_aboutSyncProgress.js]
[browser_action_keyword.js]
skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
@ -150,7 +150,7 @@ skip-if = e10s
[browser_bug416661.js]
skip-if = e10s # Bug 691614 - no e10s zoom support yet
[browser_bug417483.js]
skip-if = e10s # Bug ?????? - no about:home support yet
skip-if = e10s # Bug 1093155 - tries to use context menu from browser-chrome and gets in a mess when in e10s mode
[browser_bug419612.js]
skip-if = e10s # Bug 691614 - no e10s zoom support yet
[browser_bug422590.js]
@ -178,7 +178,7 @@ skip-if = toolkit == "cocoa" || e10s # Bug ?????? - not sure why this is timing
[browser_bug462673.js]
skip-if = e10s # Bug 924260 - "Window is closed"
[browser_bug477014.js]
skip-if = e10s # Bug 918634 - swapFrameLoaders not implemented for e10s
skip-if = e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s
[browser_bug479408.js]
skip-if = buildapp == 'mulet' || e10s # Bug 918663 - DOMLinkAdded events don't make their way to chrome
[browser_bug481560.js]
@ -188,7 +188,7 @@ skip-if = e10s
[browser_bug491431.js]
skip-if = buildapp == 'mulet'
[browser_bug495058.js]
skip-if = e10s # Bug 918634 - swapFrameLoaders (and thus replaceTabWithWindow) not implemented for e10s
skip-if = e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al (and thus replaceTabWithWindow) for e10s
[browser_bug517902.js]
skip-if = e10s # Bug 866413 - PageInfo doesn't work in e10s
[browser_bug519216.js]
@ -198,7 +198,7 @@ skip-if = e10s # Bug ?????? - some weird timing issue with progress listeners th
skip-if = e10s # Bug 918663 - DOMLinkAdded events don't make their way to chrome
[browser_bug533232.js]
[browser_bug537013.js]
skip-if = buildapp == 'mulet' || e10s # Bug 918634 - swapFrameLoaders not implemented for e10s (test calls replaceTabWithWindow)
skip-if = buildapp == 'mulet' || e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls replaceTabWithWindow)
[browser_bug537474.js]
skip-if = e10s # Bug ?????? - test doesn't wait for document to be created before it checks it
[browser_bug550565.js]
@ -429,13 +429,13 @@ skip-if = e10s # Bug ?????? - timeout after logging "Error: Channel closing: too
[browser_tabDrop.js]
skip-if = buildapp == 'mulet' || e10s
[browser_tabMatchesInAwesomebar_perwindowpb.js]
skip-if = e10s # Bug 918634 - swapFrameLoaders not implemented for e10s (test uses gBrowser.swapBrowsersAndCloseOther)
skip-if = e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls gBrowser.swapBrowsersAndCloseOther)
[browser_tab_drag_drop_perwindow.js]
skip-if = buildapp == 'mulet'
[browser_tab_dragdrop.js]
skip-if = buildapp == 'mulet' || e10s # Bug 918634 - swapFrameLoaders not implemented for e10s (test uses gBrowser.swapBrowsersAndCloseOther)
skip-if = buildapp == 'mulet' || e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls gBrowser.swapBrowsersAndCloseOther)
[browser_tab_dragdrop2.js]
skip-if = buildapp == 'mulet' || e10s
skip-if = buildapp == 'mulet' || e10s # Bug 1093206 - need to re-enable tests relying on swapFrameLoaders et al for e10s (test calls gBrowser.swapBrowsersAndCloseOther)
[browser_tabbar_big_widgets.js]
skip-if = os == "linux" || os == "mac" # No tabs in titlebar on linux
# Disabled on OS X because of bug 967917

View File

@ -6,14 +6,12 @@
*/
const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled";
const PREF_THEME = "general.skins.selectedSkin";
const PREF_LWTHEME = "lightweightThemes.isThemeSelected";
const PREF_DEVTOOLS_THEME = "devtools.theme";
registerCleanupFunction(() => {
// Set preferences back to their original values
Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
Services.prefs.clearUserPref(PREF_THEME);
Services.prefs.clearUserPref(PREF_LWTHEME);
Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
});
@ -24,6 +22,8 @@ function test() {
}
function startTests() {
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
info ("Setting browser.devedition.theme.enabled to false.");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
ok (!DevEdition.styleSheet, "There is no devedition style sheet when the pref is false.");
@ -40,30 +40,83 @@ function startTests() {
Services.prefs.setBoolPref(PREF_LWTHEME, false);
ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
// There are no listeners for the complete theme pref since applying the theme
// requires a restart.
info ("Setting general.skins.selectedSkin to a custom string.");
Services.prefs.setCharPref(PREF_THEME, "custom-theme");
ok (DevEdition.styleSheet, "The devedition stylesheet is still here when a complete theme is added.");
info ("Resetting general.skins.selectedSkin to default value.");
Services.prefs.clearUserPref(PREF_THEME);
ok (DevEdition.styleSheet, "The devedition stylesheet is still here when a complete theme is removed.");
info ("Setting browser.devedition.theme.enabled to false.");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed.");
info ("Checking :root attributes based on devtools theme.");
testDevtoolsTheme();
testLightweightThemePreview();
finish();
}
function testDevtoolsTheme() {
info ("Checking that Australis is shown when the light devtools theme is applied.");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
ok (DevEdition.styleSheet, "The devedition stylesheet exists.");
info ("Checking stylesheet and :root attributes based on devtools theme.");
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
is (document.documentElement.getAttribute("devtoolstheme"), "light",
"The documentElement has an attribute based on devtools theme.");
ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed because of light devtools theme.");
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
is (document.documentElement.getAttribute("devtoolstheme"), "dark",
"The documentElement has an attribute based on devtools theme.");
ok (DevEdition.styleSheet, "The devedition stylesheet has been readded because of dark devtools theme.");
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "light");
is (document.documentElement.getAttribute("devtoolstheme"), "light",
"The documentElement has an attribute based on devtools theme.");
ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed because of light devtools theme.");
finish();
Services.prefs.setCharPref(PREF_DEVTOOLS_THEME, "dark");
is (document.documentElement.getAttribute("devtoolstheme"), "dark",
"The documentElement has an attribute based on devtools theme.");
ok (DevEdition.styleSheet, "The devedition stylesheet has been readded because of dark devtools theme.");
}
function dummyLightweightTheme(id) {
return {
id: id,
name: id,
headerURL: "http://lwttest.invalid/a.png",
footerURL: "http://lwttest.invalid/b.png",
textcolor: "red",
accentcolor: "blue"
};
}
function testLightweightThemePreview() {
let {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
info ("Turning the pref on, then previewing lightweight themes");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
LightweightThemeManager.previewTheme(dummyLightweightTheme("preview0"));
ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a lightweight theme preview.");
LightweightThemeManager.resetPreview();
LightweightThemeManager.previewTheme(dummyLightweightTheme("preview1"));
ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a second lightweight theme preview.");
LightweightThemeManager.resetPreview();
ok (DevEdition.styleSheet, "The devedition stylesheet is enabled again after resetting the preview.");
info ("Turning the pref on, then previewing a theme, turning it off and resetting the preview");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
LightweightThemeManager.previewTheme(dummyLightweightTheme("preview2"));
ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after a lightweight theme preview.");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, false);
ok (!DevEdition.styleSheet, "The devedition stylesheet is not enabled after pref is turned off.");
LightweightThemeManager.resetPreview();
ok (!DevEdition.styleSheet, "The devedition stylesheet is still disabled after resetting the preview.");
info ("Turning the pref on, then previewing the default theme, turning it off and resetting the preview");
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
LightweightThemeManager.previewTheme(dummyLightweightTheme("{972ce4c6-7e08-4474-a285-3208198ce6fd}"));
ok (DevEdition.styleSheet, "The devedition stylesheet is still enabled after the default theme is applied.");
LightweightThemeManager.resetPreview();
ok (DevEdition.styleSheet, "The devedition stylesheet is still enabled after resetting the preview.");
}

View File

@ -2,5 +2,5 @@
# 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/.
MOZ_APP_DISPLAYNAME=FirefoxDevEdition
MOZ_APP_DISPLAYNAME=FirefoxDeveloperEdition
MOZ_APP_REMOTINGNAME=firefox-dev

View File

@ -2,7 +2,7 @@
- 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/. -->
<!ENTITY brandShortName "FirefoxDevEdition">
<!ENTITY brandShortName "Firefox Developer Edition">
<!ENTITY brandFullName "Firefox Developer Edition">
<!ENTITY vendorShortName "Mozilla">
<!ENTITY trademarkInfo.part1 " ">

View File

@ -2,7 +2,7 @@
# 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/.
brandShortName=FirefoxDevEdition
brandShortName=Firefox Developer Edition
brandFullName=Firefox Developer Edition
vendorShortName=Mozilla

View File

@ -47,7 +47,7 @@ CallProgressSocket.prototype = {
connect: function(onSuccess, onError) {
this._onSuccess = onSuccess;
this._onError = onError ||
(reason => {MozLoopService.logwarn("LoopCalls::callProgessSocket - ", reason);});
(reason => {MozLoopService.log.warn("LoopCalls::callProgessSocket - ", reason);});
if (!onSuccess) {
this._onError("missing onSuccess argument");
@ -126,9 +126,8 @@ CallProgressSocket.prototype = {
let msg = {};
try {
msg = JSON.parse(aMsg);
}
catch (error) {
MozLoopService.logerror("LoopCalls: error parsing progress message - ", error);
} catch (error) {
MozLoopService.log.error("LoopCalls: error parsing progress message - ", error);
return;
}
@ -146,7 +145,7 @@ CallProgressSocket.prototype = {
*/
_send: function(aMsg) {
if (!this._handshakeComplete) {
MozLoopService.logwarn("LoopCalls::_send error - handshake not complete");
MozLoopService.log.warn("LoopCalls::_send error - handshake not complete");
return;
}
@ -179,14 +178,12 @@ CallProgressSocket.prototype = {
* and register with the Loop server.
*/
let LoopCallsInternal = {
callsData: {
inUse: false,
},
mocks: {
webSocket: undefined,
},
conversationInProgress: {},
/**
* Callback from MozLoopPushHandler - A push notification has been received from
* the server.
@ -248,20 +245,19 @@ let LoopCallsInternal = {
let respData = JSON.parse(response.body);
if (respData.calls && Array.isArray(respData.calls)) {
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 {
if ("id" in this.conversationInProgress) {
this._returnBusy(callData);
} else {
callData.sessionType = sessionType;
callData.type = "incoming";
this._startCall(callData);
}
});
} else {
MozLoopService.logwarn("Error: missing calls[] in response");
MozLoopService.log.warn("Error: missing calls[] in response");
}
} catch (err) {
MozLoopService.logwarn("Error parsing calls info", err);
MozLoopService.log.warn("Error parsing calls info", err);
}
},
@ -269,17 +265,11 @@ let LoopCallsInternal = {
* Starts a call, saves the call data, and opens a chat window.
*
* @param {Object} callData The data associated with the call including an id.
* @param {Boolean} conversationType Whether or not the call is "incoming"
* or "outgoing"
* The data should include the type - "incoming" or
* "outgoing".
*/
_startCall: function(callData, conversationType) {
this.callsData.inUse = true;
this.callsData.data = callData;
MozLoopService.openChatWindow(
null,
// No title, let the page set that, to avoid flickering.
"",
"about:loopconversation#" + conversationType + "/" + callData.windowId);
_startCall: function(callData) {
this.conversationInProgress.id = MozLoopService.openChatWindow(callData);
},
/**
@ -290,17 +280,16 @@ let LoopCallsInternal = {
* @return true if the call is opened, false if it is not opened (i.e. busy)
*/
startDirectCall: function(contact, callType) {
if (this.callsData.inUse)
if ("id" in this.conversationInProgress)
return false;
var callData = {
contact: contact,
callType: callType,
// XXX Really we shouldn't be using random numbers, bug 1090209 will fix this.
windowId: Math.floor((Math.random() * 100000000))
type: "outgoing"
};
this._startCall(callData, "outgoing");
this._startCall(callData);
return true;
},
@ -341,21 +330,18 @@ this.LoopCalls = {
},
/**
* Returns the callData for a specific conversation window id.
* Used to signify that a call is in progress.
*
* The data was retrieved from the LoopServer via a GET/calls/<version> request
* triggered by an incoming message from the LoopPushServer.
*
* @param {Number} conversationWindowId
* @return {callData} The callData or undefined if error.
* @param {String} The window id for the call in progress.
*/
getCallData: function(conversationWindowId) {
if (LoopCallsInternal.callsData.data &&
LoopCallsInternal.callsData.data.windowId == conversationWindowId) {
return LoopCallsInternal.callsData.data;
} else {
return undefined;
setCallInProgress: function(conversationWindowId) {
if ("id" in LoopCallsInternal.conversationInProgress &&
LoopCallsInternal.conversationInProgress.id != conversationWindowId) {
MozLoopService.log.error("Starting a new conversation when one is already in progress?");
return;
}
LoopCallsInternal.conversationInProgress.id = conversationWindowId;
},
/**
@ -365,11 +351,10 @@ this.LoopCalls = {
*
* @param {Number} conversationWindowId
*/
releaseCallData: function(conversationWindowId) {
if (LoopCallsInternal.callsData.data &&
LoopCallsInternal.callsData.data.windowId == conversationWindowId) {
LoopCallsInternal.callsData.data = undefined;
LoopCallsInternal.callsData.inUse = false;
clearCallInProgress: function(conversationWindowId) {
if ("id" in LoopCallsInternal.conversationInProgress &&
LoopCallsInternal.conversationInProgress.id == conversationWindowId) {
delete LoopCallsInternal.conversationInProgress.id;
}
},

View File

@ -41,6 +41,61 @@ const extend = function(target, source) {
return target;
};
/**
* Checks whether a participant is already part of a room.
*
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#User_Identification_in_a_Room
*
* @param {Object} room A room object that contains a list of current participants
* @param {Object} participant Participant to check if it's already there
* @returns {Boolean} TRUE when the participant is already a member of the room,
* FALSE when it's not.
*/
const containsParticipant = function(room, participant) {
for (let user of room.participants) {
if (user.roomConnectionId == participant.roomConnectionId) {
return true;
}
}
return false;
};
/**
* Compares the list of participants of the room currently in the cache and an
* updated version of that room. When a new participant is found, the 'joined'
* event is emitted. When a participant is not found in the update, it emits a
* 'left' event.
*
* @param {Object} room A room object to compare the participants list
* against
* @param {Object} updatedRoom A room object that contains the most up-to-date
* list of participants
*/
const checkForParticipantsUpdate = function(room, updatedRoom) {
// Partially fetched rooms don't contain the participants list yet. Skip the
// check for now.
if (!("participants" in room)) {
return;
}
let participant;
// Check for participants that joined.
for (participant of updatedRoom.participants) {
if (!containsParticipant(room, participant)) {
eventEmitter.emit("joined", room.roomToken, participant);
eventEmitter.emit("joined:" + room.roomToken, participant);
}
}
// Check for participants that left.
for (participant of room.participants) {
if (!containsParticipant(updatedRoom, participant)) {
eventEmitter.emit("left", room.roomToken, participant);
eventEmitter.emit("left:" + room.roomToken, participant);
}
}
};
/**
* The Rooms class.
*
@ -85,11 +140,23 @@ let LoopRoomsInternal = {
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) {
// See if we already have this room in our cache.
let orig = this.rooms.get(room.roomToken);
if (orig) {
checkForParticipantsUpdate(orig, room);
}
this.rooms.set(room.roomToken, room);
yield LoopRooms.promise("get", room.roomToken);
// When a version is specified, all the data is already provided by this
// request.
if (version) {
eventEmitter.emit("update", room);
eventEmitter.emit("update" + ":" + room.roomToken, room);
} else {
// Next, request the detailed information for each room. If the request
// fails the room data will not be added to the map.
yield LoopRooms.promise("get", room.roomToken);
}
}
// Set the 'dirty' flag back to FALSE, since the list is as fresh as can be now.
@ -113,25 +180,35 @@ let LoopRoomsInternal = {
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 {
let needsUpdate = !("participants" in room);
if (!gDirty && !needsUpdate) {
// Dirty flag is not set AND the necessary data is available, so we can
// simply return the room.
callback(null, room);
return;
}
let sessionType = MozLoopService.userProfile ? LOOP_SESSION_TYPE.FXA :
LOOP_SESSION_TYPE.GUEST;
MozLoopService.hawkRequest(sessionType, "/rooms/" + encodeURIComponent(roomToken), "GET")
.then(response => {
let data = JSON.parse(response.body);
room.roomToken = roomToken;
checkForParticipantsUpdate(room, data);
extend(room, data);
// Remove the `currSize` for posterity.
if ("currSize" in room) {
delete room.currSize;
}
this.rooms.set(roomToken, room);
let eventName = !needsUpdate ? "update" : "add";
eventEmitter.emit(eventName, room);
eventEmitter.emit(eventName + ":" + roomToken, room);
callback(null, room);
}, err => callback(err)).catch(err => callback(err));
},
/**
@ -166,6 +243,15 @@ let LoopRoomsInternal = {
}, error => callback(error)).catch(error => callback(error));
},
open: function(roomToken) {
let windowData = {
roomToken: roomToken,
type: "room"
};
MozLoopService.openChatWindow(windowData);
},
/**
* Callback used to indicate changes to rooms data on the LoopServer.
*
@ -185,10 +271,13 @@ Object.freeze(LoopRoomsInternal);
* 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.
* - 'add[:{room-id}]': A new room object was successfully added to the data
* store.
* - 'remove[:{room-id}]': A room was successfully removed from the data store.
* - 'update[:{room-id}]': A room object was successfully updated with changed
* properties in the data store.
* - 'joined[:{room-id}]': A participant joined a room.
* - 'left[:{room-id}]': A participant left a room.
*
* See the internal code for the API documentation.
*/
@ -205,6 +294,10 @@ this.LoopRooms = {
return LoopRoomsInternal.create(options, callback);
},
open: function(roomToken) {
return LoopRoomsInternal.open(roomToken);
},
promise: function(method, ...params) {
return new Promise((resolve, reject) => {
this[method](...params, (error, result) => {

View File

@ -104,10 +104,21 @@ const injectObjectAPI = function(api, targetWindow) {
// through the priv => unpriv barrier with `Cu.cloneInto()`.
Object.keys(api).forEach(func => {
injectedAPI[func] = function(...params) {
let callback = params.pop();
api[func](...params, function(...results) {
callback(...[cloneValueInto(r, targetWindow) for (r of results)]);
});
let lastParam = params.pop();
// If the last parameter is a function, assume its a callback
// and wrap it differently.
if (lastParam && typeof lastParam === "function") {
api[func](...params, function(...results) {
lastParam(...[cloneValueInto(r, targetWindow) for (r of results)]);
});
} else {
try {
return cloneValueInto(api[func](...params, lastParam), targetWindow);
} catch (ex) {
return cloneValueInto(ex, targetWindow);
}
}
};
});
@ -135,6 +146,7 @@ function injectLoopAPI(targetWindow) {
let appVersionInfo;
let contactsAPI;
let roomsAPI;
let callsAPI;
let api = {
/**
@ -206,34 +218,20 @@ function injectLoopAPI(targetWindow) {
},
/**
* Returns the callData for a specific conversation window id.
* Returns the window data 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.
* This data will be relevant to the type of window, e.g. rooms or calls.
* See LoopRooms or LoopCalls for more information.
*
* @param {Number} conversationWindowId
* @returns {callData} The callData or undefined if error.
* @param {String} conversationWindowId
* @returns {Object} The window data or null if error.
*/
getCallData: {
getConversationWindowData: {
enumerable: true,
writable: true,
value: function(conversationWindowId) {
return Cu.cloneInto(LoopCalls.getCallData(conversationWindowId), targetWindow);
}
},
/**
* Releases the callData for a specific conversation window id.
*
* The result of this call will be a free call session slot.
*
* @param {Number} conversationWindowId
*/
releaseCallData: {
enumerable: true,
writable: true,
value: function(conversationWindowId) {
LoopCalls.releaseCallData(conversationWindowId);
return Cu.cloneInto(MozLoopService.getConversationWindowData(conversationWindowId),
targetWindow);
}
},
@ -273,6 +271,22 @@ function injectLoopAPI(targetWindow) {
}
},
/**
* Returns the calls API.
*
* @returns {Object} The rooms API object
*/
calls: {
enumerable: true,
get: function() {
if (callsAPI) {
return callsAPI;
}
return callsAPI = injectObjectAPI(LoopCalls, targetWindow);
}
},
/**
* Import a list of (new) contacts from an external data source.
*
@ -670,21 +684,6 @@ function injectLoopAPI(targetWindow) {
return MozLoopService.generateUUID();
}
},
/**
* Starts a direct call to the contact addresses.
*
* @param {Object} contact The contact to call
* @param {String} callType The type of call, e.g. "audio-video" or "audio-only"
* @return true if the call is opened, false if it is not opened (i.e. busy)
*/
startDirectCall: {
enumerable: true,
writable: true,
value: function(contact, callType) {
LoopCalls.startDirectCall(contact, callType);
}
},
};
function onStatusChanged(aSubject, aTopic, aData) {

View File

@ -116,6 +116,8 @@ let gFxAEnabled = true;
let gFxAOAuthClientPromise = null;
let gFxAOAuthClient = null;
let gErrors = new Map();
let gLastWindowId = 0;
let gConversationWindowData = new Map();
/**
* Internal helper methods and state
@ -693,15 +695,20 @@ let MozLoopServiceInternal = {
/**
* Opens the chat window
*
* @param {Object} contentWindow The window to open the chat window in, may
* be null.
* @param {String} title The title of the chat window.
* @param {String} url The page to load in the chat window.
* @param {Object} conversationWindowData The data to be obtained by the
* window when it opens.
* @returns {Number} The id of the window.
*/
openChatWindow: function(contentWindow, title, url) {
openChatWindow: function(conversationWindowData) {
// So I guess the origin is the loop server!?
let origin = this.loopServerUri;
url = url.spec || url;
let windowId = gLastWindowId++;
// Store the id as a string, as that's what we use elsewhere.
windowId = windowId.toString();
gConversationWindowData.set(windowId, conversationWindowData);
let url = "about:loopconversation#" + windowId;
let callback = chatbox => {
// We need to use DOMContentLoaded as otherwise the injection will happen
@ -749,7 +756,8 @@ let MozLoopServiceInternal = {
}.bind(this), true);
};
Chat.open(contentWindow, origin, title, url, undefined, undefined, callback);
Chat.open(null, origin, "", url, undefined, undefined, callback);
return windowId;
},
/**
@ -996,13 +1004,12 @@ this.MozLoopService = {
/**
* Opens the chat window
*
* @param {Object} contentWindow The window to open the chat window in, may
* be null.
* @param {String} title The title of the chat window.
* @param {String} url The page to load in the chat window.
* @param {Object} conversationWindowData The data to be obtained by the
* window when it opens.
* @returns {Number} The id of the window.
*/
openChatWindow: function(contentWindow, title, url) {
MozLoopServiceInternal.openChatWindow(contentWindow, title, url);
openChatWindow: function(conversationWindowData) {
return MozLoopServiceInternal.openChatWindow(conversationWindowData);
},
/**
@ -1416,4 +1423,24 @@ this.MozLoopService = {
return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj).catch(
error => {MozLoopServiceInternal._hawkRequestError(error);});
},
/**
* Returns the window data for a specific conversation window id.
*
* This data will be relevant to the type of window, e.g. rooms or calls.
* See LoopRooms or LoopCalls for more information.
*
* @param {String} conversationWindowId
* @returns {Object} The window data or null if error.
*/
getConversationWindowData: function(conversationWindowId) {
if (gConversationWindowData.has(conversationWindowId)) {
var conversationData = gConversationWindowData.get(conversationWindowId);
gConversationWindowData.delete(conversationWindowId);
return conversationData;
}
log.error("Window data was already fetched before. Possible race condition!");
return null;
}
};

View File

@ -38,6 +38,7 @@
<script type="text/javascript" src="loop/shared/js/localRoomStore.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/js/roomViews.js"></script>

View File

@ -392,12 +392,12 @@ loop.contacts = (function(_, mozL10n) {
break;
case "video-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
}
break;
case "audio-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
}
break;
default:

View File

@ -392,12 +392,12 @@ loop.contacts = (function(_, mozL10n) {
break;
case "video-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_VIDEO);
}
break;
case "audio-call":
if (!contact.blocked) {
navigator.mozLoop.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
navigator.mozLoop.calls.startDirectCall(contact, CALL_TYPES.AUDIO_ONLY);
}
break;
default:

View File

@ -179,12 +179,12 @@ loop.conversation = (function(mozL10n) {
});
/**
* Incoming Call failed view. Displayed when a call fails.
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var IncomingCallFailedView = React.createClass({displayName: 'IncomingCallFailedView',
var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
@ -218,7 +218,9 @@ loop.conversation = (function(mozL10n) {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired
},
getInitialState: function() {
@ -283,7 +285,7 @@ loop.conversation = (function(mozL10n) {
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return IncomingCallFailedView({
return GenericFailureView({
cancelCall: this.closeWindow.bind(this)}
)
}
@ -352,13 +354,9 @@ loop.conversation = (function(mozL10n) {
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
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.
console.error("Failed to get the call data");
return;
}
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
@ -374,7 +372,8 @@ loop.conversation = (function(mozL10n) {
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
@ -475,7 +474,8 @@ loop.conversation = (function(mozL10n) {
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
navigator.mozLoop.calls.clearCallInProgress(
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.
@ -523,6 +523,8 @@ loop.conversation = (function(mozL10n) {
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({displayName: 'AppControllerView',
mixins: [Backbone.Events],
propTypes: {
// XXX Old types required for incoming call view.
client: React.PropTypes.instanceOf(loop.Client).isRequired,
@ -530,51 +532,66 @@ loop.conversation = (function(mozL10n) {
.isRequired,
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
// XXX New types for flux style
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// if not passed, this is not a room view
localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
},
getInitialState: function() {
return this.props.store.attributes;
return this.props.conversationAppStore.getStoreState();
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
this.listenTo(this.props.conversationAppStore, "change", function() {
this.setState(this.props.conversationAppStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.stopListening(this.props.conversationAppStore);
},
closeWindow: function() {
window.close();
},
render: function() {
if (this.props.localRoomStore) {
return (
EmptyRoomView({
switch(this.state.windowType) {
case "incoming": {
return (IncomingConversationView({
client: this.props.client,
conversation: this.props.conversation,
sdk: this.props.sdk,
conversationAppStore: this.props.conversationAppStore}
));
}
case "outgoing": {
return (OutgoingConversationView({
store: this.props.conversationStore,
dispatcher: this.props.dispatcher}
));
}
case "room": {
return (EmptyRoomView({
mozLoop: navigator.mozLoop,
localRoomStore: this.props.localRoomStore}
)
);
));
}
case "failed": {
return (GenericFailureView({
cancelCall: this.closeWindow}
));
}
default: {
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
return null;
}
}
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (OutgoingConversationView({
store: this.props.store,
dispatcher: this.props.dispatcher}
));
}
return (IncomingConversationView({
client: this.props.client,
conversation: this.props.conversation,
sdk: this.props.sdk}
));
}
});
@ -605,11 +622,20 @@ loop.conversation = (function(mozL10n) {
sdk: OT
});
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
var localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});;
// XXX Old class creation for the incoming conversation view, whilst
// we transition across (bug 1072323).
@ -622,57 +648,31 @@ loop.conversation = (function(mozL10n) {
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationData().hash;
var windowId;
var outgoing;
var localRoomStore;
// XXX removeMe, along with noisy comment at the beginning of
// conversation_test.js "when locationHash begins with #room".
if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
locationHash = "#room/32";
}
var hash = locationHash.match(/#incoming\/(.*)/);
var hash = locationHash.match(/#(.*)/);
if (hash) {
windowId = hash[1];
outgoing = false;
} else if (hash = locationHash.match(/#room\/(.*)/)) {
localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
} else {
hash = locationHash.match(/#outgoing\/(.*)/);
if (hash) {
windowId = hash[1];
outgoing = true;
}
}
conversation.set({windowId: windowId});
window.addEventListener("unload", function(event) {
// Handle direct close of dialog box via [x] control.
navigator.mozLoop.releaseCallData(windowId);
navigator.mozLoop.calls.clearCallInProgress(windowId);
});
React.renderComponent(AppControllerView({
conversationAppStore: conversationAppStore,
localRoomStore: localRoomStore,
store: conversationStore,
conversationStore: conversationStore,
client: client,
conversation: conversation,
dispatcher: dispatcher,
sdk: window.OT}
), document.querySelector('#main'));
if (localRoomStore) {
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
return;
}
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
windowId: windowId,
outgoing: outgoing
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId
}));
}
@ -680,7 +680,7 @@ loop.conversation = (function(mozL10n) {
AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
IncomingCallFailedView: IncomingCallFailedView,
GenericFailureView: GenericFailureView,
init: init
};
})(document.mozL10n);

View File

@ -179,12 +179,12 @@ loop.conversation = (function(mozL10n) {
});
/**
* Incoming Call failed view. Displayed when a call fails.
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var IncomingCallFailedView = React.createClass({
var GenericFailureView = React.createClass({
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
@ -218,7 +218,9 @@ loop.conversation = (function(mozL10n) {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired
},
getInitialState: function() {
@ -283,7 +285,7 @@ loop.conversation = (function(mozL10n) {
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return <IncomingCallFailedView
return <GenericFailureView
cancelCall={this.closeWindow.bind(this)}
/>
}
@ -352,13 +354,9 @@ loop.conversation = (function(mozL10n) {
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
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.
console.error("Failed to get the call data");
return;
}
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
@ -374,7 +372,8 @@ loop.conversation = (function(mozL10n) {
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
@ -475,7 +474,8 @@ loop.conversation = (function(mozL10n) {
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.releaseCallData(this.props.conversation.get("windowId"));
navigator.mozLoop.calls.clearCallInProgress(
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.
@ -523,6 +523,8 @@ loop.conversation = (function(mozL10n) {
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
// XXX Old types required for incoming call view.
client: React.PropTypes.instanceOf(loop.Client).isRequired,
@ -530,51 +532,66 @@ loop.conversation = (function(mozL10n) {
.isRequired,
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
// XXX New types for flux style
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
.isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// if not passed, this is not a room view
localRoomStore: React.PropTypes.instanceOf(loop.store.LocalRoomStore)
},
getInitialState: function() {
return this.props.store.attributes;
return this.props.conversationAppStore.getStoreState();
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
this.listenTo(this.props.conversationAppStore, "change", function() {
this.setState(this.props.conversationAppStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.stopListening(this.props.conversationAppStore);
},
closeWindow: function() {
window.close();
},
render: function() {
if (this.props.localRoomStore) {
return (
<EmptyRoomView
switch(this.state.windowType) {
case "incoming": {
return (<IncomingConversationView
client={this.props.client}
conversation={this.props.conversation}
sdk={this.props.sdk}
conversationAppStore={this.props.conversationAppStore}
/>);
}
case "outgoing": {
return (<OutgoingConversationView
store={this.props.conversationStore}
dispatcher={this.props.dispatcher}
/>);
}
case "room": {
return (<EmptyRoomView
mozLoop={navigator.mozLoop}
localRoomStore={this.props.localRoomStore}
/>
);
/>);
}
case "failed": {
return (<GenericFailureView
cancelCall={this.closeWindow}
/>);
}
default: {
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
return null;
}
}
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (<OutgoingConversationView
store={this.props.store}
dispatcher={this.props.dispatcher}
/>);
}
return (<IncomingConversationView
client={this.props.client}
conversation={this.props.conversation}
sdk={this.props.sdk}
/>);
}
});
@ -605,11 +622,20 @@ loop.conversation = (function(mozL10n) {
sdk: OT
});
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher,
sdkDriver: sdkDriver
});
var localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});;
// XXX Old class creation for the incoming conversation view, whilst
// we transition across (bug 1072323).
@ -622,57 +648,31 @@ loop.conversation = (function(mozL10n) {
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationData().hash;
var windowId;
var outgoing;
var localRoomStore;
// XXX removeMe, along with noisy comment at the beginning of
// conversation_test.js "when locationHash begins with #room".
if (navigator.mozLoop.getLoopBoolPref("test.alwaysUseRooms")) {
locationHash = "#room/32";
}
var hash = locationHash.match(/#incoming\/(.*)/);
var hash = locationHash.match(/#(.*)/);
if (hash) {
windowId = hash[1];
outgoing = false;
} else if (hash = locationHash.match(/#room\/(.*)/)) {
localRoomStore = new loop.store.LocalRoomStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
} else {
hash = locationHash.match(/#outgoing\/(.*)/);
if (hash) {
windowId = hash[1];
outgoing = true;
}
}
conversation.set({windowId: windowId});
window.addEventListener("unload", function(event) {
// Handle direct close of dialog box via [x] control.
navigator.mozLoop.releaseCallData(windowId);
navigator.mozLoop.calls.clearCallInProgress(windowId);
});
React.renderComponent(<AppControllerView
conversationAppStore={conversationAppStore}
localRoomStore={localRoomStore}
store={conversationStore}
conversationStore={conversationStore}
client={client}
conversation={conversation}
dispatcher={dispatcher}
sdk={window.OT}
/>, document.querySelector('#main'));
if (localRoomStore) {
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: hash[1]}));
return;
}
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
windowId: windowId,
outgoing: outgoing
dispatcher.dispatch(new sharedActions.GetWindowData({
windowId: windowId
}));
}
@ -680,7 +680,7 @@ loop.conversation = (function(mozL10n) {
AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
IncomingCallFailedView: IncomingCallFailedView,
GenericFailureView: GenericFailureView,
init: init
};
})(document.mozL10n);

View File

@ -0,0 +1,86 @@
/* 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/. */
/* global loop:true */
var loop = loop || {};
loop.store = loop.store || {};
/**
* Manages the conversation window app controller view. Used to get
* the window data and store the window type.
*/
loop.store.ConversationAppStore = (function() {
/**
* Constructor
*
* @param {Object} options Options for the store. Should contain the dispatcher.
*/
var ConversationAppStore = function(options) {
if (!options.dispatcher) {
throw new Error("Missing option dispatcher");
}
if (!options.mozLoop) {
throw new Error("Missing option mozLoop");
}
this._dispatcher = options.dispatcher;
this._mozLoop = options.mozLoop;
this._storeState = {};
this._dispatcher.register(this, [
"getWindowData"
]);
};
ConversationAppStore.prototype = _.extend({
/**
* Retrieves current store state.
*
* @return {Object}
*/
getStoreState: function() {
return this._storeState;
},
/**
* Updates store states and trigger a "change" event.
*
* @param {Object} state The new store state.
*/
setStoreState: function(state) {
this._storeState = state;
this.trigger("change");
},
/**
* Handles the get window data action - obtains the window data,
* updates the store and notifies interested components.
*
* @param {sharedActions.GetWindowData} actionData The action data
*/
getWindowData: function(actionData) {
var windowData = this._mozLoop.getConversationWindowData(actionData.windowId);
if (!windowData) {
console.error("Failed to get the window data");
this.setStoreState({windowType: "failed"});
return;
}
// XXX windowData is a hack for the IncomingConversationView until
// we rework it for the flux model in bug 1088672.
this.setStoreState({
windowType: windowData.type,
windowData: windowData
});
this._dispatcher.dispatch(new loop.shared.actions.SetupWindowData(_.extend({
windowId: actionData.windowId}, windowData)));
}
}, Backbone.Events);
return ConversationAppStore;
})();

View File

@ -499,6 +499,10 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.FINISHED: {
return this._renderFeedbackView();
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return (PendingConversationView({
dispatcher: this.props.dispatcher,

View File

@ -499,6 +499,10 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.FINISHED: {
return this._renderFeedbackView();
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return (<PendingConversationView
dispatcher={this.props.dispatcher}

View File

@ -582,7 +582,9 @@ loop.panel = (function(_, mozL10n) {
},
openRoom: function(room) {
// XXX implement me; see bug 1074678
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
roomToken: room.roomToken
}));
},
render: function() {

View File

@ -582,7 +582,9 @@ loop.panel = (function(_, mozL10n) {
},
openRoom: function(room) {
// XXX implement me; see bug 1074678
this.props.dispatcher.dispatch(new sharedActions.OpenRoom({
roomToken: room.roomToken
}));
},
render: function() {

View File

@ -30,6 +30,27 @@ loop.shared.actions = (function() {
};
return {
/**
* Get the window data for the provided window id
*/
GetWindowData: Action.define("getWindowData", {
windowId: String
}),
/**
* Used to pass round the window data so that stores can
* record the appropriate data.
*/
SetupWindowData: Action.define("setupWindowData", {
windowId: String,
type: String
// Optional Items. There are other optional items typically sent
// around with this action. They are for the setup of calls and rooms and
// depend on the type. See LoopCalls and LoopRooms for the details of this
// data.
}),
/**
* Fetch a new call url from the server, intended to be sent over email when
* a contact can't be reached.
@ -37,15 +58,6 @@ loop.shared.actions = (function() {
FetchEmailLink: Action.define("fetchEmailLink", {
}),
/**
* Used to trigger gathering of initial call data.
*/
GatherCallData: Action.define("gatherCallData", {
// Specify the callId for an incoming call.
windowId: [String, null],
outgoing: Boolean
}),
/**
* Used to cancel call setup.
*/
@ -170,13 +182,11 @@ loop.shared.actions = (function() {
}),
/**
* Primes localRoomStore with roomLocalId, which triggers the EmptyRoomView
* to do any necessary setup.
*
* XXX should move to localRoomActions module
* Opens a room.
* XXX: should move to some roomActions module - refs bug 1079284
*/
SetupEmptyRoom: Action.define("setupEmptyRoom", {
localRoomId: String
OpenRoom: Action.define("openRoom", {
roomToken: String
})
};
})();

View File

@ -121,7 +121,7 @@ loop.store.ConversationStore = (function() {
this.dispatcher.register(this, [
"connectionFailure",
"connectionProgress",
"gatherCallData",
"setupWindowData",
"connectCall",
"hangupCall",
"peerHungupCall",
@ -188,37 +188,23 @@ loop.store.ConversationStore = (function() {
}
},
/**
* Handles the gather call data action, setting the state
* and starting to get the appropriate data for the type of call.
*
* @param {sharedActions.GatherCallData} actionData The action data.
*/
gatherCallData: function(actionData) {
if (!actionData.outgoing) {
// XXX Other types aren't supported yet, but set the state for the
// view selection.
this.set({outgoing: false});
return;
}
var callData = navigator.mozLoop.getCallData(actionData.windowId);
if (!callData) {
console.error("Failed to get the call data");
this.set({callState: CALL_STATES.TERMINATED});
setupWindowData: function(actionData) {
var windowType = actionData.type;
if (windowType !== "outgoing" &&
windowType !== "incoming") {
// Not for this store, don't do anything.
return;
}
this.set({
contact: callData.contact,
outgoing: actionData.outgoing,
contact: actionData.contact,
outgoing: windowType === "outgoing",
windowId: actionData.windowId,
callType: callData.callType,
callState: CALL_STATES.GATHER
callType: actionData.callType,
callState: CALL_STATES.GATHER,
videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY
});
this.set({videoMuted: this.get("callType") === CALL_TYPES.AUDIO_ONLY});
if (this.get("outgoing")) {
this._setupOutgoingCall();
} // XXX Else, other types aren't supported yet.
@ -330,6 +316,8 @@ loop.store.ConversationStore = (function() {
var contactAddresses = [];
var contact = this.get("contact");
navigator.mozLoop.calls.setCallInProgress(this.get("windowId"));
function appendContactValues(property, strip) {
if (contact.hasOwnProperty(property)) {
contact[property].forEach(function(item) {
@ -409,7 +397,7 @@ loop.store.ConversationStore = (function() {
delete this._websocket;
}
navigator.mozLoop.releaseCallData(this.get("windowId"));
navigator.mozLoop.calls.clearCallInProgress(this.get("windowId"));
},
/**

View File

@ -36,7 +36,9 @@ loop.store.LocalRoomStore = (function() {
}
this.mozLoop = options.mozLoop;
this.dispatcher.register(this, ["setupEmptyRoom"]);
this.dispatcher.register(this, [
"setupWindowData"
]);
}
LocalRoomStore.prototype = _.extend({
@ -69,24 +71,8 @@ loop.store.LocalRoomStore = (function() {
},
/**
* Proxy to mozLoop.rooms.getRoomData for setupEmptyRoom action.
*
* XXXremoveMe Can probably be removed when bug 1074664 lands.
*
* @param {sharedActions.setupEmptyRoom} actionData
* @param {Function} cb Callback(error, roomData)
*/
_fetchRoomData: function(actionData, cb) {
if (this.mozLoop.rooms && this.mozLoop.rooms.getRoomData) {
this.mozLoop.rooms.getRoomData(actionData.localRoomId, cb);
} else {
cb(null, {roomName: "Donkeys"});
}
},
/**
* Execute setupEmptyRoom event action from the dispatcher. This primes
* the store with the localRoomId, and calls MozLoop.getRoomData on that
* Execute setupWindowData event action from the dispatcher. This primes
* the store with the roomToken, and calls MozLoop.getRoomData on that
* ID. This will return either a reflection of state on the server, or,
* if the createRoom call hasn't yet returned, it will have at least the
* roomName as specified to the createRoom method.
@ -94,16 +80,22 @@ loop.store.LocalRoomStore = (function() {
* When the room name gets set, that will trigger the view to display
* that name.
*
* @param {sharedActions.setupEmptyRoom} actionData
* @param {sharedActions.SetupWindowData} actionData
*/
setupEmptyRoom: function(actionData) {
this._fetchRoomData(actionData, function(error, roomData) {
this.setStoreState({
error: error,
localRoomId: actionData.localRoomId,
serverData: roomData
});
}.bind(this));
setupWindowData: function(actionData) {
if (actionData.type !== "room") {
// Nothing for us to do here, leave it to other stores.
return;
}
this.mozLoop.rooms.get(actionData.roomToken,
function(error, roomData) {
this.setStoreState({
error: error,
roomToken: actionData.roomToken,
serverData: roomData
});
}.bind(this));
}
}, Backbone.Events);

View File

@ -328,6 +328,15 @@ loop.store = loop.store || {};
rooms: this._processRoomList(actionData.roomList)
});
},
/**
* Opens a room
*
* @param {sharedActions.OpenRoom} actionData The action data.
*/
openRoom: function(actionData) {
this._mozLoop.rooms.open(actionData.roomToken);
}
}, Backbone.Events);
loop.store.RoomListStore = RoomListStore;

View File

@ -13,6 +13,7 @@ browser.jar:
# Desktop script
content/browser/loop/js/client.js (content/js/client.js)
content/browser/loop/js/conversation.js (content/js/conversation.js)
content/browser/loop/js/conversationAppStore.js (content/js/conversationAppStore.js)
content/browser/loop/js/otconfig.js (content/js/otconfig.js)
content/browser/loop/js/panel.js (content/js/panel.js)
content/browser/loop/js/contacts.js (content/js/contacts.js)

View File

@ -0,0 +1,85 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.store.ConversationAppStore", function () {
var sharedActions = loop.shared.actions;
var sandbox, dispatcher;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should throw an error if the dispatcher is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({mozLoop: {}});
}).to.Throw(/dispatcher/);
});
it("should throw an error if mozLoop is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({dispatcher: dispatcher});
}).to.Throw(/mozLoop/);
});
});
describe("#getWindowData", function() {
var fakeWindowData, fakeGetWindowData, fakeMozLoop, store;
beforeEach(function() {
fakeWindowData = {
type: "incoming",
callId: "123456"
};
fakeGetWindowData = {
windowId: "42"
};
fakeMozLoop = {
getConversationWindowData: function(windowId) {
if (windowId === "42") {
return fakeWindowData;
}
return null;
}
};
store = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: fakeMozLoop
});
});
it("should fetch the window type from the mozLoop API", function() {
dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
expect(store.getStoreState()).eql({
windowType: "incoming",
windowData: fakeWindowData
});
});
it("should dispatch a SetupWindowData action with the data from the mozLoop API",
function() {
sandbox.stub(dispatcher, "dispatch");
store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupWindowData(_.extend({
windowId: fakeGetWindowData.windowId
}, fakeWindowData)));
});
});
});

View File

@ -438,10 +438,10 @@ describe("loop.conversationViews", function () {
loop.conversationViews.CallFailedView);
});
it("should render the PendingConversationView when the call state is 'init'",
it("should render the PendingConversationView when the call state is 'gather'",
function() {
store.set({
callState: CALL_STATES.INIT,
callState: CALL_STATES.GATHER,
contact: contact
});
@ -474,7 +474,7 @@ describe("loop.conversationViews", function () {
it("should update the rendered views when the state is changed.",
function() {
store.set({
callState: CALL_STATES.INIT,
callState: CALL_STATES.GATHER,
contact: contact
});

View File

@ -41,8 +41,9 @@ describe("loop.conversation", function() {
setLoopCharPref: sinon.stub(),
getLoopCharPref: sinon.stub().returns("http://fakeurl"),
getLoopBoolPref: sinon.stub(),
getCallData: sinon.stub(),
releaseCallData: sinon.stub(),
calls: {
clearCallInProgress: sinon.stub()
},
startAlerting: sinon.stub(),
stopAlerting: sinon.stub(),
ensureRegistered: sinon.stub(),
@ -80,7 +81,7 @@ describe("loop.conversation", function() {
sandbox.stub(loop.shared.utils.Helper.prototype,
"locationData").returns({
hash: "#incoming/42",
hash: "#42",
pathname: "/"
});
@ -112,86 +113,30 @@ describe("loop.conversation", function() {
}));
});
describe("when locationHash begins with #room", function () {
// XXX must stay in sync with "test.alwaysUseRooms" pref check
// in conversation.jsx:init until we remove that code, which should
// happen in the second patch in bug 1074686, at which time this comment
// can go away as well.
var fakeRoomID = "32";
beforeEach(function() {
loop.shared.utils.Helper.prototype.locationData
.returns({
hash: "#room/" + fakeRoomID,
pathname: ""
});
sandbox.stub(loop.store, "LocalRoomStore");
});
it("should create a localRoomStore", function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.store.LocalRoomStore);
sinon.assert.calledWithNew(loop.store.LocalRoomStore);
sinon.assert.calledWithExactly(loop.store.LocalRoomStore,
sinon.match({
dispatcher: sinon.match.instanceOf(loop.Dispatcher),
mozLoop: sinon.match.same(navigator.mozLoop)
}));
});
it("should dispatch SetupEmptyRoom with localRoomId from locationHash",
function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.SetupEmptyRoom({localRoomId: fakeRoomID}));
});
});
it("should trigger a gatherCallData action", function() {
it("should trigger a getWindowData action", function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GatherCallData({
windowId: "42",
outgoing: false
new loop.shared.actions.GetWindowData({
windowId: "42"
}));
});
it("should trigger an outgoing gatherCallData action for outgoing calls",
function() {
loop.shared.utils.Helper.prototype.locationData.returns({
hash: "#outgoing/24",
pathname: "/"
});
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GatherCallData({
windowId: "24",
outgoing: true
}));
});
});
describe("ConversationControllerView", function() {
var store, conversation, client, ccView, oldTitle, dispatcher;
describe("AppControllerView", function() {
var conversationStore, conversation, client, ccView, oldTitle, dispatcher;
var conversationAppStore, localRoomStore;
function mountTestComponent(localRoomStore) {
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversation.AppControllerView({
client: client,
conversation: conversation,
localRoomStore: localRoomStore,
sdk: {},
store: store
conversationStore: conversationStore,
conversationAppStore: conversationAppStore
}));
}
@ -202,7 +147,7 @@ describe("loop.conversation", function() {
sdk: {}
});
dispatcher = new loop.Dispatcher();
store = new loop.store.ConversationStore({
conversationStore = new loop.store.ConversationStore({
contact: {
name: [ "Mr Smith" ],
email: [{
@ -216,6 +161,14 @@ describe("loop.conversation", function() {
dispatcher: dispatcher,
sdkDriver: {}
});
localRoomStore = new loop.store.LocalRoomStore({
mozLoop: navigator.mozLoop,
dispatcher: dispatcher
});
conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
});
afterEach(function() {
@ -224,7 +177,7 @@ describe("loop.conversation", function() {
});
it("should display the OutgoingConversationView for outgoing calls", function() {
store.set({outgoing: true});
conversationAppStore.setStoreState({windowType: "outgoing"});
ccView = mountTestComponent();
@ -233,7 +186,14 @@ describe("loop.conversation", function() {
});
it("should display the IncomingConversationView for incoming calls", function() {
store.set({outgoing: false});
sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
return new Promise(function() {});
},
on: sandbox.spy()
});
conversationAppStore.setStoreState({windowType: "incoming"});
ccView = mountTestComponent();
@ -242,31 +202,34 @@ describe("loop.conversation", function() {
});
it("should display the EmptyRoomView for rooms", function() {
navigator.mozLoop.rooms = {
addCallback: function() {},
removeCallback: function() {}
};
var localRoomStore = new loop.store.LocalRoomStore({
mozLoop: navigator.mozLoop,
dispatcher: dispatcher
});
conversationAppStore.setStoreState({windowType: "room"});
ccView = mountTestComponent(localRoomStore);
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.roomViews.EmptyRoomView);
});
it("should display the GenericFailureView for failures", function() {
conversationAppStore.setStoreState({windowType: "failed"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversation.GenericFailureView);
});
});
describe("IncomingConversationView", function() {
var conversation, client, icView, oldTitle;
var conversationAppStore, conversation, client, icView, oldTitle;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversation.IncomingConversationView({
client: client,
conversation: conversation,
sdk: {}
sdk: {},
conversationAppStore: conversationAppStore
}));
}
@ -277,6 +240,11 @@ describe("loop.conversation", function() {
sdk: {}
});
conversation.set({windowId: 42});
var dispatcher = new loop.Dispatcher();
conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
sandbox.stub(conversation, "setOutgoingSessionData");
});
@ -287,13 +255,13 @@ describe("loop.conversation", function() {
describe("start", function() {
it("should set the title to incoming_call_title2", function() {
navigator.mozLoop.getCallData = function() {
return {
conversationAppStore.setStoreState({
windowData: {
progressURL: "fake",
websocketToken: "fake",
callId: 42
};
};
}
});
icView = mountTestComponent();
@ -302,7 +270,8 @@ describe("loop.conversation", function() {
});
describe("componentDidMount", function() {
var fakeSessionData;
var fakeSessionData, promise, resolveWebSocketConnect;
var rejectWebSocketConnect;
beforeEach(function() {
fakeSessionData = {
@ -315,7 +284,10 @@ describe("loop.conversation", function() {
websocketToken: "7b"
};
navigator.mozLoop.getCallData.returns(fakeSessionData);
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
stubComponent(loop.conversation, "IncomingCallView");
stubComponent(sharedView, "ConversationView");
});
@ -326,179 +298,167 @@ describe("loop.conversation", function() {
sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
});
it("should call getCallData on navigator.mozLoop", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(navigator.mozLoop.getCallData);
sinon.assert.calledWith(navigator.mozLoop.getCallData, 42);
});
describe("getCallData successful", function() {
var promise, resolveWebSocketConnect,
rejectWebSocketConnect;
describe("Session Data setup", function() {
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function () {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
return promise;
},
on: sinon.stub()
});
});
it("should store the session data", function() {
sandbox.stub(conversation, "setIncomingSessionData");
icView = mountTestComponent();
sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData);
});
it("should setup the websocket connection", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
websocketToken: "7b"
});
describe("Session Data setup", function() {
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function () {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
return promise;
},
on: sinon.stub()
});
});
describe("WebSocket Handling", function() {
beforeEach(function() {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
it("should store the session data", function() {
sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
icView = mountTestComponent();
sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData);
});
it("should setup the websocket connection", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
websocketToken: "7b"
});
});
});
describe("WebSocket Handling", function() {
beforeEach(function() {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
it("should set the state to incoming on success", function(done) {
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
});
it("should set the state to incoming on success", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("incoming");
promise.then(function () {
expect(icView.state.callStatus).eql("incoming");
done();
});
});
it("should set the state to close on success if the progress " +
"state is terminated", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("incoming");
resolveWebSocketConnect("terminated");
promise.then(function () {
expect(icView.state.callStatus).eql("incoming");
expect(icView.state.callStatus).eql("close");
done();
});
});
it("should set the state to close on success if the progress " +
"state is terminated", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("terminated");
// XXX implement me as part of bug 1047410
// see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
it.skip("should should switch view state to failed", function(done) {
icView = mountTestComponent();
rejectWebSocketConnect();
promise.then(function () {
expect(icView.state.callStatus).eql("close");
promise.then(function() {}, function() {
done();
});
});
});
describe("WebSocket Events", function() {
describe("Call cancelled or timed out before acceptance", function() {
beforeEach(function() {
// Mounting the test component automatically calls the required
// setup functions
icView = mountTestComponent();
promise = new Promise(function(resolve, reject) {
resolve();
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
sandbox.stub(window, "close");
});
describe("progress - terminated (previousState = alerting)", function() {
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "timeout"
}, "alerting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
// XXX implement me as part of bug 1047410
// see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
it.skip("should should switch view state to failed", function(done) {
icView = mountTestComponent();
rejectWebSocketConnect();
it("should close the websocket", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "closed"
}, "alerting");
promise.then(function() {}, function() {
done();
sinon.assert.calledOnce(icView._websocket.close);
done();
});
});
it("should close the window", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "answered-elsewhere"
}, "alerting");
sandbox.clock.tick(1);
sinon.assert.calledOnce(window.close);
done();
});
});
});
});
describe("WebSocket Events", function() {
describe("Call cancelled or timed out before acceptance", function() {
beforeEach(function() {
// Mounting the test component automatically calls the required
// setup functions
icView = mountTestComponent();
promise = new Promise(function(resolve, reject) {
resolve();
describe("progress - terminated (previousState not init" +
" nor alerting)",
function() {
it("should set the state to end", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "media-fail"
}, "connecting");
expect(icView.state.callStatus).eql("end");
done();
});
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
sandbox.stub(window, "close");
});
describe("progress - terminated (previousState = alerting)", function() {
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "timeout"
}, "alerting");
reason: "media-fail"
}, "connecting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
it("should close the websocket", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "closed"
}, "alerting");
sinon.assert.calledOnce(icView._websocket.close);
done();
});
});
it("should close the window", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "answered-elsewhere"
}, "alerting");
sandbox.clock.tick(1);
sinon.assert.calledOnce(window.close);
done();
});
});
});
describe("progress - terminated (previousState not init" +
" nor alerting)",
function() {
it("should set the state to end", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "media-fail"
}, "connecting");
expect(icView.state.callStatus).eql("end");
done();
});
});
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "media-fail"
}, "connecting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
});
});
});
});
@ -572,8 +532,9 @@ describe("loop.conversation", function() {
it("should release callData", function() {
icView.decline();
sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "8699");
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "8699");
});
});
@ -644,13 +605,27 @@ describe("loop.conversation", function() {
var fakeSessionData;
beforeEach(function() {
icView = mountTestComponent();
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
return new Promise(function() {});
},
on: sandbox.spy()
});
icView = mountTestComponent();
conversation.set("loopToken", "fakeToken");
navigator.mozLoop.getLoopCharPref.returns("http://fake");
stubComponent(sharedView, "ConversationView");
@ -700,7 +675,7 @@ describe("loop.conversation", function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(icView,
loop.conversation.IncomingCallFailedView);
loop.conversation.GenericFailureView);
});
it("should update the conversation window toolbar title",

View File

@ -46,6 +46,7 @@
<script src="../../content/shared/js/roomListStore.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/shared/js/localRoomStore.js"></script>
<script src="../../content/js/conversationAppStore.js"></script>
<script src="../../content/js/roomViews.js"></script>
<script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/conversation.js"></script>
@ -53,6 +54,7 @@
<script src="../../content/js/panel.js"></script>
<!-- Test scripts -->
<script src="conversationAppStore_test.js"></script>
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
<script src="panel_test.js"></script>

View File

@ -763,7 +763,7 @@ describe("loop.panel", function() {
var buttonNode = view.getDOMNode().querySelector("button[disabled]");
expect(buttonNode).to.not.equal(null);
});
});
it("should disable the create button when a list retrieval operation is pending",
function() {
@ -774,6 +774,20 @@ describe("loop.panel", function() {
var buttonNode = view.getDOMNode().querySelector("button[disabled]");
expect(buttonNode).to.not.equal(null);
});
describe("#openRoom", function() {
it("should dispatch an OpenRoom action", function() {
var view = createTestComponent();
var dispatch = sandbox.stub(dispatcher, "dispatch");
view.openRoom({roomToken: "42cba"});
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.OpenRoom({
roomToken: "42cba"
}));
});
});
});

View File

@ -79,25 +79,23 @@ const kDanglingContact = {
};
const promiseLoadContacts = function() {
let deferred = Promise.defer();
LoopContacts.removeAll(err => {
if (err) {
deferred.reject(err);
return;
}
gExpectedAdds.push(...kContacts);
LoopContacts.addMany(kContacts, (err, contacts) => {
return new Promise((resolve, reject) => {
LoopContacts.removeAll(err => {
if (err) {
deferred.reject(err);
reject(err);
return;
}
deferred.resolve(contacts);
gExpectedAdds.push(...kContacts);
LoopContacts.addMany(kContacts, (err, contacts) => {
if (err) {
reject(err);
return;
}
resolve(contacts);
});
});
});
return deferred.promise;
};
// Get a copy of a contact without private properties.
@ -162,36 +160,36 @@ add_task(function* () {
}
info("Add a contact.");
let deferred = Promise.defer();
gExpectedAdds.push(kDanglingContact);
LoopContacts.add(kDanglingContact, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kDanglingContact);
info("Check if it's persisted.");
LoopContacts.get(contact._guid, (err, contact) => {
yield new Promise((resolve, reject) => {
gExpectedAdds.push(kDanglingContact);
LoopContacts.add(kDanglingContact, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kDanglingContact);
deferred.resolve();
info("Check if it's persisted.");
LoopContacts.get(contact._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kDanglingContact);
resolve();
});
});
});
yield deferred.promise;
});
add_task(function* () {
info("Test removing all contacts.");
let contacts = yield promiseLoadContacts();
let deferred = Promise.defer();
LoopContacts.removeAll(function(err) {
Assert.ok(!err, "There shouldn't be an error");
LoopContacts.getAll(function(err, found) {
yield new Promise((resolve, reject) => {
LoopContacts.removeAll(function(err) {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(found.length, 0, "There shouldn't be any contacts left");
deferred.resolve();
})
LoopContacts.getAll(function(err, found) {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(found.length, 0, "There shouldn't be any contacts left");
resolve();
})
});
});
yield deferred.promise;
});
// Test retrieving a contact.
@ -199,58 +197,58 @@ add_task(function* () {
let contacts = yield promiseLoadContacts();
info("Get a single contact.");
let deferred = Promise.defer();
LoopContacts.get(contacts[1]._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kContacts[1]);
deferred.resolve();
yield new Promise((resolve, reject) => {
LoopContacts.get(contacts[1]._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kContacts[1]);
resolve();
});
});
yield deferred.promise;
info("Get a single contact by id.");
deferred = Promise.defer();
LoopContacts.getByServiceId(2, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kContacts[1]);
deferred.resolve();
yield new Promise((resolve, reject) => {
LoopContacts.getByServiceId(2, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
compareContacts(contact, kContacts[1]);
resolve();
});
});
yield deferred.promise;
info("Get a couple of contacts.");
deferred = Promise.defer();
let toRetrieve = [contacts[0], contacts[2], contacts[3]];
LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(result.length, toRetrieve.length, "Result list should be the same " +
"size as the list of items to retrieve");
for (let contact of toRetrieve) {
let found = result.filter(c => c._guid == contact._guid);
Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
compareContacts(found[0], contact);
}
deferred.resolve();
yield new Promise((resolve, reject) => {
let toRetrieve = [contacts[0], contacts[2], contacts[3]];
LoopContacts.getMany(toRetrieve.map(contact => contact._guid), (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(result.length, toRetrieve.length, "Result list should be the same " +
"size as the list of items to retrieve");
for (let contact of toRetrieve) {
let found = result.filter(c => c._guid == contact._guid);
Assert.ok(found.length, "Contact " + contact._guid + " should be in the list");
compareContacts(found[0], contact);
}
resolve();
});
});
yield deferred.promise;
info("Get all contacts.");
deferred = Promise.defer();
LoopContacts.getAll((err, contacts) => {
Assert.ok(!err, "There shouldn't be an error");
for (let i = 0, l = contacts.length; i < l; ++i) {
compareContacts(contacts[i], kContacts[i]);
}
deferred.resolve();
yield new Promise((resolve, reject) => {
LoopContacts.getAll((err, contacts) => {
Assert.ok(!err, "There shouldn't be an error");
for (let i = 0, l = contacts.length; i < l; ++i) {
compareContacts(contacts[i], kContacts[i]);
}
resolve();
});
});
yield deferred.promise;
info("Get a non-existent contact.");
deferred = Promise.defer();
LoopContacts.get(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
deferred.resolve();
return new Promise((resolve, reject) => {
LoopContacts.get(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
resolve();
});
});
yield deferred.promise;
});
// Test removing a contact.
@ -258,47 +256,47 @@ add_task(function* () {
let contacts = yield promiseLoadContacts();
info("Remove a single contact.");
let deferred = Promise.defer();
let toRemove = contacts[2]._guid;
gExpectedRemovals.push(toRemove);
LoopContacts.remove(toRemove, err => {
Assert.ok(!err, "There shouldn't be an error");
LoopContacts.get(toRemove, (err, contact) => {
yield new Promise((resolve, reject) => {
let toRemove = contacts[2]._guid;
gExpectedRemovals.push(toRemove);
LoopContacts.remove(toRemove, err => {
Assert.ok(!err, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
deferred.resolve();
LoopContacts.get(toRemove, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
resolve();
});
});
});
yield deferred.promise;
info("Remove a non-existing contact.");
deferred = Promise.defer();
LoopContacts.remove(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.ok(!contact, "There shouldn't be a contact");
deferred.resolve();
});
yield deferred.promise;
info("Remove multiple contacts.");
deferred = Promise.defer();
toRemove = [contacts[0]._guid, contacts[1]._guid];
gExpectedRemovals.push(...toRemove);
LoopContacts.removeMany(toRemove, err => {
Assert.ok(!err, "There shouldn't be an error");
LoopContacts.getAll((err, contacts) => {
yield new Promise((resolve, reject) => {
LoopContacts.remove(1000, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
let ids = contacts.map(contact => contact._guid);
Assert.equal(ids.indexOf(toRemove[0]), -1, "Contact '" + toRemove[0] +
"' shouldn't be there");
Assert.equal(ids.indexOf(toRemove[1]), -1, "Contact '" + toRemove[1] +
"' shouldn't be there");
deferred.resolve();
Assert.ok(!contact, "There shouldn't be a contact");
resolve();
});
});
info("Remove multiple contacts.");
yield new Promise((resolve, reject) => {
let toRemove = [contacts[0]._guid, contacts[1]._guid];
gExpectedRemovals.push(...toRemove);
LoopContacts.removeMany(toRemove, err => {
Assert.ok(!err, "There shouldn't be an error");
LoopContacts.getAll((err, contacts) => {
Assert.ok(!err, "There shouldn't be an error");
let ids = contacts.map(contact => contact._guid);
Assert.equal(ids.indexOf(toRemove[0]), -1, "Contact '" + toRemove[0] +
"' shouldn't be there");
Assert.equal(ids.indexOf(toRemove[1]), -1, "Contact '" + toRemove[1] +
"' shouldn't be there");
resolve();
});
});
});
yield deferred.promise;
});
// Test updating a contact.
@ -308,40 +306,40 @@ add_task(function* () {
const newBday = (new Date(403920000000)).toISOString();
info("Update a single contact.");
let deferred = Promise.defer();
let toUpdate = {
_guid: contacts[2]._guid,
bday: newBday
};
gExpectedUpdates.push(contacts[2]._guid);
LoopContacts.update(toUpdate, (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(result, toUpdate._guid, "Result should be the same as the contact ID");
LoopContacts.get(toUpdate._guid, (err, contact) => {
yield new Promise((resolve, reject) => {
let toUpdate = {
_guid: contacts[2]._guid,
bday: newBday
};
gExpectedUpdates.push(contacts[2]._guid);
LoopContacts.update(toUpdate, (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(contact.bday, newBday, "Birthday should be the same");
info("Check that all other properties were left intact.");
contacts[2].bday = newBday;
compareContacts(contact, contacts[2]);
deferred.resolve();
Assert.equal(result, toUpdate._guid, "Result should be the same as the contact ID");
LoopContacts.get(toUpdate._guid, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(contact.bday, newBday, "Birthday should be the same");
info("Check that all other properties were left intact.");
contacts[2].bday = newBday;
compareContacts(contact, contacts[2]);
resolve();
});
});
});
yield deferred.promise;
info("Update a non-existing contact.");
deferred = Promise.defer();
toUpdate = {
_guid: 1000,
bday: newBday
};
LoopContacts.update(toUpdate, (err, contact) => {
Assert.ok(err, "There should be an error");
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
"Error message should be correct");
deferred.resolve();
yield new Promise((resolve, reject) => {
let toUpdate = {
_guid: 1000,
bday: newBday
};
LoopContacts.update(toUpdate, (err, contact) => {
Assert.ok(err, "There should be an error");
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
"Error message should be correct");
resolve();
});
});
yield deferred.promise;
});
// Test blocking and unblocking a contact.
@ -349,62 +347,62 @@ add_task(function* () {
let contacts = yield promiseLoadContacts();
info("Block contact.");
let deferred = Promise.defer();
let toBlock = contacts[1]._guid;
gExpectedUpdates.push(toBlock);
LoopContacts.block(toBlock, (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(result, toBlock, "Result should be the same as the contact ID");
LoopContacts.get(toBlock, (err, contact) => {
yield new Promise((resolve, reject) => {
let toBlock = contacts[1]._guid;
gExpectedUpdates.push(toBlock);
LoopContacts.block(toBlock, (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
deferred.resolve();
Assert.equal(result, toBlock, "Result should be the same as the contact ID");
LoopContacts.get(toBlock, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, true, "Blocked status should be set");
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
resolve();
});
});
});
yield deferred.promise;
info("Block a non-existing contact.");
deferred = Promise.defer();
LoopContacts.block(1000, err => {
Assert.ok(err, "There should be an error");
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
"Error message should be correct");
deferred.resolve();
});
yield deferred.promise;
info("Unblock a contact.");
deferred = Promise.defer();
let toUnblock = contacts[1]._guid;
gExpectedUpdates.push(toUnblock);
LoopContacts.unblock(toUnblock, (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(result, toUnblock, "Result should be the same as the contact ID");
LoopContacts.get(toUnblock, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
deferred.resolve();
yield new Promise((resolve, reject) => {
LoopContacts.block(1000, err => {
Assert.ok(err, "There should be an error");
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
"Error message should be correct");
resolve();
});
});
info("Unblock a contact.");
yield new Promise((resolve, reject) => {
let toUnblock = contacts[1]._guid;
gExpectedUpdates.push(toUnblock);
LoopContacts.unblock(toUnblock, (err, result) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.equal(result, toUnblock, "Result should be the same as the contact ID");
LoopContacts.get(toUnblock, (err, contact) => {
Assert.ok(!err, "There shouldn't be an error");
Assert.strictEqual(contact.blocked, false, "Blocked status should be set");
info("Check that all other properties were left intact.");
delete contact.blocked;
compareContacts(contact, contacts[1]);
resolve();
});
});
});
yield deferred.promise;
info("Unblock a non-existing contact.");
deferred = Promise.defer();
LoopContacts.unblock(1000, err => {
Assert.ok(err, "There should be an error");
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
"Error message should be correct");
deferred.resolve();
yield new Promise((resolve, reject) => {
LoopContacts.unblock(1000, err => {
Assert.ok(err, "There should be an error");
Assert.equal(err.message, "Contact with _guid '1000' could not be found",
"Error message should be correct");
resolve();
});
});
yield deferred.promise;
});
// Test if the event emitter implementation doesn't leak and is working as expected.

View File

@ -418,21 +418,21 @@ add_task(function* openFxASettings() {
};
yield promiseOAuthParamsSetup(BASE_URL, params);
let deferredTab = Promise.defer();
let progressListener = {
onLocationChange: function onLocationChange(aBrowser) {
gBrowser.removeTabsProgressListener(progressListener);
let contentURI = Services.io.newURI(params.content_uri, null, null);
is(aBrowser.currentURI.spec, Services.io.newURI("/settings", null, contentURI).spec,
"Check settings tab URL");
deferredTab.resolve();
},
};
gBrowser.addTabsProgressListener(progressListener);
yield new Promise((resolve, reject) => {
let progressListener = {
onLocationChange: function onLocationChange(aBrowser) {
gBrowser.removeTabsProgressListener(progressListener);
let contentURI = Services.io.newURI(params.content_uri, null, null);
is(aBrowser.currentURI.spec, Services.io.newURI("/settings", null, contentURI).spec,
"Check settings tab URL");
resolve();
},
};
gBrowser.addTabsProgressListener(progressListener);
MozLoopService.openFxASettings();
MozLoopService.openFxASettings();
});
yield deferredTab.promise;
while (gBrowser.tabs.length > 1) {
gBrowser.removeTab(gBrowser.tabs[1]);
}

View File

@ -92,38 +92,36 @@ add_task(function* token_request_invalid_state() {
// Helper methods
function promiseParams() {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", BASE_URL + "/fxa-oauth/params", true);
xhr.responseType = "json";
xhr.addEventListener("load", () => {
info("/fxa-oauth/params response:\n" + JSON.stringify(xhr.response, null, 4));
deferred.resolve(xhr);
return new Promise((resolve, reject) => {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", BASE_URL + "/fxa-oauth/params", true);
xhr.responseType = "json";
xhr.addEventListener("load", () => {
info("/fxa-oauth/params response:\n" + JSON.stringify(xhr.response, null, 4));
resolve(xhr);
});
xhr.addEventListener("error", reject);
xhr.send();
});
xhr.addEventListener("error", deferred.reject);
xhr.send();
return deferred.promise;
}
function promiseToken(code, state) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", BASE_URL + "/fxa-oauth/token", true);
xhr.setRequestHeader("Authorization", "Hawk ...");
xhr.responseType = "json";
xhr.addEventListener("load", () => {
info("/fxa-oauth/token response:\n" + JSON.stringify(xhr.response, null, 4));
deferred.resolve(xhr);
return new Promise((resolve, reject) => {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", BASE_URL + "/fxa-oauth/token", true);
xhr.setRequestHeader("Authorization", "Hawk ...");
xhr.responseType = "json";
xhr.addEventListener("load", () => {
info("/fxa-oauth/token response:\n" + JSON.stringify(xhr.response, null, 4));
resolve(xhr);
});
xhr.addEventListener("error", reject);
let payload = {
code: code,
state: state,
};
xhr.send(JSON.stringify(payload, null, 4));
});
xhr.addEventListener("error", deferred.reject);
let payload = {
code: code,
state: state,
};
xhr.send(JSON.stringify(payload, null, 4));
return deferred.promise;
}

View File

@ -17,58 +17,57 @@ const WAS_OFFLINE = Services.io.offline;
var gMozLoopAPI;
function promiseGetMozLoopAPI() {
let deferred = Promise.defer();
let loopPanel = document.getElementById("loop-notification-panel");
let btn = document.getElementById("loop-call-button");
return new Promise((resolve, reject) => {
let loopPanel = document.getElementById("loop-notification-panel");
let btn = document.getElementById("loop-call-button");
// Wait for the popup to be shown if it's not already, then we can get the iframe and
// wait for the iframe's load to be completed.
if (loopPanel.state == "closing" || loopPanel.state == "closed") {
loopPanel.addEventListener("popupshown", () => {
loopPanel.removeEventListener("popupshown", onpopupshown, true);
onpopupshown();
}, true);
// Wait for the popup to be shown if it's not already, then we can get the iframe and
// wait for the iframe's load to be completed.
if (loopPanel.state == "closing" || loopPanel.state == "closed") {
loopPanel.addEventListener("popupshown", () => {
loopPanel.removeEventListener("popupshown", onpopupshown, true);
onpopupshown();
}, true);
// Now we're setup, click the button.
btn.click();
} else {
setTimeout(onpopupshown, 0);
}
function onpopupshown() {
let iframe = document.getElementById(btn.getAttribute("notificationFrameId"));
if (iframe.contentDocument &&
iframe.contentDocument.readyState == "complete") {
gMozLoopAPI = iframe.contentWindow.navigator.wrappedJSObject.mozLoop;
deferred.resolve();
// Now we're setup, click the button.
btn.click();
} else {
iframe.addEventListener("load", function panelOnLoad(e) {
iframe.removeEventListener("load", panelOnLoad, true);
setTimeout(onpopupshown, 0);
}
function onpopupshown() {
let iframe = document.getElementById(btn.getAttribute("notificationFrameId"));
if (iframe.contentDocument &&
iframe.contentDocument.readyState == "complete") {
gMozLoopAPI = iframe.contentWindow.navigator.wrappedJSObject.mozLoop;
// We do this in an execute soon to allow any other event listeners to
// be handled, just in case.
deferred.resolve();
}, true);
}
}
resolve();
} else {
iframe.addEventListener("load", function panelOnLoad(e) {
iframe.removeEventListener("load", panelOnLoad, true);
// Remove the iframe after each test. This also avoids mochitest complaining
// about leaks on shutdown as we intentionally hold the iframe open for the
// life of the application.
registerCleanupFunction(function() {
loopPanel.hidePopup();
let frameId = btn.getAttribute("notificationFrameId");
let frame = document.getElementById(frameId);
if (frame) {
loopPanel.removeChild(frame);
gMozLoopAPI = iframe.contentWindow.navigator.wrappedJSObject.mozLoop;
// We do this in an execute soon to allow any other event listeners to
// be handled, just in case.
resolve();
}, true);
}
}
// Remove the iframe after each test. This also avoids mochitest complaining
// about leaks on shutdown as we intentionally hold the iframe open for the
// life of the application.
registerCleanupFunction(function() {
loopPanel.hidePopup();
let frameId = btn.getAttribute("notificationFrameId");
let frame = document.getElementById(frameId);
if (frame) {
loopPanel.removeChild(frame);
}
});
});
return deferred.promise;
}
/**
@ -105,16 +104,15 @@ function loadLoopPanel(aOverrideOptions = {}) {
}
function promiseOAuthParamsSetup(baseURL, params) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", baseURL + "/setup_params", true);
xhr.setRequestHeader("X-Params", JSON.stringify(params));
xhr.addEventListener("load", () => deferred.resolve(xhr));
xhr.addEventListener("error", error => deferred.reject(error));
xhr.send();
return deferred.promise;
return new Promise((resolve, reject) => {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", baseURL + "/setup_params", true);
xhr.setRequestHeader("X-Params", JSON.stringify(params));
xhr.addEventListener("load", () => resolve(xhr));
xhr.addEventListener("error", error => reject(error));
xhr.send();
});
}
function* resetFxA() {
@ -149,41 +147,39 @@ function checkLoggedOutState() {
}
function promiseDeletedOAuthParams(baseURL) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("DELETE", baseURL + "/setup_params", true);
xhr.addEventListener("load", () => deferred.resolve(xhr));
xhr.addEventListener("error", deferred.reject);
xhr.send();
return deferred.promise;
return new Promise((resolve, reject) => {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("DELETE", baseURL + "/setup_params", true);
xhr.addEventListener("load", () => resolve(xhr));
xhr.addEventListener("error", reject);
xhr.send();
});
}
function promiseObserverNotified(aTopic, aExpectedData = null) {
let deferred = Promise.defer();
Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
Services.obs.removeObserver(onNotification, aTopic);
is(aData, aExpectedData, "observer data should match expected data")
deferred.resolve({subject: aSubject, data: aData});
}, aTopic, false);
return deferred.promise;
return new Promise((resolve, reject) => {
Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) {
Services.obs.removeObserver(onNotification, aTopic);
is(aData, aExpectedData, "observer data should match expected data")
resolve({subject: aSubject, data: aData});
}, aTopic, false);
});
}
/**
* Get the last registration on the test server.
*/
function promiseOAuthGetRegistration(baseURL) {
let deferred = Promise.defer();
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", baseURL + "/get_registration", true);
xhr.responseType = "json";
xhr.addEventListener("load", () => deferred.resolve(xhr));
xhr.addEventListener("error", deferred.reject);
xhr.send();
return deferred.promise;
return new Promise((resolve, reject) => {
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", baseURL + "/get_registration", true);
xhr.responseType = "json";
xhr.addEventListener("load", () => resolve(xhr));
xhr.addEventListener("error", reject);
xhr.send();
});
}
function getLoopString(stringID) {

View File

@ -38,7 +38,10 @@ describe("loop.store.ConversationStore", function () {
navigator.mozLoop = {
getLoopBoolPref: sandbox.stub(),
releaseCallData: sandbox.stub()
calls: {
setCallInProgress: sandbox.stub(),
clearCallInProgress: sandbox.stub()
}
};
dispatcher = new loop.Dispatcher();
@ -156,8 +159,9 @@ describe("loop.store.ConversationStore", function () {
dispatcher.dispatch(
new sharedActions.ConnectionFailure({reason: "fake"}));
sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
@ -221,40 +225,29 @@ describe("loop.store.ConversationStore", function () {
});
});
describe("#gatherCallData", function() {
describe("#setupWindowData", function() {
var fakeSetupWindowData;
beforeEach(function() {
store.set({callState: CALL_STATES.INIT});
navigator.mozLoop = {
getCallData: function() {
return {
contact: contact,
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
};
}
fakeSetupWindowData = {
windowId: "123456",
type: "outgoing",
contact: contact,
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO
};
});
afterEach(function() {
delete navigator.mozLoop;
});
it("should set the state to 'gather'", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
windowId: "76543218",
outgoing: true
}));
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("callState")).eql(CALL_STATES.GATHER);
});
it("should save the basic call information", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
windowId: "123456",
outgoing: true
}));
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("windowId")).eql("123456");
expect(store.get("outgoing")).eql(true);
@ -262,28 +255,16 @@ describe("loop.store.ConversationStore", function () {
it("should save the basic information from the mozLoop api", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
windowId: "123456",
outgoing: true
}));
new sharedActions.SetupWindowData(fakeSetupWindowData));
expect(store.get("contact")).eql(contact);
expect(store.get("callType")).eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
describe("outgoing calls", function() {
var outgoingCallData;
beforeEach(function() {
outgoingCallData = {
windowId: "123456",
outgoing: true
};
});
it("should request the outgoing call data", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -291,7 +272,7 @@ describe("loop.store.ConversationStore", function () {
});
it("should include all email addresses in the call data", function() {
contact = {
fakeSetupWindowData.contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
@ -306,7 +287,7 @@ describe("loop.store.ConversationStore", function () {
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -314,7 +295,7 @@ describe("loop.store.ConversationStore", function () {
});
it("should include trim phone numbers for the call data", function() {
contact = {
fakeSetupWindowData.contact = {
name: [ "Mr Smith" ],
tel: [{
type: "home",
@ -324,7 +305,7 @@ describe("loop.store.ConversationStore", function () {
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -332,7 +313,7 @@ describe("loop.store.ConversationStore", function () {
});
it("should include all email and telephone values in the call data", function() {
contact = {
fakeSetupWindowData.contact = {
name: [ "Mr Smith" ],
email: [{
type: "home",
@ -355,7 +336,7 @@ describe("loop.store.ConversationStore", function () {
};
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
@ -375,8 +356,8 @@ describe("loop.store.ConversationStore", function () {
client.setupOutgoingCall.callsArgWith(2, null, callData);
store.gatherCallData(
new sharedActions.GatherCallData(outgoingCallData));
store.setupWindowData(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
@ -389,8 +370,8 @@ describe("loop.store.ConversationStore", function () {
it("should dispatch a connection failure action on failure", function() {
client.setupOutgoingCall.callsArgWith(2, {});
store.gatherCallData(
new sharedActions.GatherCallData(outgoingCallData));
store.setupWindowData(
new sharedActions.SetupWindowData(fakeSetupWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
@ -525,8 +506,9 @@ describe("loop.store.ConversationStore", function () {
it("should release mozLoop callsData", function() {
dispatcher.dispatch(new sharedActions.HangupCall());
sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
@ -565,8 +547,9 @@ describe("loop.store.ConversationStore", function () {
it("should release mozLoop callsData", function() {
dispatcher.dispatch(new sharedActions.PeerHungupCall());
sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});
@ -613,8 +596,9 @@ describe("loop.store.ConversationStore", function () {
it("should release mozLoop callsData", function() {
dispatcher.dispatch(new sharedActions.CancelCall());
sinon.assert.calledOnce(navigator.mozLoop.releaseCallData);
sinon.assert.calledWithExactly(navigator.mozLoop.releaseCallData, "42");
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "42");
});
});

View File

@ -22,31 +22,30 @@ describe("loop.Dispatcher", function () {
it("should register a store against an action name", function() {
var object = { fake: true };
dispatcher.register(object, ["gatherCallData"]);
dispatcher.register(object, ["getWindowData"]);
expect(dispatcher._eventData["gatherCallData"][0]).eql(object);
expect(dispatcher._eventData["getWindowData"][0]).eql(object);
});
it("should register multiple store against an action name", function() {
var object1 = { fake: true };
var object2 = { fake2: true };
dispatcher.register(object1, ["gatherCallData"]);
dispatcher.register(object2, ["gatherCallData"]);
dispatcher.register(object1, ["getWindowData"]);
dispatcher.register(object2, ["getWindowData"]);
expect(dispatcher._eventData["gatherCallData"][0]).eql(object1);
expect(dispatcher._eventData["gatherCallData"][1]).eql(object2);
expect(dispatcher._eventData["getWindowData"][0]).eql(object1);
expect(dispatcher._eventData["getWindowData"][1]).eql(object2);
});
});
describe("#dispatch", function() {
var gatherStore1, gatherStore2, cancelStore1, connectStore1;
var gatherAction, cancelAction, connectAction, resolveCancelStore1;
var getDataStore1, getDataStore2, cancelStore1, connectStore1;
var getDataAction, cancelAction, connectAction, resolveCancelStore1;
beforeEach(function() {
gatherAction = new sharedActions.GatherCallData({
windowId: "42",
outgoing: false
getDataAction = new sharedActions.GetWindowData({
windowId: "42"
});
cancelAction = new sharedActions.CancelCall();
@ -54,11 +53,11 @@ describe("loop.Dispatcher", function () {
sessionData: {}
});
gatherStore1 = {
gatherCallData: sinon.stub()
getDataStore1 = {
getWindowData: sinon.stub()
};
gatherStore2 = {
gatherCallData: sinon.stub()
getDataStore2 = {
getWindowData: sinon.stub()
};
cancelStore1 = {
cancelCall: sinon.stub()
@ -67,8 +66,8 @@ describe("loop.Dispatcher", function () {
connectCall: function() {}
};
dispatcher.register(gatherStore1, ["gatherCallData"]);
dispatcher.register(gatherStore2, ["gatherCallData"]);
dispatcher.register(getDataStore1, ["getWindowData"]);
dispatcher.register(getDataStore2, ["getWindowData"]);
dispatcher.register(cancelStore1, ["cancelCall"]);
dispatcher.register(connectStore1, ["connectCall"]);
});
@ -76,33 +75,33 @@ describe("loop.Dispatcher", function () {
it("should dispatch an action to the required object", function() {
dispatcher.dispatch(cancelAction);
sinon.assert.notCalled(gatherStore1.gatherCallData);
sinon.assert.notCalled(getDataStore1.getWindowData);
sinon.assert.calledOnce(cancelStore1.cancelCall);
sinon.assert.calledWithExactly(cancelStore1.cancelCall, cancelAction);
sinon.assert.notCalled(gatherStore2.gatherCallData);
sinon.assert.notCalled(getDataStore2.getWindowData);
});
it("should dispatch actions to multiple objects", function() {
dispatcher.dispatch(gatherAction);
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledWithExactly(gatherStore1.gatherCallData, gatherAction);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledWithExactly(getDataStore1.getWindowData, getDataAction);
sinon.assert.notCalled(cancelStore1.cancelCall);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
sinon.assert.calledWithExactly(gatherStore2.gatherCallData, gatherAction);
sinon.assert.calledOnce(getDataStore2.getWindowData);
sinon.assert.calledWithExactly(getDataStore2.getWindowData, getDataAction);
});
it("should dispatch multiple actions", function() {
dispatcher.dispatch(cancelAction);
dispatcher.dispatch(gatherAction);
dispatcher.dispatch(getDataAction);
sinon.assert.calledOnce(cancelStore1.cancelCall);
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
});
describe("Queued actions", function() {
@ -110,10 +109,10 @@ describe("loop.Dispatcher", function () {
// Restore the stub, so that we can easily add a function to be
// returned. Unfortunately, sinon doesn't make this easy.
sandbox.stub(connectStore1, "connectCall", function() {
dispatcher.dispatch(gatherAction);
dispatcher.dispatch(getDataAction);
sinon.assert.notCalled(gatherStore1.gatherCallData);
sinon.assert.notCalled(gatherStore2.gatherCallData);
sinon.assert.notCalled(getDataStore1.getWindowData);
sinon.assert.notCalled(getDataStore2.getWindowData);
});
});
@ -132,8 +131,8 @@ describe("loop.Dispatcher", function () {
sinon.assert.calledOnce(connectStore1.connectCall);
// These should be called, because the dispatcher synchronously queues actions.
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
sinon.assert.calledOnce(getDataStore1.getWindowData);
sinon.assert.calledOnce(getDataStore2.getWindowData);
});
});
});

View File

@ -31,20 +31,20 @@ describe("loop.store.LocalRoomStore", function () {
});
});
describe("#setupEmptyRoom", function() {
var store, fakeMozLoop, fakeRoomId, fakeRoomName;
describe("#setupWindowData", function() {
var store, fakeMozLoop, fakeToken, fakeRoomName;
beforeEach(function() {
fakeRoomId = "337-ff-54";
fakeToken = "337-ff-54";
fakeRoomName = "Monkeys";
fakeMozLoop = {
rooms: { getRoomData: sandbox.stub() }
rooms: { get: sandbox.stub() }
};
store = new loop.store.LocalRoomStore(
{mozLoop: fakeMozLoop, dispatcher: dispatcher});
fakeMozLoop.rooms.getRoomData.
withArgs(fakeRoomId).
fakeMozLoop.rooms.get.
withArgs(fakeToken).
callsArgOnWith(1, // index of callback argument
store, // |this| to call it on
null, // args to call the callback with...
@ -57,8 +57,11 @@ describe("loop.store.LocalRoomStore", function () {
done();
});
dispatcher.dispatch(new sharedActions.SetupEmptyRoom(
{localRoomId: fakeRoomId}));
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowId: "42",
type: "room",
roomToken: fakeToken
}));
});
it("should set localRoomId on the store from the action data",
@ -66,13 +69,16 @@ describe("loop.store.LocalRoomStore", function () {
store.once("change", function () {
expect(store.getStoreState()).
to.have.property('localRoomId', fakeRoomId);
to.have.property('roomToken', fakeToken);
done();
});
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
});
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowId: "42",
type: "room",
roomToken: fakeToken
}));
});
it("should set serverData.roomName from the getRoomData callback",
function(done) {
@ -83,16 +89,19 @@ describe("loop.store.LocalRoomStore", function () {
done();
});
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowId: "42",
type: "room",
roomToken: fakeToken
}));
});
it("should set error on the store when getRoomData calls back an error",
function(done) {
var fakeError = new Error("fake error");
fakeMozLoop.rooms.getRoomData.
withArgs(fakeRoomId).
fakeMozLoop.rooms.get.
withArgs(fakeToken).
callsArgOnWith(1, // index of callback argument
store, // |this| to call it on
fakeError); // args to call the callback with...
@ -102,8 +111,11 @@ describe("loop.store.LocalRoomStore", function () {
done();
});
dispatcher.dispatch(
new sharedActions.SetupEmptyRoom({localRoomId: fakeRoomId}));
dispatcher.dispatch(new sharedActions.SetupWindowData({
windowId: "42",
type: "room",
roomToken: fakeToken
}));
});
});

View File

@ -326,4 +326,27 @@ describe("loop.store.RoomListStore", function () {
});
});
});
describe("#openRoom", function() {
var store, fakeMozLoop;
beforeEach(function() {
fakeMozLoop = {
rooms: {
open: sinon.spy()
}
};
store = new loop.store.RoomListStore({
dispatcher: dispatcher,
mozLoop: fakeMozLoop
});
});
it("should open the room via mozLoop", function() {
dispatcher.dispatch(new sharedActions.OpenRoom({roomToken: "42abc"}));
sinon.assert.calledOnce(fakeMozLoop.rooms.open);
sinon.assert.calledWithExactly(fakeMozLoop.rooms.open, "42abc");
});
});
});

View File

@ -4,6 +4,9 @@
Cu.import("resource://services-common/utils.js");
Cu.import("resource:///modules/loop/LoopRooms.jsm");
Cu.import("resource:///modules/Chat.jsm");
let openChatOrig = Chat.open;
const kRooms = new Map([
["_nxD4V4FflQ", {
@ -51,6 +54,38 @@ let roomDetail = {
}]
};
const kRoomUpdates = {
"1": {
participants: []
},
"2": {
participants: [{
displayName: "Alexis",
account: "alexis@example.com",
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
}]
},
"3": {
participants: [{
displayName: "Adam",
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
}]
},
"4": {
participants: [{
displayName: "Adam",
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
}, {
displayName: "Alexis",
account: "alexis@example.com",
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb"
}, {
displayName: "Ruharb",
roomConnectionId: "5de6281c-6568-455f-af08-c0b0a973100e"
}]
}
};
const kCreateRoomProps = {
roomName: "UX Discussion",
expiresIn: 5,
@ -64,6 +99,76 @@ const kCreateRoomData = {
expiresAt: 1405534180
};
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));
};
// LoopRooms emits various events. Test if they work as expected here.
let gExpectedAdds = [];
let gExpectedUpdates = [];
let gExpectedJoins = {};
let gExpectedLeaves = {};
const onRoomAdded = function(e, room) {
let expectedIds = gExpectedAdds.map(room => room.roomToken);
let idx = expectedIds.indexOf(room.roomToken);
Assert.ok(idx > -1, "Added room should be expected");
let expected = gExpectedAdds[idx];
compareRooms(room, expected);
gExpectedAdds.splice(idx, 1);
};
const onRoomUpdated = function(e, room) {
let idx = gExpectedUpdates.indexOf(room.roomToken);
Assert.ok(idx > -1, "Updated room should be expected");
gExpectedUpdates.splice(idx, 1);
};
const onRoomJoined = function(e, roomToken, participant) {
let participants = gExpectedJoins[roomToken];
Assert.ok(participants, "Participant should be expected to join");
let idx = participants.indexOf(participant.roomConnectionId);
Assert.ok(idx > -1, "Participant should be expected to join");
participants.splice(idx, 1);
if (!participants.length) {
delete gExpectedJoins[roomToken];
}
};
const onRoomLeft = function(e, roomToken, participant) {
let participants = gExpectedLeaves[roomToken];
Assert.ok(participants, "Participant should be expected to leave");
let idx = participants.indexOf(participant.roomConnectionId);
Assert.ok(idx > -1, "Participant should be expected to leave");
participants.splice(idx, 1);
if (!participants.length) {
delete gExpectedLeaves[roomToken];
}
};
const parseQueryString = function(qs) {
let map = {};
let parts = qs.split("=");
for (let i = 0, l = parts.length; i < l; ++i) {
if (i % 2 === 1) {
map[parts[i - 1]] = parts[i];
}
}
return map;
};
add_task(function* setup_server() {
loopServer.registerPathHandler("/registration", (req, res) => {
res.setStatusLine(null, 200, "OK");
@ -82,7 +187,14 @@ add_task(function* setup_server() {
res.write(JSON.stringify(kCreateRoomData));
} else {
res.write(JSON.stringify([...kRooms.values()]));
if (req.queryString) {
let qs = parseQueryString(req.queryString);
let room = kRooms.get("_nxD4V4FflQ");
room.participants = kRoomUpdates[qs.version].participants;
res.write(JSON.stringify([room]));
} else {
res.write(JSON.stringify([...kRooms.values()]));
}
}
res.processAsync();
@ -116,27 +228,13 @@ add_task(function* setup_server() {
res.processAsync();
res.finish();
});
yield MozLoopService.promiseRegisteredWithServers();
});
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));
};
// Test if fetching a list of all available rooms works correctly.
add_task(function* test_getAllRooms() {
yield MozLoopService.promiseRegisteredWithServers();
gExpectedAdds.push(...kRooms.values());
let rooms = yield LoopRooms.promise("getAll");
Assert.equal(rooms.length, 3);
for (let room of rooms) {
@ -144,32 +242,97 @@ add_task(function* test_getAllRooms() {
}
});
// Test if fetching a room works correctly.
add_task(function* test_getRoom() {
yield MozLoopService.promiseRegisteredWithServers();
let roomToken = "_nxD4V4FflQ";
let room = yield LoopRooms.promise("get", roomToken);
Assert.deepEqual(room, kRooms.get(roomToken));
});
// Test if fetching a room with incorrect token or return values yields an error.
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");
});
// Test if creating a new room works as expected.
add_task(function* test_createRoom() {
let eventCalled = false;
LoopRooms.once("add", (e, room) => {
compareRooms(room, kCreateRoomProps);
eventCalled = true;
});
gExpectedAdds.push(kCreateRoomProps);
let room = yield LoopRooms.promise("create", kCreateRoomProps);
compareRooms(room, kCreateRoomProps);
Assert.ok(eventCalled, "Event should have fired");
});
// Test if opening a new room window works correctly.
add_task(function* test_openRoom() {
let openedUrl;
Chat.open = function(contentWindow, origin, title, url) {
openedUrl = url;
};
LoopRooms.open("fakeToken");
Assert.ok(openedUrl, "should open a chat window");
// Stop the busy kicking in for following tests.
let windowId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
let windowData = MozLoopService.getConversationWindowData(windowId);
Assert.equal(windowData.type, "room", "window data should contain room as the type");
Assert.equal(windowData.roomToken, "fakeToken", "window data should have the roomToken");
});
// Test if push updates function as expected.
add_task(function* test_roomUpdates() {
gExpectedUpdates.push("_nxD4V4FflQ");
gExpectedLeaves["_nxD4V4FflQ"] = [
"2a1787a6-4a73-43b5-ae3e-906ec1e763cb",
"781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"
];
roomsPushNotification("1");
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedLeaves).length === 0);
gExpectedUpdates.push("_nxD4V4FflQ");
gExpectedJoins["_nxD4V4FflQ"] = ["2a1787a6-4a73-43b5-ae3e-906ec1e763cb"];
roomsPushNotification("2");
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedJoins).length === 0);
gExpectedUpdates.push("_nxD4V4FflQ");
gExpectedJoins["_nxD4V4FflQ"] = ["781f012b-f1ea-4ce1-9105-7cfc36fb4ec7"];
gExpectedLeaves["_nxD4V4FflQ"] = ["2a1787a6-4a73-43b5-ae3e-906ec1e763cb"];
roomsPushNotification("3");
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedLeaves).length === 0);
gExpectedUpdates.push("_nxD4V4FflQ");
gExpectedJoins["_nxD4V4FflQ"] = [
"2a1787a6-4a73-43b5-ae3e-906ec1e763cb",
"5de6281c-6568-455f-af08-c0b0a973100e"];
roomsPushNotification("4");
yield waitForCondition(() => Object.getOwnPropertyNames(gExpectedJoins).length === 0);
});
// Test if the event emitter implementation doesn't leak and is working as expected.
add_task(function* () {
Assert.strictEqual(gExpectedAdds.length, 0, "No room additions should be expected anymore");
Assert.strictEqual(gExpectedUpdates.length, 0, "No room updates should be expected anymore");
});
function run_test() {
setupFakeLoopServer();
LoopRooms.on("add", onRoomAdded);
LoopRooms.on("update", onRoomUpdated);
LoopRooms.on("joined", onRoomJoined);
LoopRooms.on("left", onRoomLeft);
do_register_cleanup(function () {
// Revert original Chat.open implementation
Chat.open = openChatOrig;
LoopRooms.off("add", onRoomAdded);
LoopRooms.off("update", onRoomUpdated);
LoopRooms.off("joined", onRoomJoined);
LoopRooms.off("left", onRoomLeft);
});
run_next_test();
}

View File

@ -26,8 +26,10 @@ add_test(function test_busy_2guest_calls() {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
let windowId;
Chat.open = function(contentWindow, origin, title, url) {
opened++;
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
};
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
@ -35,7 +37,7 @@ add_test(function test_busy_2guest_calls() {
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
LoopCalls.releaseCallData(firstCallId);
LoopCalls.clearCallInProgress(windowId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");
@ -49,8 +51,10 @@ add_test(function test_busy_1fxa_1guest_calls() {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
let windowId;
Chat.open = function(contentWindow, origin, title, url) {
opened++;
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
};
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
@ -59,7 +63,7 @@ add_test(function test_busy_1fxa_1guest_calls() {
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
LoopCalls.releaseCallData(firstCallId);
LoopCalls.clearCallInProgress(windowId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");
@ -73,8 +77,10 @@ add_test(function test_busy_2fxa_calls() {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
let windowId;
Chat.open = function(contentWindow, origin, title, url) {
opened++;
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
};
mockPushHandler.notify(1, MozLoopService.channelIDs.callsFxA);
@ -82,7 +88,7 @@ add_test(function test_busy_2fxa_calls() {
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
LoopCalls.releaseCallData(firstCallId);
LoopCalls.clearCallInProgress(windowId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");
@ -96,8 +102,10 @@ add_test(function test_busy_1guest_1fxa_calls() {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
let windowId;
Chat.open = function(contentWindow, origin, title, url) {
opened++;
windowId = url.match(/about:loopconversation\#(\d+)$/)[1];
};
mockPushHandler.notify(1, MozLoopService.channelIDs.callsGuest);
@ -106,7 +114,7 @@ add_test(function test_busy_1guest_1fxa_calls() {
waitForCondition(() => {return actionReceived && opened > 0}).then(() => {
do_check_true(opened === 1, "should open only one chat window");
do_check_true(actionReceived, "should respond with busy/reject to second call");
LoopCalls.releaseCallData(firstCallId);
LoopCalls.clearCallInProgress(windowId);
run_next_test();
}, () => {
do_throw("should have opened a chat window for first call and rejected second call");

View File

@ -26,11 +26,11 @@ add_task(function test_startDirectCall_opens_window() {
do_check_true(!!openedUrl, "should open a chat window");
// Stop the busy kicking in for following tests.
let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
LoopCalls.releaseCallData(callId);
let windowId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
LoopCalls.clearCallInProgress(windowId);
});
add_task(function test_startDirectCall_getCallData() {
add_task(function test_startDirectCall_getConversationWindowData() {
let openedUrl;
Chat.open = function(contentWindow, origin, title, url) {
openedUrl = url;
@ -38,15 +38,15 @@ add_task(function test_startDirectCall_getCallData() {
LoopCalls.startDirectCall(contact, "audio-video");
let callId = openedUrl.match(/about:loopconversation\#outgoing\/(.*)/)[1];
let windowId = openedUrl.match(/about:loopconversation\#(\d+)$/)[1];
let callData = LoopCalls.getCallData(callId);
let callData = MozLoopService.getConversationWindowData(windowId);
do_check_eq(callData.callType, "audio-video", "should have the correct call type");
do_check_eq(callData.contact, contact, "should have the contact details");
// Stop the busy kicking in for following tests.
LoopCalls.releaseCallData(callId);
LoopCalls.clearCallInProgress(windowId);
});
function run_test() {

View File

@ -2352,7 +2352,12 @@ let E10SUINotification = {
checkStatus: function() {
let skipE10sChecks = false;
try {
// This order matters, because
// browser.tabs.remote.autostart.disabled-because-using-a11y is not
// always defined and will throw when not present.
// privacy.trackingprotection.enabled is always defined.
skipE10sChecks = (UpdateChannel.get() != "nightly") ||
Services.prefs.getBoolPref("privacy.trackingprotection.enabled") ||
Services.prefs.getBoolPref("browser.tabs.remote.autostart.disabled-because-using-a11y");
} catch(e) {}

View File

@ -4,7 +4,7 @@ ac_add_options --with-l10n-base=../../../l10n
ac_add_options --enable-update-channel=${MOZ_UPDATE_CHANNEL}
ac_add_options --enable-update-packaging
if test "${MOZ_UPDATE_CHANNEL}" = "nightly" -o "${MOZ_UPDATE_CHANNEL}" = "aurora"; then
if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
ac_add_options --with-macbundlename-prefix=Firefox
fi

View File

@ -9,7 +9,7 @@ ac_add_options --enable-dtrace
# Nightlies only since this has a cost in performance
ac_add_options --enable-js-diagnostics
if test "${MOZ_UPDATE_CHANNEL}" = "nightly" -o "${MOZ_UPDATE_CHANNEL}" = "aurora"; then
if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
ac_add_options --with-macbundlename-prefix=Firefox
fi

View File

@ -9,7 +9,7 @@ ac_add_options --with-google-oauth-api-keyfile=/builds/google-oauth-api.key
# Needed to enable breakpad in application.ini
export MOZILLA_OFFICIAL=1
if test "${MOZ_UPDATE_CHANNEL}" = "nightly" -o "${MOZ_UPDATE_CHANNEL}" = "aurora"; then
if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
ac_add_options --with-macbundlename-prefix=Firefox
fi

View File

@ -11,7 +11,7 @@ ac_add_options --disable-unified-compilation
# Package js shell.
export MOZ_PACKAGE_JSSHELL=1
if test "${MOZ_UPDATE_CHANNEL}" = "nightly" -o "${MOZ_UPDATE_CHANNEL}" = "aurora"; then
if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then
ac_add_options --with-macbundlename-prefix=Firefox
fi

View File

@ -48,7 +48,7 @@ whitelist['nightly']['linux64'] += [
]
whitelist['nightly']['macosx-universal'] += [
'if test "${MOZ_UPDATE_CHANNEL}" = "nightly" -o "${MOZ_UPDATE_CHANNEL}" = "aurora"; then',
'if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then',
'ac_add_options --with-macbundlename-prefix=Firefox',
'fi',
'mk_add_options MOZ_MAKE_FLAGS="-j12"',

View File

@ -1817,7 +1817,7 @@ richlistitem[type~="action"][actiontype="switchtab"] > .ac-url-box > .ac-action-
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 64, 16, 48);
}
.tab-close-button:not([selected]):not(:hover):-moz-lwtheme-brighttext {
#TabsToolbar[brighttext] .tab-close-button:not([selected]):not(:hover) {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 80, 16, 64);
}

View File

@ -3,3 +3,61 @@
% file, You can obtain one at http://mozilla.org/MPL/2.0/.
%include ../shared/devedition.inc.css
.tab-close-button[selected]:not(:hover) {
background-image: -moz-image-rect(url("chrome://global/skin/icons/close.svg"), 0, 80, 16, 64);
}
/* The menubar should match the dark theme */
#toolbar-menubar {
-moz-appearance: none;
}
#main-menubar {
color: var(--chrome-color);
}
#main-menubar > menu:not([open]) {
color: inherit;
}
/* Allow buttons with -moz-appearance set to look normal on hover and open states */
#navigator-toolbox .toolbarbutton-1:not(.subviewbutton):-moz-any(:hover, [open="true"]),
toolbarbutton.bookmark-item:not(.subviewbutton):-moz-any(:hover, [open="true"]) {
color: initial;
}
/* Square back and forward buttons */
#back-button:not(:-moz-lwtheme) > .toolbarbutton-icon,
#forward-button:not(:-moz-lwtheme) > .toolbarbutton-icon {
margin: 0;
border: none;
padding: 2px 6px;
background: #252C33;
box-shadow: none !important;
}
/* Override a box shadow for disabled back button */
#main-window:not([customizing]) #back-button[disabled] > .toolbarbutton-icon {
box-shadow: none !important;
}
#back-button:hover:not([disabled="true"]) > .toolbarbutton-icon,
#forward-button:hover:not([disabled="true"]) > .toolbarbutton-icon {
background: #1B2127 !important;
}
#back-button > .toolbarbutton-icon {
border-radius: 2px 0 0 2px !important;
}
.urlbar-history-dropmarker {
-moz-appearance: none;
padding: 0 3px;
list-style-image: var(--urlbar-dropmarker-url);
-moz-image-region: var(--urlbar-dropmarker-region);
}
/* Add the proper background for tab overflow */
#alltabs-button,
#new-tab-button {
background: var(--chrome-background-color);
}

View File

@ -198,6 +198,8 @@ browser.jar:
skin/classic/browser/translating-16@2x.png (../shared/translation/translating-16@2x.png)
skin/classic/browser/translation-16.png (../shared/translation/translation-16.png)
skin/classic/browser/translation-16@2x.png (../shared/translation/translation-16@2x.png)
skin/classic/browser/devedition/search.svg (../shared/devedition/search.svg)
skin/classic/browser/devedition/urlbar-history-dropmarker.svg (../shared/devedition/urlbar-history-dropmarker.svg)
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)

View File

@ -26,6 +26,7 @@
0 1px 0 hsla(0,0%,100%,.5) inset;
--toolbarbutton-active-background: hsla(0,0%,0%,.02) linear-gradient(hsla(0,0%,0%,.12), transparent) border-box;
--toolbarbutton-active-bordercolor: hsla(0,0%,0%,.3);
--toolbarbutton-active-boxshadow: 0 1px 0 hsla(0,0%,100%,.5),
0 1px 0 hsla(0,0%,0%,.05) inset,
0 1px 1px hsla(0,0%,0%,.2) inset;
@ -606,7 +607,7 @@ toolbar .toolbarbutton-1[type="menu-button"]:not(:-moz-any([disabled],[open]))[b
toolbar .toolbarbutton-1[type="menu-button"]:not(:-moz-any([disabled],[open],[buttonover])):hover:active > .toolbarbutton-menubutton-dropmarker,
toolbar .toolbarbutton-1[type="menu-button"][open]:not([disabled]) > .toolbarbutton-menubutton-dropmarker {
background: var(--toolbarbutton-active-background);
border-color: hsla(0,0%,0%,.3);
border-color: var(--toolbarbutton-active-bordercolor);
box-shadow: var(--toolbarbutton-active-boxshadow);
transition-duration: 10ms;
}
@ -766,7 +767,7 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
}
#webide-button@toolbarButtonPressed@ {
-moz-image-region: rect(18px, 342px, 36px, 324px);
-moz-image-region: rect(18px, 738px, 36px, 720px);
}
#new-tab-button@toolbarButtonPressed@ {

View File

@ -3,3 +3,85 @@
% file, You can obtain one at http://mozilla.org/MPL/2.0/.
%include ../shared/devedition.inc.css
/* Include extra space on left/right for dragging since there is no space above
the tabs */
#main-window[tabsintitlebar] #TabsToolbar {
padding-left: 50px;
padding-right: 50px;
margin-bottom: 0; /* Don't overlap the inner highlight at the top of the nav-bar */
}
/* Get rid of 1px bright strip at the top of window */
#main-window[tabsintitlebar] #titlebar-content {
background: var(--chrome-background-color);
}
/* Resize things so that the native titlebar is in line with the tabs */
#main-window[tabsintitlebar] > #titlebar > #titlebar-content > #titlebar-buttonbox-container,
#main-window[tabsintitlebar] > #titlebar > #titlebar-content > #titlebar-secondary-buttonbox > #titlebar-fullscreen-button {
margin-top: 6px;
}
/* Square back and forward buttons. Need !important on these because there
are a lot of more specific selectors sprinkled around elsewhere for changing
background / shadows for different states */
#back-button,
#forward-button {
height: 22px !important;
box-shadow: none !important;
border: none !important;
background: #252C33 !important;
}
#back-button:hover:not([disabled="true"]),
#forward-button:hover:not([disabled="true"]) {
background: #1B2127 !important;
}
#back-button {
border-radius: 3px 0 0 3px !important;
padding: 0 !important;
margin: 0 !important;
}
#back-button:hover:active:not([disabled="true"]) {
-moz-image-region: rect(18px, 54px, 36px, 36px);
}
/* Use smaller back button icon */
@media (min-resolution: 2dppx) {
#back-button {
-moz-image-region: rect(0, 108px, 36px, 72px);
}
#back-button:hover:active:not([disabled="true"]) {
-moz-image-region: rect(36px, 108px, 72px, 72px);
}
}
#forward-button:hover:active:not(:-moz-lwtheme) {
background-image: none;
box-shadow: none;
}
/* Use forward-facing magnifying glass for the search box */
.search-go-button {
list-style-image: url("chrome://browser/skin/devedition/search.svg#search-icon-mac-inverted");
}
/* Don't use default colors when in full screen */
#main-window:not([customizing]) #navigator-toolbox[inFullscreen] > #TabsToolbar:not(:-moz-lwtheme) {
-moz-appearance: none;
}
/* Tab styling - make sure to use an inverted icon for the selected tab
(brighttext only covers the unselected tabs) */
.tab-close-button[selected=true]:not(:hover) {
-moz-image-region: rect(0, 64px, 16px, 48px);
}
@media (min-resolution: 2dppx) {
.tab-close-button[selected=true]:not(:hover) {
-moz-image-region: rect(0, 128px, 32px, 96px);
}
}

View File

@ -319,6 +319,8 @@ browser.jar:
skin/classic/browser/translating-16@2x.png (../shared/translation/translating-16@2x.png)
skin/classic/browser/translation-16.png (../shared/translation/translation-16.png)
skin/classic/browser/translation-16@2x.png (../shared/translation/translation-16@2x.png)
skin/classic/browser/devedition/search.svg (../shared/devedition/search.svg)
skin/classic/browser/devedition/urlbar-history-dropmarker.svg (../shared/devedition/urlbar-history-dropmarker.svg)
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)

View File

@ -20,6 +20,15 @@
%include ../browser.inc
:root {
--panel-ui-button-background-image: linear-gradient(to bottom, transparent, hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, transparent),
linear-gradient(to bottom, transparent, hsla(210,54%,20%,.3) 30%, hsla(210,54%,20%,.3) 70%, transparent),
linear-gradient(to bottom, transparent, hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, transparent);
--panel-ui-button-background-size: 1px calc(100% - 1px), 1px calc(100% - 1px), 1px calc(100% - 1px) !important;
--panel-ui-button-background-position: 0px 0px, 1px 0px, 2px 0px;
--panel-ui-button-background-repeat: no-repeat;
}
#PanelUI-popup #PanelUI-contents:empty {
height: 128px;
}
@ -87,12 +96,10 @@
}
#PanelUI-button {
background-image: linear-gradient(to bottom, transparent, hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, transparent),
linear-gradient(to bottom, transparent, hsla(210,54%,20%,.3) 30%, hsla(210,54%,20%,.3) 70%, transparent),
linear-gradient(to bottom, transparent, hsla(0,0%,100%,.3) 30%, hsla(0,0%,100%,.3) 70%, transparent);
background-size: 1px calc(100% - 1px), 1px calc(100% - 1px), 1px calc(100% - 1px) !important;
background-position: 0px 0px, 1px 0px, 2px 0px;
background-repeat: no-repeat;
background-image: var(--panel-ui-button-background-image);
background-size: var(--panel-ui-button-background-size);
background-position: var(--panel-ui-button-background-position);
background-repeat: var(--panel-ui-button-background-repeat);
}
#PanelUI-button:-moz-locale-dir(rtl) {

View File

@ -1,3 +1,227 @@
% 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/.
/* devedition.css is loaded in browser.xul after browser.css when it is
preffed on. The bulk of the styling is here in the shared file, but
there are overrides for each platform in their devedition.css files. */
:root {
/* Chrome */
--space-above-tabbar: 1px;
--chrome-background-color: #1C2126;
--chrome-color: #F5F7FA;
--chrome-secondary-background-color: #39424D;
--chrome-navigator-toolbox-separator-color: rgba(0,0,0,.2);
/* Tabs */
--tabs-toolbar-color: #F5F7FA;
--tab-background-color: #1C2126;
--tab-color: #ced3d9;
--tab-hover-background-color: hsla(206,37%,4%,.5);
--tab-separator-color: #464C50;
--tab-selection-color: #f5f7fa;
--tab-selection-background-color: #1a4666;
--tab-selection-box-shadow: 0 2px 0 #d7f1ff inset,
0 8px 3px -5px #2b82bf inset,
0 -2px 0 rgba(0,0,0,.2) inset;
/* Toolbar buttons */
--toolbarbutton-hover-background: rgba(25,33, 38,.6) linear-gradient(rgba(25,33,38,.6), rgba(25,33,38,.6)) padding-box;
--toolbarbutton-hover-boxshadow: none;
--toolbarbutton-hover-bordercolor: rgba(25,33,38,.6);
--toolbarbutton-active-background: rgba(25,33,38,1) linear-gradient(rgba(25,33,38,1), rgba(25,33,38,1)) border-box;
--toolbarbutton-active-boxshadow: none;
--toolbarbutton-active-bordercolor: rgba(25,33,38,.8);
--toolbarbutton-checkedhover-backgroundcolor: #1D4F73;
--toolbarbutton-combined-boxshadow: none;
--toolbarbutton-combined-backgroundimage: linear-gradient(#5F6670 0, #5F6670 18px);
--toolbarbutton-text-shadow: none;
/* Identity box */
--identity-box-chrome-color: #46afe3;
--identity-box-chrome-background-image: linear-gradient(#5F6670 0, #5F6670 100%);
--identity-box-verified-background-image: linear-gradient(#5F6670 0, #5F6670 100%);
--verified-identity-box-backgroundcolor: transparent;
/* Url and search bars */
--url-and-searchbar-background-color: #171B1F;
--url-and-searchbar-color: #fff;
--urlbar-dropmarker-url: url("chrome://browser/skin/devedition/urlbar-history-dropmarker.svg");
--urlbar-dropmarker-region: rect(0px, 11px, 14px, 0px);
--urlbar-dropmarker-active-region: rect(0px, 22px, 14px, 11px);
--urlbar-dropmarker-2x-url: url("chrome://browser/skin/devedition/urlbar-history-dropmarker.svg");
--urlbar-dropmarker-2x-region: rect(0px, 11px, 14px, 0px);
--urlbar-dropmarker-active-2x-region: rect(0px, 22px, 14px, 11px);
/* Menu button separator */
--panel-ui-button-background-image: linear-gradient(to bottom, #5E6670, #5E6670);
--panel-ui-button-background-size: 1px calc(100% - 1px);
--panel-ui-button-background-position: 1px 0px;
}
.searchbar-dropmarker-image {
--searchbar-dropmarker-url: url("chrome://browser/skin/devtools/dropmarker.svg");
--searchbar-dropmarker-2x-url: url("chrome://browser/skin/devtools/dropmarker.svg");
}
/* Give some space to drag the window around while customizing
(normal space to left and right of tabs doesn't work in this case) */
#main-window[tabsintitlebar][customizing] {
--space-above-tabbar: 9px;
}
.tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox {
padding-left: 0;
padding-right: 0;
}
#navigator-toolbox ::-moz-selection {
background-color: #074D75;
color: #fff;
}
/* Change the base colors for the browser chrome */
#tabbrowser-tabs,
#TabsToolbar,
#browser-panel {
background: var(--chrome-background-color);
color: var(--chrome-color);
}
#navigator-toolbox::after {
background: var(--chrome-navigator-toolbox-separator-color)
}
#navigator-toolbox > toolbar:not(#TabsToolbar):not(#toolbar-menubar),
#browser-bottombox {
background: var(--chrome-secondary-background-color) !important;
color: var(--chrome-color);
}
#navigator-toolbox .toolbarbutton-1:not(.subviewbutton):not(:hover):not([open]),
toolbarbutton.bookmark-item:not(.subviewbutton):not(:hover):not([open]) {
color: var(--chrome-color);
text-shadow: var(--toolbarbutton-text-shadow);
}
/* Using toolbar[brighttext] instead of important to override linux */
toolbar[brighttext] #downloads-indicator-counter {
text-shadow: var(--toolbarbutton-text-shadow);
color: var(--chrome-color);
}
#TabsToolbar {
text-shadow: none !important;
color: var(--chrome-color) !important; /* Make sure that the brighttext attribute is added */
}
/* URL bar and search bar*/
.searchbar-textbox,
#urlbar {
background-color: var(--url-and-searchbar-background-color) !important;
background-image: none !important;
color: var(--url-and-searchbar-color);
border: none !important;
box-shadow: none !important;
}
window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
overflow: -moz-hidden-unscrollable;
clip-path: none;
-moz-margin-start: 0;
}
/* Make the white notication box stick out less. */
#notification-popup-box {
border-radius: 0;
border: none;
background: transparent;
}
/* Nav bar specific stuff */
#nav-bar {
margin-top: 0 !important;
border: none !important;
border-radius: 0 !important;
box-shadow: 0 -1px var(--chrome-navigator-toolbox-separator-color) !important;
background-image: none !important;
}
/* No extra vertical padding for nav bar */
#nav-bar-customization-target,
#nav-bar {
padding-top: 0;
padding-bottom: 0;
}
/* Use smaller back button icon */
#back-button {
-moz-image-region: rect(0, 54px, 18px, 36px);
}
.search-go-button {
/* !important is needed because searchbar.css is loaded after this */
-moz-image-region: auto !important;
list-style-image: url("chrome://browser/skin/devedition/search.svg#search-icon-inverted");
}
.tab-background {
visibility: hidden;
}
#tabbrowser-tabs[movingtab] > .tabbrowser-tab[beforeselected]:not([last-visible-tab])::after,
.tabbrowser-tab:not([selected]):not([afterselected-visible]):not([afterhovered]):not([first-visible-tab]):not(:hover)::before,
#tabbrowser-tabs:not([overflow]) > .tabbrowser-tab[last-visible-tab]:not([selected]):not([beforehovered]):not(:hover)::after {
background-image: linear-gradient(to top, #474C50, #474C50);
background-position: 1px 0;
background-repeat: no-repeat;
background-size: 1px 100%;
}
.tabbrowser-arrowscrollbox > .scrollbutton-down,
.tabbrowser-arrowscrollbox > .scrollbutton-up {
background-color: var(--tab-background-color);
border-color: transparent;
}
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-start-indicator:not([collapsed]),
.tabbrowser-arrowscrollbox > .arrowscrollbox-overflow-end-indicator:not([collapsed]) {
margin-bottom: 0;
}
.tabbrowser-tab {
/* We normally rely on other tab elements for pointer events, but this
theme hides those so we need it set here instead */
pointer-events: auto;
color: var(--tab-color);
background-color: var(--tab-background-color);
}
.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):hover,
.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):hover,
.tabbrowser-tab:hover {
background-color: var(--tab-hover-background-color);
color: var(--tab-hover-color);
}
.tabbrowser-tab[selected] {
color: var(--tab-selection-color);
background-color: var(--tab-selection-background-color);
box-shadow: var(--tab-selection-box-shadow);
}
/* New tab buttons */
#TabsToolbar > #new-tab-button,
.tabs-newtab-button {
background-image: none !important;
margin: 0 !important;
width: 35px !important;
}
#TabsToolbar > #new-tab-button:hover,
.tabs-newtab-button:hover {
/* Important needed because !important is used in browser.css */
background-color: var(--tab-hover-background-color) !important;
}

View File

@ -0,0 +1,41 @@
<?xml version="1.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/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
viewBox="0 0 16 16"
enable-background="new 0 0 16 16"
width="16"
height="16"
xml:space="preserve">
<style>
use:not(:target) {
display: none;
}
use {
fill: #797C80;
}
use[id*="-inverted"] {
fill: #B6BABF;
}
use[id*="-mac"] {
transform: translate(16px) scaleX(-1);
}
</style>
<defs style="display: none;">
<path id="search" fill-rule="evenodd" clip-rule="evenodd" d="M9.356,1.178c-3.014,0-5.458,2.45-5.458,5.472c0,1.086,0.32,2.096,0.864,2.947
l-3.279,3.287c-0.396,0.397-0.396,1.041,0,1.438l0.202,0.202c0.396,0.397,1.039,0.397,1.435,0l3.275-3.283
c0.854,0.554,1.869,0.88,2.962,0.88c3.014,0,5.458-2.45,5.458-5.471C14.814,3.627,12.371,1.178,9.356,1.178z M9.356,10.001
c-1.847,0-3.344-1.501-3.344-3.352c0-1.851,1.497-3.352,3.344-3.352c1.846,0,3.344,1.501,3.344,3.352
C12.7,8.501,11.203,10.001,9.356,10.001z"/>
</defs>
<use id="search-icon" xlink:href="#search"/>
<use id="search-icon-inverted" xlink:href="#search"/>
<use id="search-icon-mac" xlink:href="#search"/>
<use id="search-icon-mac-inverted" xlink:href="#search"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="33" height="14" viewBox="0 0 33 14" enable-background="new 0 0 33 14">
<defs style="display: none;">
<polygon points="0,0 5.5,7 11,0" id="dropmarker-shape"/>
</defs>
<style>
use {
fill: #B6BABF;
}
.hover {
fill: #61BDEB;
}
.active {
fill: #39ACE6;
}
</style>
<use xlink:href="#dropmarker-shape" style="transform: translate(0, 4px)"></use>
<use xlink:href="#dropmarker-shape" style="transform: translate(11px, 4px)" class="hover"></use>
<use xlink:href="#dropmarker-shape" style="transform: translate(22px, 4px)" class="active"></use>
</svg>

After

Width:  |  Height:  |  Size: 691 B

View File

@ -3,3 +3,98 @@
% file, You can obtain one at http://mozilla.org/MPL/2.0/.
%include ../shared/devedition.inc.css
#TabsToolbar::after {
display: none;
}
#back-button > .toolbarbutton-icon,
#forward-button > .toolbarbutton-icon {
background: #252C33 !important;
border-radius: 0 !important;
width: auto !important;
height: auto !important;
padding: 2px 6px !important;
margin: 0 !important;
border: none !important;
box-shadow: none !important;
}
#back-button > .toolbarbutton-icon {
border-radius: 2px 0 0 2px !important;
}
#nav-bar .toolbarbutton-1:not([type=menu-button]),
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-button,
#nav-bar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
padding-top: 2px;
padding-bottom: 2px;
}
#browser-panel,
#titlebar-content {
background: transparent;
}
/* Ensure that the entire background is styled when maximized */
#main-window[sizemode="maximized"] #browser-panel {
background: var(--chrome-background-color) !important;
}
/* The menu items need to be visible when the entire background is styled */
#main-window[sizemode="maximized"] #main-menubar {
color: var(--chrome-color);
background-color: transparent;
}
#main-window[sizemode="maximized"] #main-menubar > menu:not(:-moz-window-inactive) {
color: inherit;
}
/* Restore draggable space on the sides of tabs when maximized */
#main-window[sizemode="maximized"] .tabbrowser-arrowscrollbox > .arrowscrollbox-scrollbox {
padding-left: 15px;
padding-right: 15px;
}
@media (-moz-windows-classic) {
#main-window[sizemode="maximized"] #TabsToolbar {
background: transparent;
}
}
#toolbar-menubar {
text-shadow: none !important;
}
#main-window[tabsintitlebar][sizemode="normal"]:not([inFullscreen])[chromehidden~="menubar"] #toolbar-menubar ~ #TabsToolbar,
#main-window[tabsintitlebar][sizemode="normal"]:not([inFullscreen]) #toolbar-menubar[autohide="true"][inactive] ~ #TabsToolbar {
margin-top: 22px;
}
/* Enough space so that the dark background doesn't begin until after the
* window resize controls end on Aero Basic, which has different positioning for the
* window caption buttons, and therefore needs to be special-cased.
*/
@media (-moz-windows-default-theme) {
@media not all and (-moz-windows-compositor) {
#main-window[tabsintitlebar][sizemode="normal"]:not([inFullscreen])[chromehidden~="menubar"] #toolbar-menubar ~ #TabsToolbar,
#main-window[tabsintitlebar][sizemode="normal"]:not([inFullscreen]) #toolbar-menubar[autohide="true"][inactive] ~ #TabsToolbar {
margin-top: 28px;
}
}
}
.searchbar-dropmarker-image {
/* Reset image-region from the windows theme */
-moz-image-region: auto !important;
/* Add margin otherwise it looks weird */
-moz-margin-start: 2px;
}
/* Tab styling - make sure to use an inverted icon for the selected tab
(brighttext only covers the unselected tabs) */
.tab-close-button[selected=true]:not(:hover) {
-moz-image-region: rect(0, 64px, 16px, 48px);
}

View File

@ -233,6 +233,8 @@ browser.jar:
skin/classic/browser/translating-16@2x.png (../shared/translation/translating-16@2x.png)
skin/classic/browser/translation-16.png (../shared/translation/translation-16.png)
skin/classic/browser/translation-16@2x.png (../shared/translation/translation-16@2x.png)
skin/classic/browser/devedition/search.svg (../shared/devedition/search.svg)
skin/classic/browser/devedition/urlbar-history-dropmarker.svg (../shared/devedition/urlbar-history-dropmarker.svg)
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)
@ -664,6 +666,8 @@ browser.jar:
skin/classic/aero/browser/translating-16@2x.png (../shared/translation/translating-16@2x.png)
skin/classic/aero/browser/translation-16.png (../shared/translation/translation-16.png)
skin/classic/aero/browser/translation-16@2x.png (../shared/translation/translation-16@2x.png)
skin/classic/aero/browser/devedition/search.svg (../shared/devedition/search.svg)
skin/classic/aero/browser/devedition/urlbar-history-dropmarker.svg (../shared/devedition/urlbar-history-dropmarker.svg)
* skin/classic/aero/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/aero/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/aero/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)

View File

@ -2186,6 +2186,10 @@ GK_ATOM(Home, "Home")
GK_ATOM(Clear, "Clear")
GK_ATOM(VolumeUp, "VolumeUp")
GK_ATOM(VolumeDown, "VolumeDown")
GK_ATOM(NextTrack, "NextTrack")
GK_ATOM(PreviousTrack, "PreviousTrack")
GK_ATOM(MediaStop, "MediaStop")
GK_ATOM(PlayPause, "PlayPause")
GK_ATOM(Menu, "Menu")
GK_ATOM(New, "New")
GK_ATOM(Open, "Open")

View File

@ -33,6 +33,9 @@ public final class ThumbnailHelper {
public static final float THUMBNAIL_ASPECT_RATIO = 0.571f; // this is a 4:7 ratio (as per UX decision)
// Should actually be more like 0.83 (140/168) but various roundings mean that 0.9 works better
public static final float NEW_TABLET_THUMBNAIL_ASPECT_RATIO = 0.9f;
public static enum CachePolicy {
STORE,
NO_STORE
@ -118,11 +121,15 @@ public final class ThumbnailHelper {
// Apply any pending width updates.
mWidth = mPendingWidth.get();
mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);
if(NewTabletUI.isEnabled(GeckoAppShell.getContext())) {
mHeight = Math.round(mWidth * NEW_TABLET_THUMBNAIL_ASPECT_RATIO);
} else {
mHeight = Math.round(mWidth * THUMBNAIL_ASPECT_RATIO);
}
int pixelSize = (GeckoAppShell.getScreenDepth() == 24) ? 4 : 2;
int capacity = mWidth * mHeight * pixelSize;
Log.d(LOGTAG, "Using new thumbnail size: " + capacity + " (width " + mWidth + ")");
Log.d(LOGTAG, "Using new thumbnail size: " + capacity + " (width " + mWidth + " - height " + mHeight + ")");
if (mBuffer == null || mBuffer.capacity() != capacity) {
if (mBuffer != null) {
mBuffer = DirectBufferAllocator.free(mBuffer);

View File

@ -224,7 +224,7 @@ public class TopSitesGridItemView extends RelativeLayout {
ImageLoader.with(getContext()).cancelRequest(mThumbnailView);
mThumbnailView.setScaleType(SCALE_TYPE_THUMBNAIL);
mThumbnailView.setImageBitmap(thumbnail);
mThumbnailView.setImageBitmap(thumbnail, true);
mThumbnailView.setBackgroundDrawable(null);
}
@ -279,7 +279,7 @@ public class TopSitesGridItemView extends RelativeLayout {
}
mThumbnailView.setScaleType(SCALE_TYPE_FAVICON);
mThumbnailView.setImageBitmap(favicon);
mThumbnailView.setImageBitmap(favicon, false);
if (mFaviconURL != null) {
final int bgColor = Favicons.getFaviconColor(mFaviconURL);

View File

@ -5,14 +5,18 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.NewTabletUI;
import org.mozilla.gecko.R;
import org.mozilla.gecko.ThumbnailHelper;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff.Mode;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.widget.ImageView;
@ -35,11 +39,16 @@ public class TopSitesThumbnailView extends ImageView {
// Paint for drawing the border.
private final Paint mBorderPaint;
private boolean mResize = false;
private int mWidth;
private int mHeight;
public TopSitesThumbnailView(Context context) {
this(context, null);
// A border will be drawn if needed.
setWillNotDraw(false);
}
public TopSitesThumbnailView(Context context, AttributeSet attrs) {
@ -56,6 +65,35 @@ public class TopSitesThumbnailView extends ImageView {
mBorderPaint.setStyle(Paint.Style.STROKE);
}
public void setImageBitmap(Bitmap bm, boolean resize) {
super.setImageBitmap(bm);
mResize = resize;
}
@Override
public void setImageResource(int resId) {
super.setImageResource(resId);
mResize = false;
}
@Override
public void setImageDrawable(Drawable drawable) {
super.setImageDrawable(drawable);
mResize = false;
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if(NewTabletUI.isEnabled(getContext()) && mResize) {
setScaleType(ScaleType.MATRIX);
RectF rect = new RectF(0, 0, mWidth, mHeight);
Matrix matrix = new Matrix();
matrix.setRectToRect(rect, rect, Matrix.ScaleToFit.CENTER);
setImageMatrix(matrix);
}
}
/**
* Measure the view to determine the measured width and height.
* The height is constrained by the measured width.
@ -69,9 +107,9 @@ public class TopSitesThumbnailView extends ImageView {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// Force the height based on the aspect ratio.
final int width = getMeasuredWidth();
final int height = (int) (width * ThumbnailHelper.THUMBNAIL_ASPECT_RATIO);
setMeasuredDimension(width, height);
mWidth = getMeasuredWidth();
mHeight = (int) (mWidth * ThumbnailHelper.THUMBNAIL_ASPECT_RATIO);
setMeasuredDimension(mWidth, mHeight);
}
/**

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android" >
<item>
<shape android:shape="rectangle">
<solid android:color="@color/background_normal"/>
</shape>
</item>
<item>
<bitmap android:src="@drawable/tab_panel_tab_globe"
android:gravity="center"
/>
</item>
</layer-list>

View File

@ -52,7 +52,7 @@
<org.mozilla.gecko.widget.ThumbnailView android:id="@+id/thumbnail"
android:layout_width="@dimen/new_tablet_tab_thumbnail_width"
android:layout_height="@dimen/new_tablet_tab_thumbnail_height"
android:src="@drawable/tab_thumbnail_default"/>
/>
</RelativeLayout>

View File

@ -24,8 +24,7 @@
<org.mozilla.gecko.widget.ThumbnailView android:id="@+id/thumbnail"
android:layout_width="@dimen/tab_thumbnail_width"
android:layout_height="@dimen/tab_thumbnail_height"
android:src="@drawable/tab_thumbnail_default"/>
android:layout_height="@dimen/tab_thumbnail_height"/>
<LinearLayout android:layout_width="@dimen/tab_thumbnail_width"
android:layout_height="wrap_content"

View File

@ -24,8 +24,7 @@
<org.mozilla.gecko.widget.ThumbnailView android:id="@+id/thumbnail"
android:layout_width="@dimen/tab_thumbnail_width"
android:layout_height="@dimen/tab_thumbnail_height"
android:src="@drawable/tab_thumbnail_default"/>
android:layout_height="@dimen/tab_thumbnail_height"/>
</org.mozilla.gecko.widget.TabThumbnailWrapper>

View File

@ -95,16 +95,14 @@ public class TabsLayoutItemView extends LinearLayout
mTabId = tab.getId();
Drawable thumbnailImage = tab.getThumbnail();
if (thumbnailImage != null) {
setThumbnail(thumbnailImage);
} else {
mThumbnail.setImageResource(R.drawable.tab_thumbnail_default);
}
mThumbnail.setImageDrawable(thumbnailImage);
if (mThumbnailWrapper != null) {
mThumbnailWrapper.setRecording(tab.isRecording());
}
mTitle.setText(tab.getDisplayTitle());
mCloseButton.setTag(this);
}
public int getTabId() {

View File

@ -202,6 +202,7 @@ abstract class BaseTest extends BaseRobocopTest {
*/
protected final void focusUrlBar() {
// Click on the browser toolbar to enter editing mode
mSolo.waitForView(R.id.browser_toolbar);
final View toolbarView = mSolo.getView(R.id.browser_toolbar);
mSolo.clickOnView(toolbarView);
@ -221,10 +222,10 @@ abstract class BaseTest extends BaseRobocopTest {
}
protected final void enterUrl(String url) {
final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text);
focusUrlBar();
final EditText urlEditView = (EditText) mSolo.getView(R.id.url_edit_text);
// Send the keys for the URL we want to enter
mSolo.clearEditText(urlEditView);
mSolo.enterText(urlEditView, url);
@ -329,25 +330,27 @@ abstract class BaseTest extends BaseRobocopTest {
return result;
}
// TODO: With Robotium 4.2, we should use Condition and waitForCondition instead.
// Future boolean tests should not use this method.
protected final boolean waitForTest(BooleanTest t, int timeout) {
long end = SystemClock.uptimeMillis() + timeout;
while (SystemClock.uptimeMillis() < end) {
if (t.test()) {
return true;
/**
* @deprecated use {@link #waitForCondition(Condition, int)} instead
*/
@Deprecated
protected final boolean waitForTest(final BooleanTest t, final int timeout) {
final boolean isSatisfied = mSolo.waitForCondition(new Condition() {
@Override
public boolean isSatisfied() {
return t.test();
}
mSolo.sleep(100);
}, timeout);
if (!isSatisfied) {
// log out wait failure for diagnostic purposes only;
// a failed wait may be normal and does not necessarily
// warrant a test assertion/failure
mAsserter.dumpLog("waitForTest timeout after " + timeout + " ms");
}
// log out wait failure for diagnostic purposes only;
// a failed wait may be normal and does not necessarily
// warrant a test assertion/failure
mAsserter.dumpLog("waitForTest timeout after "+timeout+" ms");
return false;
return isSatisfied;
}
// TODO: With Robotium 4.2, we should use Condition and waitForCondition instead.
// Future boolean tests should not implement this interface.
protected interface BooleanTest {
public boolean test();
}

View File

@ -5,6 +5,8 @@
package org.mozilla.gecko.widget;
import org.mozilla.gecko.R;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
@ -21,6 +23,7 @@ public class ThumbnailView extends ImageView {
private int mWidthSpec = -1;
private int mHeightSpec = -1;
private boolean mLayoutChanged;
private boolean mScale = false;
public ThumbnailView(Context context, AttributeSet attrs) {
super(context, attrs);
@ -30,13 +33,18 @@ public class ThumbnailView extends ImageView {
@Override
public void onDraw(Canvas canvas) {
if (!mScale) {
super.onDraw(canvas);
return;
}
Drawable d = getDrawable();
if (mLayoutChanged) {
int w1 = d.getIntrinsicWidth();
int h1 = d.getIntrinsicHeight();
int w2 = getWidth();
int h2 = getHeight();
float scale = (w2/h2 < w1/h1) ? (float)h2/h1 : (float)w2/w1;
mMatrix.setScale(scale, scale);
}
@ -59,4 +67,19 @@ public class ThumbnailView extends ImageView {
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
public void setImageDrawable(Drawable drawable) {
if (drawable == null) {
drawable = getResources().getDrawable(R.drawable.tab_panel_tab_background);
setScaleType(ScaleType.FIT_XY);
mScale = false;
} else {
mScale = true;
setScaleType(ScaleType.FIT_CENTER);
}
super.setImageDrawable(drawable);
}
}

View File

@ -2519,6 +2519,8 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
NS_IMETHODIMP
nsNavBookmarks::GetKeywordForURI(nsIURI* aURI, nsAString& aKeyword)
{
PLACES_WARN_DEPRECATED();
NS_ENSURE_ARG(aURI);
aKeyword.Truncate(0);

View File

@ -348,10 +348,6 @@ add_task(function test_bookmarks() {
let k = bs.getKeywordForBookmark(kwTestItemId);
do_check_eq("bar", k);
// test getKeywordForURI
k = bs.getKeywordForURI(uri("http://keywordtest.com/"));
do_check_eq("bar", k);
// test getURIForKeyword
let u = bs.getURIForKeyword("bar");
do_check_eq("http://keywordtest.com/", u.spec);

View File

@ -11,8 +11,16 @@ function check_bookmark_keyword(aItemId, aKeyword)
function check_uri_keyword(aURI, aKeyword)
{
let keyword = aKeyword ? aKeyword.toLowerCase() : null;
do_check_eq(PlacesUtils.bookmarks.getKeywordForURI(aURI),
keyword);
for (let bm of PlacesUtils.getBookmarksForURI(aURI)) {
let kid = PlacesUtils.bookmarks.getKeywordForBookmark(bm);
if (kid && !keyword) {
Assert.ok(false, `${aURI.spec} should not have a keyword`);
} else if (keyword && kid == keyword) {
Assert.equal(kid, keyword, "Found the keyword");
break;
}
}
if (aKeyword) {
// This API can't tell which uri the user wants, so it returns a random one.

View File

@ -88,8 +88,6 @@ add_task(function () {
ok(PlacesUtils.bookmarks.isBookmarked(bookmarkUri),
"Bookmark should be bookmarked, data should be retrievable");
is(bookmarkKeyword, PlacesUtils.bookmarks.getKeywordForURI(bookmarkUri),
"Check bookmark uri keyword");
is(getPlacesItemsCount(), count,
"Check the new bookmark items count");
is(isBookmarkAltered(), false, "Check if bookmark has been visited");

View File

@ -2357,15 +2357,25 @@ SelectProfile(nsIProfileLock* *aResult, nsIToolkitProfileService* aProfileSvc, n
// create a default profile
nsCOMPtr<nsIToolkitProfile> profile;
nsresult rv = aProfileSvc->CreateProfile(nullptr, // choose a default dir for us
#ifdef MOZ_DEV_EDITION
NS_LITERAL_CSTRING("dev-edition-default"),
#else
NS_LITERAL_CSTRING("default"),
#endif
getter_AddRefs(profile));
if (NS_SUCCEEDED(rv)) {
#ifndef MOZ_DEV_EDITION
aProfileSvc->SetDefaultProfile(profile);
#endif
aProfileSvc->Flush();
rv = profile->Lock(nullptr, aResult);
if (NS_SUCCEEDED(rv)) {
if (aProfileName)
#ifdef MOZ_DEV_EDITION
aProfileName->AssignLiteral("dev-edition-default");
#else
aProfileName->AssignLiteral("default");
#endif
return NS_OK;
}
}
@ -4630,6 +4640,13 @@ mozilla::BrowserTabsRemoteAutostart()
}
#endif
bool tpEnabled = Preferences::GetBool("privacy.trackingprotection.enabled",
false);
if (tpEnabled) {
gBrowserTabsRemoteAutostart = false;
LogE10sBlockedReason("Tracking protection is enabled");
}
mozilla::Telemetry::Accumulate(mozilla::Telemetry::E10S_AUTOSTART, gBrowserTabsRemoteAutostart);
if (Preferences::GetBool("browser.enabledE10SFromPrompt", false)) {
mozilla::Telemetry::Accumulate(mozilla::Telemetry::E10S_STILL_ACCEPTED_FROM_PROMPT,

View File

@ -250,6 +250,18 @@ nsWindowBase::DispatchCommandEvent(uint32_t aEventCommand)
case APPCOMMAND_SEND_MAIL:
command = nsGkAtoms::SendMail;
break;
case APPCOMMAND_MEDIA_NEXTTRACK:
command = nsGkAtoms::NextTrack;
break;
case APPCOMMAND_MEDIA_PREVIOUSTRACK:
command = nsGkAtoms::PreviousTrack;
break;
case APPCOMMAND_MEDIA_STOP:
command = nsGkAtoms::MediaStop;
break;
case APPCOMMAND_MEDIA_PLAY_PAUSE:
command = nsGkAtoms::PlayPause;
break;
default:
return false;
}
@ -287,6 +299,10 @@ nsWindowBase::HandleAppCommandMsg(WPARAM aWParam,
case APPCOMMAND_FORWARD_MAIL:
case APPCOMMAND_REPLY_TO_MAIL:
case APPCOMMAND_SEND_MAIL:
case APPCOMMAND_MEDIA_NEXTTRACK:
case APPCOMMAND_MEDIA_PREVIOUSTRACK:
case APPCOMMAND_MEDIA_STOP:
case APPCOMMAND_MEDIA_PLAY_PAUSE:
// We shouldn't consume the message always because if we don't handle
// the message, the sender (typically, utility of keyboard or mouse)
// may send other key messages which indicate well known shortcut key.

View File

@ -142,16 +142,17 @@
#define APPCOMMAND_BROWSER_FAVORITES 6
#define APPCOMMAND_BROWSER_HOME 7
#define APPCOMMAND_MEDIA_NEXTTRACK 11
#define APPCOMMAND_MEDIA_PREVIOUSTRACK 12
#define APPCOMMAND_MEDIA_STOP 13
#define APPCOMMAND_MEDIA_PLAY_PAUSE 14
/*
* Additional commands currently not in use.
*
*#define APPCOMMAND_VOLUME_MUTE 8
*#define APPCOMMAND_VOLUME_DOWN 9
*#define APPCOMMAND_VOLUME_UP 10
*#define APPCOMMAND_MEDIA_NEXTTRACK 11
*#define APPCOMMAND_MEDIA_PREVIOUSTRACK 12
*#define APPCOMMAND_MEDIA_STOP 13
*#define APPCOMMAND_MEDIA_PLAY_PAUSE 14
*#define APPCOMMAND_LAUNCH_MAIL 15
*#define APPCOMMAND_LAUNCH_MEDIA_SELECT 16
*#define APPCOMMAND_LAUNCH_APP1 17