merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2014-10-30 14:49:31 +01:00
commit 72ea59b550
90 changed files with 1232 additions and 280 deletions

View File

@ -1695,6 +1695,10 @@ pref("identity.fxaccounts.remote.signin.uri", "https://accounts.firefox.com/sign
// "identity.fxaccounts.remote.signup.uri" pref.
pref("identity.fxaccounts.settings.uri", "https://accounts.firefox.com/settings");
// Migrate any existing Firefox Account data from the default profile to the
// Developer Edition profile.
pref("identity.fxaccounts.migrateToDevEdition", false);
// On GTK, we now default to showing the menubar only when alt is pressed:
#ifdef MOZ_WIDGET_GTK
pref("ui.key.menuAccessKeyFocuses", true);

View File

@ -331,8 +331,11 @@ function init() {
show("remote");
wrapper.init(url, entryPoint);
});
} else if (window.location.href.contains("action=migrateToDevEdition") &&
user == null) {
migrateToDevEdition(user, entryPoint);
} else {
// No action specified
// No action specified, or migration request when we already have a user.
if (user) {
show("stage", "manage");
let sb = Services.strings.createBundle("chrome://browser/locale/syncSetup.properties");
@ -372,6 +375,48 @@ function show(id, childId) {
}
}
// Migrate sync data from the default profile to the dev-edition profile.
function migrateToDevEdition(user, entryPoint) {
let migrateSyncCreds = false;
try {
migrateSyncCreds = Services.prefs.getBoolPref("identity.fxaccounts.migrateToDevEdition");
} catch (e) {}
if (migrateSyncCreds) {
Cu.import("resource://gre/modules/osfile.jsm");
let fxAccountsStorage = OS.Path.join(window.getDefaultProfilePath(), fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
return OS.File.read(fxAccountsStorage, { encoding: "utf-8" }).then(text => {
let accountData = JSON.parse(text).accountData;
return fxAccounts.setSignedInUser(accountData);
}).then(() => {
return fxAccounts.promiseAccountsForceSigninURI().then(url => {
show("remote");
wrapper.init(url, entryPoint);
});
}).then(null, error => {
log("Failed to migrate FX Account: " + error);
show("stage", "intro");
// load the remote frame in the background
wrapper.init(fxAccounts.getAccountsSignUpURI(), entryPoint);
}).then(() => {
// Reset the pref after migration.
Services.prefs.setBoolPref("identity.fxaccounts.migrateToDevEdition", false);
});
} else {
show("stage", "intro");
// load the remote frame in the background
wrapper.init(fxAccounts.getAccountsSignUpURI(), entryPoint);
}
}
// Helper function that returns the path of the default profile on disk. Will be
// overridden in tests.
function getDefaultProfilePath() {
let defaultProfile = Cc["@mozilla.org/toolkit/profile-service;1"]
.getService(Ci.nsIToolkitProfileService)
.defaultProfile;
return defaultProfile.rootDir.path;
}
document.addEventListener("DOMContentLoaded", function onload() {
document.removeEventListener("DOMContentLoaded", onload, true);
init();

View File

@ -22,8 +22,12 @@ searchbar {
-moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
}
.browserStack > browser {
-moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-browser");
}
.browserStack > browser[remote="true"] {
-moz-binding: url("chrome://global/content/bindings/remote-browser.xml#remote-browser");
-moz-binding: url("chrome://browser/content/tabbrowser.xml#tabbrowser-remote-browser");
}
toolbar[customizable="true"] {

View File

@ -799,34 +799,62 @@ function gKeywordURIFixup({ target: browser, data: fixupInfo }) {
}
}
// Called when a docshell has attempted to load a page in an incorrect process.
// This function is responsible for loading the page in the correct process.
function RedirectLoad({ target: browser, data }) {
// A shared function used by both remote and non-remote browser XBL bindings to
// load a URI or redirect it to the correct process.
function _loadURIWithFlags(browser, uri, flags, referrer, charset, postdata) {
if (!uri) {
uri = "about:blank";
}
if (!(flags & browser.webNavigation.LOAD_FLAGS_FROM_EXTERNAL)) {
browser.userTypedClear++;
}
try {
let shouldBeRemote = gMultiProcessBrowser &&
E10SUtils.shouldBrowserBeRemote(uri);
if (browser.isRemoteBrowser == shouldBeRemote) {
browser.webNavigation.loadURI(uri, flags, referrer, postdata, null);
} else {
LoadInOtherProcess(browser, {
uri: uri,
flags: flags,
referrer: referrer ? referrer.spec : null,
});
}
} finally {
if (browser.userTypedClear) {
browser.userTypedClear--;
}
}
}
// Starts a new load in the browser first switching the browser to the correct
// process
function LoadInOtherProcess(browser, loadOptions, historyIndex = -1) {
let tab = gBrowser.getTabForBrowser(browser);
// Flush the tab state before getting it
TabState.flush(browser);
let tabState = JSON.parse(SessionStore.getTabState(tab));
if (data.historyIndex < 0) {
// Add a pseudo-history state for the new url to load
let newEntry = {
url: data.uri,
referrer: data.referrer,
};
tabState.entries = tabState.entries.slice(0, tabState.index);
tabState.entries.push(newEntry);
tabState.index++;
if (historyIndex < 0) {
tabState.userTypedValue = null;
// Tell session history the new page to load
SessionStore._restoreTabAndLoad(tab, JSON.stringify(tabState), loadOptions);
}
else {
// Update the history state to point to the requested index
tabState.index = data.historyIndex + 1;
tabState.index = historyIndex + 1;
// SessionStore takes care of setting the browser remoteness before restoring
// history into it.
SessionStore.setTabState(tab, JSON.stringify(tabState));
}
}
// SessionStore takes care of setting the browser remoteness before restoring
// history into it.
SessionStore.setTabState(tab, JSON.stringify(tabState));
// Called when a docshell has attempted to load a page in an incorrect process.
// This function is responsible for loading the page in the correct process.
function RedirectLoad({ target: browser, data }) {
LoadInOtherProcess(browser, data.loadOptions, data.historyIndex);
}
var gBrowserInit = {

View File

@ -5377,4 +5377,42 @@
</implementation>
</binding>
<binding id="tabbrowser-browser"
extends="chrome://global/content/bindings/browser.xml#browser">
<implementation>
<!-- throws exception for unknown schemes -->
<method name="loadURIWithFlags">
<parameter name="aURI"/>
<parameter name="aFlags"/>
<parameter name="aReferrerURI"/>
<parameter name="aCharset"/>
<parameter name="aPostData"/>
<body>
<![CDATA[
_loadURIWithFlags(this, aURI, aFlags, aReferrerURI, aCharset, aPostData);
]]>
</body>
</method>
</implementation>
</binding>
<binding id="tabbrowser-remote-browser"
extends="chrome://global/content/bindings/remote-browser.xml#remote-browser">
<implementation>
<!-- throws exception for unknown schemes -->
<method name="loadURIWithFlags">
<parameter name="aURI"/>
<parameter name="aFlags"/>
<parameter name="aReferrerURI"/>
<parameter name="aCharset"/>
<parameter name="aPostData"/>
<body>
<![CDATA[
_loadURIWithFlags(this, aURI, aFlags, aReferrerURI, aCharset, aPostData);
]]>
</body>
</method>
</implementation>
</binding>
</bindings>

View File

@ -231,7 +231,6 @@ skip-if = e10s # Bug ?????? - test directly manipulates content
[browser_bug579872.js]
[browser_bug580638.js]
[browser_bug580956.js]
skip-if = e10s # Bug 516755 - SessionStore disabled for e10s
[browser_bug581242.js]
skip-if = e10s # Bug 930863 - pageshow issues ("TypeError: charset is undefined" in pageshow listener, as document is null)
[browser_bug581253.js]
@ -254,7 +253,6 @@ skip-if = e10s # Bug 516755 - SessionStore disabled for e10s (calls duplicateTab
[browser_bug623155.js]
skip-if = e10s # Bug ?????? - URLBar issues (apparently issues with redirection)
[browser_bug623893.js]
skip-if = e10s # Bug 916974 - Session history doesn't work in e10s
[browser_bug624734.js]
[browser_bug633691.js]
skip-if = e10s # Bug ?????? - test directly manipulates content (eg, var expertDiv = gBrowser.contentDocument.getElementById("expertContent");)
@ -272,6 +270,7 @@ skip-if = e10s # Bug ?????? - test directly manipulates content (doc.querySelect
[browser_bug719271.js]
skip-if = e10s # Bug 691614 - no e10s zoom support yet
[browser_bug724239.js]
skip-if = e10s # Bug 1077738
[browser_bug734076.js]
skip-if = e10s # Bug ?????? - test directly manipulates content
[browser_bug735471.js]
@ -287,7 +286,6 @@ skip-if = e10s # Bug ?????? - test directly manipulates content
[browser_bug816527.js]
skip-if = e10s # Bug 916974 - Session history doesn't work in e10s
[browser_bug817947.js]
skip-if = e10s # Bug 916974 - Session history doesn't work in e10s
[browser_bug822367.js]
[browser_bug832435.js]
[browser_bug839103.js]
@ -494,7 +492,6 @@ skip-if = e10s # Bug 516755 - SessionStore disabled for e10s
[browser_registerProtocolHandler_notification.js]
skip-if = e10s # Bug 940206 - nsIWebContentHandlerRegistrar::registerProtocolHandler doesn't work in e10s
[browser_no_mcb_on_http_site.js]
skip-if = e10s # Bug 516755 - SessionStore disabled for e10s
[browser_bug1003461-switchtab-override.js]
skip-if = e10s
[browser_bug1024133-switchtab-override-keynav.js]

View File

@ -15,6 +15,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
// Preference helpers.
@ -180,6 +182,107 @@ let gTests = [
is(url, expected, "action=reauth got the expected URL");
},
},
{
desc: "Test action=migrateToDevEdition (success)",
teardown: function* () {
gBrowser.removeCurrentTab();
yield signOut();
},
run: function* ()
{
let fxAccountsCommon = {};
Cu.import("resource://gre/modules/FxAccountsCommon.js", fxAccountsCommon);
const pref = "identity.fxaccounts.migrateToDevEdition";
changedPrefs.add(pref);
Services.prefs.setBoolPref(pref, true);
// Create the signedInUser.json file that will be used as the source of
// migrated user data.
let signedInUser = {
version: 1,
accountData: {
email: "foo@example.com",
uid: "1234@lcip.org",
sessionToken: "dead",
verified: true
}
};
// We use a sub-dir of the real profile dir as the "pretend" profile dir
// for this test.
let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
let mockDir = profD.clone();
mockDir.append("about-accounts-mock-profd");
mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
let fxAccountsStorage = OS.Path.join(mockDir.path, fxAccountsCommon.DEFAULT_STORAGE_FILENAME);
yield OS.File.writeAtomic(fxAccountsStorage, JSON.stringify(signedInUser));
info("Wrote file " + fxAccountsStorage);
// this is a little subtle - we load about:blank so we get a tab, then
// we send a message which does both (a) load the URL we want and (b) mocks
// the default profile path used by about:accounts.
let tab = yield promiseNewTabLoadEvent("about:blank?");
let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
let mm = tab.linkedBrowser.messageManager;
mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
url: "about:accounts?action=migrateToDevEdition",
profilePath: mockDir.path,
});
let response = yield readyPromise;
// We are expecting the iframe to be on the "force reauth" URL
let expected = yield fxAccounts.promiseAccountsForceSigninURI();
is(response.data.url, expected);
let userData = yield fxAccounts.getSignedInUser();
SimpleTest.isDeeply(userData, signedInUser.accountData, "All account data were migrated");
// The migration pref will have been switched off by now.
is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
yield OS.File.remove(fxAccountsStorage);
yield OS.File.removeEmptyDir(mockDir.path);
},
},
{
desc: "Test action=migrateToDevEdition (no user to migrate)",
teardown: function* () {
gBrowser.removeCurrentTab();
yield signOut();
},
run: function* ()
{
const pref = "identity.fxaccounts.migrateToDevEdition";
changedPrefs.add(pref);
Services.prefs.setBoolPref(pref, true);
let profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
let mockDir = profD.clone();
mockDir.append("about-accounts-mock-profd");
mockDir.createUnique(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
// but leave it empty, so we don't think a user is logged in.
let tab = yield promiseNewTabLoadEvent("about:blank?");
let readyPromise = promiseOneMessage(tab, "test:load-with-mocked-profile-path-response");
let mm = tab.linkedBrowser.messageManager;
mm.sendAsyncMessage("test:load-with-mocked-profile-path", {
url: "about:accounts?action=migrateToDevEdition",
profilePath: mockDir.path,
});
let response = yield readyPromise;
// We are expecting the iframe to be on the "signup" URL
let expected = fxAccounts.getAccountsSignUpURI();
is(response.data.url, expected);
// and expect no signed in user.
let userData = yield fxAccounts.getSignedInUser();
is(userData, null);
// The migration pref should have still been switched off.
is(Services.prefs.getBoolPref(pref), false, pref + " got the expected value");
yield OS.File.removeEmptyDir(mockDir.path);
},
},
{
desc: "Test observers about:accounts",
teardown: function() {
@ -323,5 +426,6 @@ function setSignedInUser(data) {
}
function signOut() {
return fxAccounts.signOut();
// we always want a "localOnly" signout here...
return fxAccounts.signOut(true);
}

View File

@ -52,7 +52,8 @@ let check_history = Task.async(function*() {
// Waits for a load and updates the known history
let waitForLoad = Task.async(function*(uri) {
info("Loading " + uri);
gBrowser.loadURI(uri);
// Longwinded but this ensures we don't just shortcut to LoadInNewProcess
gBrowser.selectedBrowser.webNavigation.loadURI(uri, Ci.nsIWebNavigation.LOAD_FLAGS_NONE, null, null, null);
yield waitForDocLoadComplete();
gExpectedHistory.index++;
@ -78,7 +79,7 @@ let forward = Task.async(function*() {
// Tests that navigating from a page that should be in the remote process and
// a page that should be in the main process works and retains history
add_task(function*() {
add_task(function* test_navigation() {
SimpleTest.requestCompleteLog();
let remoting = Services.prefs.getBoolPref("browser.tabs.remote.autostart");
@ -138,5 +139,59 @@ add_task(function*() {
yield check_history();
info("9");
yield back();
is(gBrowser.selectedTab.getAttribute("remote"), "", "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
yield check_history();
info("10");
// Load a new remote page, this should replace the last history entry
gExpectedHistory.entries.splice(gExpectedHistory.entries.length - 1, 1);
yield waitForLoad("http://example.com/" + DUMMY_PATH);
is(gBrowser.selectedTab.getAttribute("remote"), expectedRemote, "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
yield check_history();
info("11");
gBrowser.removeCurrentTab();
});
// Tests that calling gBrowser.loadURI or browser.loadURI to load a page in a
// different process updates the browser synchronously
add_task(function* test_synchronous() {
let remoting = Services.prefs.getBoolPref("browser.tabs.remote.autostart");
let expectedRemote = remoting ? "true" : "";
info("1");
// Create a tab and load a remote page in it
gBrowser.selectedTab = gBrowser.addTab("about:blank", {skipAnimation: true});
let {permanentKey} = gBrowser.selectedBrowser;
yield waitForLoad("http://example.org/" + DUMMY_PATH);
is(gBrowser.selectedTab.getAttribute("remote"), expectedRemote, "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
info("2");
// Load another page
info("Loading about:robots");
gBrowser.selectedBrowser.loadURI("about:robots");
is(gBrowser.selectedTab.getAttribute("remote"), "", "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
yield waitForDocLoadComplete();
is(gBrowser.selectedTab.getAttribute("remote"), "", "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
info("3");
// Load the remote page again
info("Loading http://example.org/" + DUMMY_PATH);
gBrowser.loadURI("http://example.org/" + DUMMY_PATH);
is(gBrowser.selectedTab.getAttribute("remote"), expectedRemote, "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
yield waitForDocLoadComplete();
is(gBrowser.selectedTab.getAttribute("remote"), expectedRemote, "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
info("4");
gBrowser.removeCurrentTab();
});

View File

@ -5,6 +5,8 @@
// This file is loaded as a "content script" for browser_aboutAccounts tests
"use strict";
const {interfaces: Ci, utils: Cu} = Components;
addEventListener("load", function load(event) {
if (event.target != content.document) {
return;
@ -16,6 +18,10 @@ addEventListener("load", function load(event) {
addEventListener("DOMContentLoaded", function domContentLoaded(event) {
removeEventListener("DOMContentLoaded", domContentLoaded, true);
let iframe = content.document.getElementById("remote");
if (!iframe) {
// at least one test initially loads about:blank - in that case, we are done.
return;
}
iframe.addEventListener("load", function iframeLoaded(event) {
if (iframe.contentWindow.location.href == "about:blank" ||
event.target != iframe) {
@ -46,3 +52,23 @@ addMessageListener("test:check-visibilities", function (message) {
}
sendAsyncMessage("test:check-visibilities-response", result);
});
addMessageListener("test:load-with-mocked-profile-path", function (message) {
addEventListener("DOMContentLoaded", function domContentLoaded(event) {
removeEventListener("DOMContentLoaded", domContentLoaded, true);
content.getDefaultProfilePath = () => message.data.profilePath;
// now wait for the iframe to load.
let iframe = content.document.getElementById("remote");
iframe.addEventListener("load", function iframeLoaded(event) {
if (iframe.contentWindow.location.href == "about:blank" ||
event.target != iframe) {
return;
}
iframe.removeEventListener("load", iframeLoaded, true);
sendAsyncMessage("test:load-with-mocked-profile-path-response",
{url: iframe.getAttribute("src")});
}, true);
});
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
webNav.loadURI(message.data.url, webNav.LOAD_FLAGS_NONE, null, null, null);
}, true);

View File

@ -38,6 +38,7 @@ const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
const kPrefDeveditionTheme = "browser.devedition.theme.enabled";
const kPrefWebIDEInNavbar = "devtools.webide.widget.inNavbarByDefault";
/**
* The keys are the handlers that are fired when the event type (the value)
@ -198,18 +199,24 @@ let CustomizableUIInternal = {
}, true);
PanelWideWidgetTracker.init();
let navbarPlacements = [
"urlbar-container",
"search-container",
"bookmarks-menu-button",
"downloads-button",
"home-button",
"loop-call-button",
];
if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) {
navbarPlacements.push("webide-button");
}
this.registerArea(CustomizableUI.AREA_NAVBAR, {
legacy: true,
type: CustomizableUI.TYPE_TOOLBAR,
overflowable: true,
defaultPlacements: [
"urlbar-container",
"search-container",
"bookmarks-menu-button",
"downloads-button",
"home-button",
"loop-call-button",
],
defaultPlacements: navbarPlacements,
defaultCollapsed: false,
}, true);
#ifndef XP_MACOSX

View File

@ -48,7 +48,7 @@ const extractFieldsFromNode = function(fieldMap, node, ns = null, target = {}, w
if (!nodeList[0].firstChild) {
continue;
}
let value = nodeList[0].firstChild.nodeValue;
let value = nodeList[0].textContent;
target[field] = wrapInArray ? [value] : value;
}
}
@ -168,8 +168,8 @@ this.GoogleImporter.prototype = {
Task.spawn(function* () {
let code = yield this._promiseAuthCode(windowRef);
let tokenSet = yield this._promiseTokenSet(code);
let contactEntries = yield this._promiseContactEntries(tokenSet);
let {total, success, ids} = yield this._processContacts(contactEntries, db);
let contactEntries = yield this._getContactEntries(tokenSet);
let {total, success, ids} = yield this._processContacts(contactEntries, db, tokenSet);
yield this._purgeContacts(ids, db);
return {
@ -286,22 +286,12 @@ this.GoogleImporter.prototype = {
});
},
/**
* Fetches all the contacts in a users' address book.
*
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
*
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
* @returns An `Error` object upon failure or an Array of contact XML nodes.
*/
_promiseContactEntries: function(tokenSet) {
return new Promise(function(resolve, reject) {
_promiseRequestXML: function(URL, tokenSet) {
return new Promise((resolve, reject) => {
let request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
request.open("GET", getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
"loop.oauth.google.getContactsURL",
false) + "?max-results=" + kContactsMaxResults);
request.open("GET", URL);
request.setRequestHeader("Content-Type", "application/xml; charset=utf-8");
request.setRequestHeader("GData-Version", "3.0");
@ -310,19 +300,17 @@ this.GoogleImporter.prototype = {
request.onload = function() {
if (request.status < 400) {
let doc = request.responseXML;
// First get the profile id.
// First get the profile id, which is present in each XML request.
let currNode = doc.documentElement.firstChild;
while (currNode) {
if (currNode.nodeType == 1 && currNode.localName == "id") {
gProfileId = currNode.firstChild.nodeValue;
gProfileId = currNode.textContent;
break;
}
currNode = currNode.nextSibling;
}
// Then kick of the importing of contact entries.
let entries = Array.prototype.slice.call(doc.querySelectorAll("entry"));
resolve(entries);
resolve(doc);
} else {
reject(new Error(request.status + " " + request.statusText));
}
@ -336,6 +324,46 @@ this.GoogleImporter.prototype = {
});
},
/**
* Fetches all the contacts in a users' address book.
*
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contacts
*
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
* @returns An `Error` object upon failure or an Array of contact XML nodes.
*/
_getContactEntries: Task.async(function* (tokenSet) {
let URL = getUrlParam("https://www.google.com/m8/feeds/contacts/default/full",
"loop.oauth.google.getContactsURL",
false) + "?max-results=" + kContactsMaxResults;
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
// Then kick of the importing of contact entries.
return Array.prototype.slice.call(xmlDoc.querySelectorAll("entry"));
}),
/**
* Fetches the default group from a users' address book, called 'Contacts'.
*
* @see https://developers.google.com/google-apps/contacts/v3/#retrieving_all_contact_groups
*
* @param {Object} tokenSet OAuth tokenset used to authenticate the request
* @returns An `Error` object upon failure or the String group ID.
*/
_getContactsGroupId: Task.async(function* (tokenSet) {
let URL = getUrlParam("https://www.google.com/m8/feeds/groups/default/full",
"loop.oauth.google.getGroupsURL",
false) + "?max-results=" + kContactsMaxResults;
let xmlDoc = yield this._promiseRequestXML(URL, tokenSet);
let contactsEntry = xmlDoc.querySelector("systemGroup[id=\"Contacts\"]");
if (!contactsEntry) {
throw new Error("Contacts group not present");
}
// Select the actual <entry> node, which is the parent of the <systemGroup>
// node we just selected.
contactsEntry = contactsEntry.parentNode;
return contactsEntry.getElementsByTagName("id")[0].textContent;
}),
/**
* Process the contact XML nodes that Google provides, convert them to the MozContact
* format, check if the contact already exists in the database and when it doesn't,
@ -343,21 +371,28 @@ this.GoogleImporter.prototype = {
* During this process statistics are collected about the amount of successful
* imports. The consumer of this class may use these statistics to inform the
* user.
* Note: only contacts that are part of the 'Contacts' system group will be
* imported.
*
* @param {Array} contactEntries List of XML DOMNodes contact entries.
* @param {LoopContacts} db Instance of the LoopContacts database
* object, which will store the newly found
* contacts.
* @param {Object} tokenSet OAuth tokenset used to authenticate a
* request
* @returns An `Error` object upon failure or an Object with statistics in the
* following format: `{ total: 25, success: 13, ids: {} }`.
*/
_processContacts: Task.async(function* (contactEntries, db) {
_processContacts: Task.async(function* (contactEntries, db, tokenSet) {
let stats = {
total: contactEntries.length,
success: 0,
ids: {}
};
// Contacts that are _not_ part of the 'Contacts' group will be ignored.
let contactsGroupId = yield this._getContactsGroupId(tokenSet);
for (let entry of contactEntries) {
let contact = this._processContactFields(entry);
@ -367,6 +402,12 @@ this.GoogleImporter.prototype = {
yield db.promise("remove", existing._guid);
}
// After contact removal, check if the entry is part of the correct group.
if (!entry.querySelector("groupMembershipInfo[deleted=\"false\"][href=\"" +
contactsGroupId + "\"]")) {
continue;
}
// If the contact contains neither email nor phone number, then it is not
// useful in the Loop address book: do not add.
if (!("email" in contact) && !("tel" in contact)) {
@ -450,7 +491,7 @@ this.GoogleImporter.prototype = {
for (let [,phoneNode] of Iterator(phoneNodes)) {
let phoneNumber = phoneNode.hasAttribute("uri") ?
phoneNode.getAttribute("uri").replace("tel:", "") :
phoneNode.firstChild.nodeValue;
phoneNode.textContent;
contact.tel.push({
pref: (phoneNode.getAttribute("primary") == "true"),
type: [getFieldType(phoneNode)],
@ -466,8 +507,8 @@ this.GoogleImporter.prototype = {
for (let [,orgNode] of Iterator(orgNodes)) {
let orgElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgName")[0];
let titleElement = orgNode.getElementsByTagNameNS(kNS_GD, "orgTitle")[0];
contact.org.push(orgElement ? orgElement.firstChild.nodeValue : "")
contact.jobTitle.push(titleElement ? titleElement.firstChild.nodeValue : "");
contact.org.push(orgElement ? orgElement.textContent : "")
contact.jobTitle.push(titleElement ? titleElement.textContent : "");
}
}

View File

@ -68,7 +68,7 @@ let LoopRoomsInternal = {
}
Task.spawn(function* () {
yield MozLoopService.register();
yield MozLoopService.promiseRegisteredWithServers();
if (!gDirty) {
callback(null, [...this.rooms.values()]);

View File

@ -369,7 +369,7 @@ function injectLoopAPI(targetWindow) {
value: function(callback) {
// We translate from a promise to a callback, as we can't pass promises from
// Promise.jsm across the priv versus unpriv boundary.
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
callback(null);
}, err => {
callback(cloneValueInto(err, targetWindow));

View File

@ -30,6 +30,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
Cu.import("resource://gre/modules/osfile.jsm", this);
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Timer.jsm");
Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
Cu.importGlobalProperties(["URL"]);
@ -111,7 +112,6 @@ function getJSONPref(aName) {
let gRegisteredDeferred = null;
let gHawkClient = null;
let gLocalizedStrings = null;
let gInitializeTimer = null;
let gFxAEnabled = true;
let gFxAOAuthClientPromise = null;
let gFxAOAuthClient = null;
@ -309,7 +309,7 @@ let MozLoopServiceInternal = {
/**
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
* with the Loop server as a GUEST. It will return early if already registered.
*
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
@ -871,42 +871,17 @@ let MozLoopServiceInternal = {
};
Object.freeze(MozLoopServiceInternal);
let gInitializeTimerFunc = (deferredInitialization) => {
// Kick off the push notification service into registering after a timeout.
// This ensures we're not doing too much straight after the browser's finished
// starting up.
gInitializeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
gInitializeTimer.initWithCallback(Task.async(function* initializationCallback() {
yield MozLoopService.register().then(Task.async(function*() {
if (!MozLoopServiceInternal.fxAOAuthTokenData) {
log.debug("MozLoopService: Initialized without an already logged-in account");
deferredInitialization.resolve("initialized to guest status");
return;
}
log.debug("MozLoopService: Initializing with already logged-in account");
let registeredPromise =
MozLoopServiceInternal.registerWithLoopServer(
LOOP_SESSION_TYPE.FXA, {
calls: MozLoopServiceInternal.pushHandler.registeredChannels[MozLoopService.channelIDs.callsFxA],
rooms: MozLoopServiceInternal.pushHandler.registeredChannels[MozLoopService.channelIDs.roomsFxA]
});
registeredPromise.then(() => {
deferredInitialization.resolve("initialized to logged-in status");
}, error => {
log.debug("MozLoopService: error logging in using cached auth token");
MozLoopServiceInternal.setError("login", error);
deferredInitialization.reject("error logging in using cached auth token");
});
}), error => {
log.debug("MozLoopService: Failure of initial registration", error);
deferredInitialization.reject(error);
});
gInitializeTimer = null;
}),
MozLoopServiceInternal.initialRegistrationDelayMilliseconds, Ci.nsITimer.TYPE_ONE_SHOT);
setTimeout(MozLoopService.delayedInitialize.bind(MozLoopService, deferredInitialization),
MozLoopServiceInternal.initialRegistrationDelayMilliseconds);
};
/**
* Public API
*/
@ -962,13 +937,60 @@ this.MozLoopService = {
let deferredInitialization = Promise.defer();
gInitializeTimerFunc(deferredInitialization);
return deferredInitialization.promise.catch(error => {
return deferredInitialization.promise;
}),
/**
* The core of the initialization work that happens once the browser is ready
* (after a timer when called during startup).
*
* Can be called more than once (e.g. if the initial setup fails at some phase).
* @param {Deferred} deferredInitialization
*/
delayedInitialize: Task.async(function*(deferredInitialization) {
// Set or clear an error depending on how deferredInitialization gets resolved.
// We do this first so that it can handle the early returns below.
let completedPromise = deferredInitialization.promise.then(result => {
MozLoopServiceInternal.clearError("initialization");
return result;
},
error => {
// If we get a non-object then setError was already called for a different error type.
if (typeof(error) == "object") {
// This never gets cleared since there is no UI to recover. Only restarting will work.
MozLoopServiceInternal.setError("initialization", error);
}
throw error;
});
try {
yield this.promiseRegisteredWithServers();
} catch (ex) {
log.debug("MozLoopService: Failure of initial registration", ex);
deferredInitialization.reject(ex);
yield completedPromise;
return;
}
if (!MozLoopServiceInternal.fxAOAuthTokenData) {
log.debug("MozLoopService: Initialized without an already logged-in account");
deferredInitialization.resolve("initialized to guest status");
yield completedPromise;
return;
}
log.debug("MozLoopService: Initializing with already logged-in account");
let pushURLs = {
calls: MozLoopServiceInternal.pushHandler.registeredChannels[this.channelIDs.callsFxA],
rooms: MozLoopServiceInternal.pushHandler.registeredChannels[this.channelIDs.roomsFxA]
};
MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, pushURLs).then(() => {
deferredInitialization.resolve("initialized to logged-in status");
}, error => {
log.debug("MozLoopService: error logging in using cached auth token");
MozLoopServiceInternal.setError("login", error);
deferredInitialization.reject("error logging in using cached auth token");
});
yield completedPromise;
}),
/**
@ -1081,25 +1103,10 @@ this.MozLoopService = {
Services.tm.mainThread);
},
/**
* Starts registration of Loop with the push server, and then will register
* with the Loop server. It will return early if already registered.
*
* @returns {Promise} a promise that is resolved with no params on completion, or
* rejected with an error code or string.
* @see MozLoopServiceInternal.promiseRegisteredWithServers
*/
register: function() {
log.debug("registering");
// Don't do anything if loop is not enabled.
if (!Services.prefs.getBoolPref("loop.enabled")) {
throw new Error("Loop is not enabled");
}
if (Services.prefs.getBoolPref("loop.throttled")) {
throw new Error("Loop is disabled by the soft-start mechanism");
}
promiseRegisteredWithServers: function() {
return MozLoopServiceInternal.promiseRegisteredWithServers();
},

View File

@ -471,8 +471,13 @@ loop.panel = (function(_, mozL10n) {
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
},
getInitialState: function() {
return { urlCopied: false };
},
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.room.ctime > this.props.room.ctime;
return (nextProps.room.ctime > this.props.room.ctime) ||
(nextState.urlCopied !== this.state.urlCopied);
},
handleClickRoom: function(event) {
@ -480,6 +485,16 @@ loop.panel = (function(_, mozL10n) {
this.props.openRoom(this.props.room);
},
handleCopyButtonClick: function(event) {
event.preventDefault();
navigator.mozLoop.copyString(this.props.room.roomUrl);
this.setState({urlCopied: true});
},
handleMouseLeave: function(event) {
this.setState({urlCopied: false});
},
_isActive: function() {
// XXX bug 1074679 will implement this properly
return this.props.room.currSize > 0;
@ -491,12 +506,18 @@ loop.panel = (function(_, mozL10n) {
"room-entry": true,
"room-active": this._isActive()
});
var copyButtonClasses = React.addons.classSet({
'copy-link': true,
'checked': this.state.urlCopied
});
return (
React.DOM.div({className: roomClasses},
React.DOM.div({className: roomClasses, onMouseLeave: this.handleMouseLeave},
React.DOM.h2(null,
React.DOM.span({className: "room-notification"}),
room.roomName
room.roomName,
React.DOM.button({className: copyButtonClasses,
onClick: this.handleCopyButtonClick})
),
React.DOM.p(null,
React.DOM.a({ref: "room", href: "#", onClick: this.handleClickRoom},
@ -762,6 +783,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
RoomEntry: RoomEntry,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView

View File

@ -471,8 +471,13 @@ loop.panel = (function(_, mozL10n) {
room: React.PropTypes.instanceOf(loop.store.Room).isRequired
},
getInitialState: function() {
return { urlCopied: false };
},
shouldComponentUpdate: function(nextProps, nextState) {
return nextProps.room.ctime > this.props.room.ctime;
return (nextProps.room.ctime > this.props.room.ctime) ||
(nextState.urlCopied !== this.state.urlCopied);
},
handleClickRoom: function(event) {
@ -480,6 +485,16 @@ loop.panel = (function(_, mozL10n) {
this.props.openRoom(this.props.room);
},
handleCopyButtonClick: function(event) {
event.preventDefault();
navigator.mozLoop.copyString(this.props.room.roomUrl);
this.setState({urlCopied: true});
},
handleMouseLeave: function(event) {
this.setState({urlCopied: false});
},
_isActive: function() {
// XXX bug 1074679 will implement this properly
return this.props.room.currSize > 0;
@ -491,12 +506,18 @@ loop.panel = (function(_, mozL10n) {
"room-entry": true,
"room-active": this._isActive()
});
var copyButtonClasses = React.addons.classSet({
'copy-link': true,
'checked': this.state.urlCopied
});
return (
<div className={roomClasses}>
<div className={roomClasses} onMouseLeave={this.handleMouseLeave}>
<h2>
<span className="room-notification" />
{room.roomName}
{room.roomName}
<button className={copyButtonClasses}
onClick={this.handleCopyButtonClick}/>
</h2>
<p>
<a ref="room" href="#" onClick={this.handleClickRoom}>
@ -762,6 +783,7 @@ loop.panel = (function(_, mozL10n) {
AvailabilityDropdown: AvailabilityDropdown,
CallUrlResult: CallUrlResult,
PanelView: PanelView,
RoomEntry: RoomEntry,
RoomList: RoomList,
SettingsDropdown: SettingsDropdown,
ToSView: ToSView

View File

@ -79,8 +79,8 @@
.fx-embedded-answer-btn-text {
vertical-align: bottom;
/* don't stretch the button if the localized text is too big */
max-width: 80%;
/* always leave space for the icon (width and margin) */
max-width: calc(100% - .8rem - .2rem);
}
.fx-embedded-btn-icon-video,
@ -91,6 +91,7 @@
height: .8rem;
background-repeat: no-repeat;
cursor: pointer;
-moz-margin-start: .2rem;
}
.fx-embedded-btn-icon-video,

View File

@ -202,6 +202,49 @@ body {
text-decoration: underline;
}
.room-list > .room-entry > h2 > .copy-link {
display: inline-block;
width: 24px;
height: 24px;
border: none;
margin: .1em .5em; /* relative to _this_ line's font, not the document's */
background-color: transparent; /* override browser default for button tags */
}
@keyframes drop-and-fade-in {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 100; transform: translateY(0); }
}
.room-list > .room-entry:hover > h2 > .copy-link {
background: transparent url(../img/svg/copy-16x16.svg);
cursor: pointer;
animation: drop-and-fade-in 0.4s;
animation-fill-mode: forwards;
}
/* scale this up to 1.1x and then back to the original size */
@keyframes pulse {
0%, 100% { transform: scale(1.0); }
50% { transform: scale(1.1); }
}
.room-list > .room-entry > h2 > .copy-link.checked {
background: transparent url(../img/svg/checkmark-16x16.svg);
animation: pulse .250s;
animation-timing-function: ease-in-out;
}
.room-list > .room-entry > h2 {
display: inline-block;
}
/* keep the various room-entry row pieces aligned with each other */
.room-list > .room-entry > h2 > button,
.room-list > .room-entry > h2 > span {
vertical-align: middle;
}
/* Buttons */
.button-group {

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" 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" xml:space="preserve">
<circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8"
cy="8" r="8"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
d="M7.236,12L12,5.007L10.956,4L7.224,9.465l-2.14-2.326L4,8.146L7.236,12z"/>
</svg>

After

Width:  |  Height:  |  Size: 597 B

View File

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" 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" xml:space="preserve">
<circle fill-rule="evenodd" clip-rule="evenodd" fill="#0096DD" cx="8" cy="8"
r="8"/>
<g>
<g>
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="none"
stroke="#FFFFFF" stroke-width="0.75" stroke-miterlimit="10"
d="M10.815,6.286H7.556c-0.164,0-0.296,0.128-0.296,0.286v5.143C7.259,11.872,7.392,12,7.556,12h4.148
C11.867,12,12,11.872,12,11.714V7.429L10.815,6.286z
M8.741,6.275V5.143L7.556,4H7.528C6.509,4,4.593,4,4.593,4H4.296
C4.133,4,4,4.128,4,4.286v5.143c0,0.158,0.133,0.286,0.296,0.286H7.25V6.561c0-0.158,0.133-0.286,0.296-0.286H8.741z"/>
</g>
</g>
<g>
<polygon fill-rule="evenodd" clip-rule="evenodd"
fill="#FFFFFF" points="10.222,8 10.222,6.857 11.407,8"/>
</g>
<g>
<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF"
points="6.963,5.714 6.963,4.571 8.148,5.714"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -48,6 +48,8 @@ browser.jar:
content/browser/loop/shared/img/svg/glyph-account-16x16.svg (content/shared/img/svg/glyph-account-16x16.svg)
content/browser/loop/shared/img/svg/glyph-signin-16x16.svg (content/shared/img/svg/glyph-signin-16x16.svg)
content/browser/loop/shared/img/svg/glyph-signout-16x16.svg (content/shared/img/svg/glyph-signout-16x16.svg)
content/browser/loop/shared/img/svg/copy-16x16.svg (content/shared/img/svg/copy-16x16.svg)
content/browser/loop/shared/img/svg/checkmark-16x16.svg (content/shared/img/svg/checkmark-16x16.svg)
content/browser/loop/shared/img/audio-call-avatar.svg (content/shared/img/audio-call-avatar.svg)
content/browser/loop/shared/img/beta-ribbon.svg (content/shared/img/beta-ribbon.svg)
content/browser/loop/shared/img/icons-10x10.svg (content/shared/img/icons-10x10.svg)

View File

@ -264,7 +264,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: "connecting"
@ -571,6 +570,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var FailedConversationView = React.createClass({displayName: 'FailedConversationView',
mixins: [sharedMixins.AudioMixin],
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),

View File

@ -264,7 +264,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
var PendingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: "connecting"
@ -571,6 +570,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var FailedConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),

View File

@ -637,6 +637,72 @@ describe("loop.panel", function() {
});
});
describe("loop.panel.RoomEntry", function() {
var buttonNode, roomData, roomEntry, roomStore, dispatcher;
beforeEach(function() {
dispatcher = new loop.Dispatcher();
roomData = {
roomToken: "QzBbvGmIZWU",
roomUrl: "http://sample/QzBbvGmIZWU",
roomName: "Second Room Name",
maxSize: 2,
participants: [
{ displayName: "Alexis", account: "alexis@example.com",
roomConnectionId: "2a1787a6-4a73-43b5-ae3e-906ec1e763cb" },
{ displayName: "Adam",
roomConnectionId: "781f012b-f1ea-4ce1-9105-7cfc36fb4ec7" }
],
ctime: 1405517418
};
roomStore = new loop.store.Room(roomData);
roomEntry = mountRoomEntry();
buttonNode = roomEntry.getDOMNode().querySelector("button.copy-link");
});
function mountRoomEntry() {
return TestUtils.renderIntoDocument(loop.panel.RoomEntry({
openRoom: sandbox.stub(),
room: roomStore
}));
}
it("should not display copy-link button by default", function() {
expect(buttonNode).to.not.equal(null);
});
it("should copy the URL when the click event fires", function() {
TestUtils.Simulate.click(buttonNode);
sinon.assert.calledOnce(navigator.mozLoop.copyString);
sinon.assert.calledWithExactly(navigator.mozLoop.copyString,
roomData.roomUrl);
});
it("should set state.urlCopied when the click event fires", function() {
TestUtils.Simulate.click(buttonNode);
expect(roomEntry.state.urlCopied).to.equal(true);
});
it("should switch to displaying a check icon when the URL has been copied",
function() {
TestUtils.Simulate.click(buttonNode);
expect(buttonNode.classList.contains("checked")).eql(true);
});
it("should not display a check icon after mouse leaves the entry",
function() {
var roomNode = roomEntry.getDOMNode();
TestUtils.Simulate.click(buttonNode);
TestUtils.SimulateNative.mouseOut(roomNode);
expect(buttonNode.classList.contains("checked")).eql(false);
});
});
describe("loop.panel.RoomList", function() {
var roomListStore, dispatcher;

View File

@ -2,6 +2,7 @@
support-files =
fixtures/google_auth.txt
fixtures/google_contacts.txt
fixtures/google_groups.txt
fixtures/google_token.txt
google_service.sjs
head.js

View File

@ -17,23 +17,22 @@ function promiseImport() {
});
}
const kContactsCount = 7;
const kIncomingTotalContactsCount = 8;
const kExpectedImportCount = 7;
add_task(function* test_GoogleImport() {
let stats;
// An error may throw and the test will fail when that happens.
stats = yield promiseImport();
let contactsCount = mockDb.size;
// Assert the world.
Assert.equal(stats.total, contactsCount, "Five contacts should get processed");
Assert.equal(stats.success, contactsCount, "Five contacts should be imported");
Assert.equal(stats.total, kIncomingTotalContactsCount, kIncomingTotalContactsCount + " contacts should get processed");
Assert.equal(stats.success, kExpectedImportCount, kExpectedImportCount + " contacts should be imported");
yield promiseImport();
Assert.equal(Object.keys(mockDb._store).length, contactsCount, "Database should be the same size after reimport");
Assert.equal(mockDb.size, kExpectedImportCount, "Database should be the same size after reimport");
let currentContact = contactsCount;
let currentContact = kExpectedImportCount;
let c = mockDb._store[mockDb._next_guid - currentContact];
Assert.equal(c.name[0], "John Smith", "Full name should match");
@ -96,4 +95,7 @@ add_task(function* test_GoogleImport() {
Assert.equal(c.tel[0].pref, false, "Pref should match");
Assert.equal(c.category[0], "google", "Category should match");
Assert.equal(c.id, "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6", "UID should match and be scoped to provider");
c = yield mockDb.promise("getByServiceId", "http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9");
Assert.equal(c, null, "Contacts that are not part of the default group should not be imported");
});

View File

@ -253,7 +253,7 @@ add_task(function* basicAuthorizationAndRegistration() {
mockPushHandler.registrationPushURL = "https://localhost/pushUrl/guest";
// Notification observed due to the error being cleared upon successful registration.
let statusChangedPromise = promiseObserverNotified("loop-status-changed");
yield MozLoopService.register();
yield MozLoopService.promiseRegisteredWithServers();
yield statusChangedPromise;
// Normally the same pushUrl would be registered but we change it in the test
@ -318,7 +318,7 @@ add_task(function* loginWithParams401() {
test_error: "params_401",
};
yield promiseOAuthParamsSetup(BASE_URL, params);
yield MozLoopService.register();
yield MozLoopService.promiseRegisteredWithServers();
let loginPromise = MozLoopService.logInToFxA();
yield loginPromise.then(tokenData => {

View File

@ -34,6 +34,7 @@
</gd:name>
<gd:email address="john.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109576547678240773721" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/1</id>
@ -51,6 +52,7 @@
</gd:name>
<gd:email address="jane.smith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/112886528199784431028" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;R3YyejRVLit7I2A9WhJWEkkNQwc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/2</id>
@ -68,6 +70,7 @@
</gd:name>
<gd:email address="davy.jones@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/109710625881478599011" rel="profile"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/3</id>
@ -79,6 +82,7 @@
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/3" rel="edit" type="application/atom+xml"/>
<gd:email address="noname@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Q3w7ezVSLit7I2A9WB5WGUkNRgE.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/7</id>
@ -90,6 +94,7 @@
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/7" rel="edit" type="application/atom+xml"/>
<gd:email address="lycnix" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;RXkzfjVSLit7I2A9XRdRGUgITgA.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/8</id>
@ -101,6 +106,7 @@
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/8" rel="edit" type="application/atom+xml"/>
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile" uri="tel:+31-6-12345678">0612345678</gd:phoneNumber>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;SX8-ejVSLit7I2A9XRdQFUkDRgY.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/6</id>
@ -114,4 +120,21 @@
<gd:phoneNumber rel="http://schemas.google.com/g/2005#mobile">215234523452345</gd:phoneNumber>
<gContact:groupMembershipInfo deleted="false" href="http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6"/>
</entry>
<entry gd:etag="&quot;Rn8zejVSLit7I2A9WhVRFUQOQQc.&quot;">
<id>http://www.google.com/m8/feeds/contacts/tester%40mochi.com/base/9</id>
<updated>2012-03-24T13:10:37.182Z</updated>
<app:edited xmlns:app="http://www.w3.org/2007/app">2012-03-24T13:10:37.182Z</app:edited>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#contact"/>
<title>Little Smith</title>
<link href="https://www.google.com/m8/feeds/photos/media/tester%40mochi.com/9" rel="http://schemas.google.com/contacts/2008/rel#photo" type="image/*"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="self" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/contacts/tester%40mochi.com/full/9" rel="edit" type="application/atom+xml"/>
<gd:name>
<gd:fullName>Little Smith</gd:fullName>
<gd:givenName>Little</gd:givenName>
<gd:familyName>Smith</gd:familyName>
</gd:name>
<gd:email address="littlebabysmith@example.com" primary="true" rel="http://schemas.google.com/g/2005#other"/>
<gContact:website href="http://www.google.com/profiles/111456826635924971693" rel="profile"/>
</entry>
</feed>

View File

@ -0,0 +1,56 @@
<?xml version='1.0' encoding='UTF-8'?>
<feed gd:etag="W/&quot;CEIAQngzfyt7I2A9XRdXFEQ.&quot;" xmlns="http://www.w3.org/2005/Atom" xmlns:batch="http://schemas.google.com/gdata/batch" xmlns:gContact="http://schemas.google.com/contact/2008" xmlns:gd="http://schemas.google.com/g/2005" xmlns:openSearch="http://a9.com/-/spec/opensearch/1.1/">
<id>tester@mochi.com</id>
<updated>2014-10-28T10:35:43.687Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>Mochi Tester's Contact Groups</title>
<link href="http://www.google.com/" rel="alternate" type="text/html"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#feed" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full" rel="http://schemas.google.com/g/2005#post" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/batch" rel="http://schemas.google.com/g/2005#batch" type="application/atom+xml"/>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full?max-results=10000000" rel="self" type="application/atom+xml"/>
<author>
<name>Mochi Tester</name>
<email>tester@mochi.com</email>
</author>
<generator uri="http://www.google.com/m8/feeds" version="1.0">Contacts</generator>
<openSearch:totalResults>4</openSearch:totalResults>
<openSearch:startIndex>1</openSearch:startIndex>
<openSearch:itemsPerPage>10000000</openSearch:itemsPerPage>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/6</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: My Contacts</title>
<content>System Group: My Contacts</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/6" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Contacts"/>
</entry>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/d</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: Friends</title>
<content>System Group: Friends</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/d" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Friends"/>
</entry>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/e</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: Family</title>
<content>System Group: Family</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/e" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Family"/>
</entry>
<entry gd:etag="&quot;YDwreyM.&quot;">
<id>http://www.google.com/m8/feeds/groups/tester%40mochi.com/base/f</id>
<updated>1970-01-01T00:00:00.000Z</updated>
<category scheme="http://schemas.google.com/g/2005#kind" term="http://schemas.google.com/contact/2008#group"/>
<title>System Group: Coworkers</title>
<content>System Group: Coworkers</content>
<link href="https://www.google.com/m8/feeds/groups/tester%40mochi.com/full/f" rel="self" type="application/atom+xml"/>
<gContact:systemGroup id="Coworkers"/>
</entry>
</feed>

View File

@ -143,5 +143,15 @@ const methodHandlers = {
}
respondWithFile(res, "google_contacts.txt", "text/xml");
},
groups: function(req, res, params) {
try {
checkAuth(req);
} catch (ex) {
sendError(res, ex, ex.code);
}
respondWithFile(res, "google_groups.txt", "text/xml");
}
};

View File

@ -501,6 +501,41 @@ describe("loop.webapp", function() {
});
});
});
describe("FailedConversationView", function() {
var view, conversation, client, fakeAudio;
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
client = new loop.StandaloneClient({
baseServerUrl: "http://fake.example.com"
});
conversation = new sharedModels.ConversationModel({}, {
sdk: {}
});
conversation.set("loopToken", "fakeToken");
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.FailedConversationView({
conversation: conversation,
client: client,
notifications: notifications
}));
});
it("should play a failure sound, once", function() {
sinon.assert.calledOnce(window.Audio);
sinon.assert.calledWithExactly(window.Audio,
"shared/sounds/failure.ogg");
expect(fakeAudio.loop).to.equal(false);
});
});
});
describe("WebappRootView", function() {

View File

@ -31,6 +31,11 @@ var loopServer;
Services.prefs.setBoolPref("loop.enabled", true);
Services.prefs.setBoolPref("loop.throttled", false);
// Cleanup function for all tests
do_register_cleanup(() => {
MozLoopService.errors.clear();
});
function setupFakeLoopServer() {
loopServer = new HttpServer();
loopServer.start(-1);

View File

@ -135,7 +135,7 @@ const compareRooms = function(room1, room2) {
};
add_task(function* test_getAllRooms() {
yield MozLoopService.register(mockPushHandler);
yield MozLoopService.promiseRegisteredWithServers();
let rooms = yield LoopRooms.promise("getAll");
Assert.equal(rooms.length, 3);
@ -145,7 +145,7 @@ add_task(function* test_getAllRooms() {
});
add_task(function* test_getRoom() {
yield MozLoopService.register(mockPushHandler);
yield MozLoopService.promiseRegisteredWithServers();
let roomToken = "_nxD4V4FflQ";
let room = yield LoopRooms.promise("get", roomToken);

View File

@ -24,7 +24,7 @@ let msgHandler = function(msg) {
add_test(function test_busy_2guest_calls() {
actionReceived = false;
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
opened++;
@ -47,7 +47,7 @@ add_test(function test_busy_2guest_calls() {
add_test(function test_busy_1fxa_1guest_calls() {
actionReceived = false;
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
opened++;
@ -71,7 +71,7 @@ add_test(function test_busy_1fxa_1guest_calls() {
add_test(function test_busy_2fxa_calls() {
actionReceived = false;
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
opened++;
@ -94,7 +94,7 @@ add_test(function test_busy_2fxa_calls() {
add_test(function test_busy_1guest_1fxa_calls() {
actionReceived = false;
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = 0;
Chat.open = function() {
opened++;

View File

@ -31,7 +31,7 @@ add_test(function test_set_do_not_disturb() {
add_test(function test_do_not_disturb_disabled_should_open_chat_window() {
MozLoopService.doNotDisturb = false;
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = false;
Chat.open = function() {
opened = true;

View File

@ -1,7 +1,7 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
var startTimerCalled = false;
let startTimerCalled = false;
/**
* Tests that registration doesn't happen when the expiry time is
@ -23,7 +23,7 @@ add_task(function test_initialize_no_expiry() {
*/
add_task(function test_initialize_expiry_past() {
// Set time to be 2 seconds in the past.
var nowSeconds = Date.now() / 1000;
let nowSeconds = Date.now() / 1000;
Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", nowSeconds - 2);
startTimerCalled = false;
@ -39,7 +39,7 @@ add_task(function test_initialize_expiry_past() {
*/
add_task(function test_initialize_starts_timer() {
// Set time to be 1 minute in the future
var nowSeconds = Date.now() / 1000;
let nowSeconds = Date.now() / 1000;
Services.prefs.setIntPref("loop.urlsExpiryTimeSeconds", nowSeconds + 60);
startTimerCalled = false;
@ -49,8 +49,7 @@ add_task(function test_initialize_starts_timer() {
"should start the timer when expiry time is in the future");
});
function run_test()
{
function run_test() {
setupFakeLoopServer();
// Override MozLoopService's initializeTimer, so that we can verify the timeout is called

View File

@ -10,7 +10,7 @@ let openChatOrig = Chat.open;
add_test(function test_openChatWindow_on_notification() {
Services.prefs.setCharPref("loop.seenToS", "unseen");
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
let opened = false;
Chat.open = function() {
opened = true;

View File

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

View File

@ -75,12 +75,17 @@ add_task(function test_initialize_with_invalid_fxa_token() {
"FXA pref should be cleared if token was invalid");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), "",
"FXA profile pref should be cleared if token was invalid");
Assert.ok(MozLoopServiceInternal.errors.has("login"),
"Initialization error should have been reported to UI");
});
});
add_task(function test_initialize_with_fxa_token() {
Services.prefs.setCharPref(LOOP_FXA_PROFILE_PREF, FAKE_FXA_PROFILE);
Services.prefs.setCharPref(LOOP_FXA_TOKEN_PREF, FAKE_FXA_TOKEN_DATA);
MozLoopService.errors.clear();
loopServer.registerPathHandler("/registration", (request, response) => {
response.setStatusLine(null, 200, "OK");
});
@ -90,6 +95,7 @@ add_task(function test_initialize_with_fxa_token() {
"FXA pref should still be set after initialization");
Assert.equal(Services.prefs.getCharPref(LOOP_FXA_PROFILE_PREF), FAKE_FXA_PROFILE,
"FXA profile should still be set after initialization");
Assert.ok(!MozLoopServiceInternal.errors.has("login"), "Initialization error should not exist");
});
});

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ add_test(function test_registration_handles_bogus_hawk_token() {
response.finish();
});
MozLoopService.register().then(() => {
MozLoopService.promiseRegisteredWithServers().then(() => {
do_throw("should not succeed with a bogus token");
}, err => {

View File

@ -57,6 +57,7 @@ navigator.mozLoop = {
}
},
releaseCallData: function() {},
copyString: function() {},
contacts: {
getAll: function(callback) {
callback(null, []);

View File

@ -159,7 +159,7 @@ ContentRestoreInternal.prototype = {
* Start loading the current page. When the data has finished loading from the
* network, finishCallback is called. Returns true if the load was successful.
*/
restoreTabContent: function (finishCallback) {
restoreTabContent: function (loadArguments, finishCallback) {
let tabData = this._tabData;
this._tabData = null;
@ -188,7 +188,19 @@ ContentRestoreInternal.prototype = {
webNavigation.setCurrentURI(Utils.makeURI("about:blank"));
try {
if (tabData.userTypedValue && tabData.userTypedClear) {
if (loadArguments) {
// A load has been redirected to a new process so get history into the
// same state it was before the load started then trigger the load.
let activeIndex = tabData.index - 1;
if (activeIndex > 0) {
// Go to the right history entry, but don't load anything yet.
history.getEntryAtIndex(activeIndex, true);
}
let referrer = loadArguments.referrer ?
Utils.makeURI(loadArguments.referrer) : null;
webNavigation.loadURI(loadArguments.uri, loadArguments.loadFlags,
referrer, null, null);
} else if (tabData.userTypedValue && tabData.userTypedClear) {
// If the user typed a URL into the URL bar and hit enter right before
// we crashed, we want to start loading that page again. A non-zero
// userTypedClear value means that the load had started.

View File

@ -67,7 +67,7 @@ let SessionHistoryInternal = {
let data = {entries: []};
let isPinned = docShell.isAppTab;
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
let history = webNavigation.sessionHistory;
let history = webNavigation.sessionHistory.QueryInterface(Ci.nsISHistoryInternal);
if (history && history.count > 0) {
let oldest;
@ -88,17 +88,28 @@ let SessionHistoryInternal = {
newest = history.count - 1;
}
try {
for (let i = oldest; i <= newest; i++) {
let shEntry = history.getEntryAtIndex(i, false);
let entry = this.serializeEntry(shEntry, isPinned);
data.entries.push(entry);
}
} catch (ex) {
// In some cases, getEntryAtIndex will throw. This seems to be due to
// history.count being higher than it should be. By doing this in a
// try-catch, we'll update history to where it breaks, print an error
// message, and still save sessionstore.js.
// Loop over the transaction linked list directly so we can get the
// persist property for each transaction.
let txn = history.rootTransaction;
let i = 0;
while (txn && i < oldest) {
txn = txn.next;
i++;
}
while (txn && i <= newest) {
let shEntry = txn.sHEntry;
let entry = this.serializeEntry(shEntry, isPinned);
entry.persist = txn.persist;
data.entries.push(entry);
txn = txn.next;
i++;
}
if (i <= newest) {
// In some cases, there don't seem to be as many history entries as
// history.count claims. we'll save whatever history we can, print an
// error message, and still save sessionstore.js.
debug("SessionStore failed gathering complete history " +
"for the focused window/tab. See bug 669196.");
}
@ -297,11 +308,12 @@ let SessionHistoryInternal = {
let idMap = { used: {} };
let docIdentMap = {};
for (let i = 0; i < tabData.entries.length; i++) {
let entry = tabData.entries[i];
//XXXzpao Wallpaper patch for bug 514751
if (!tabData.entries[i].url)
if (!entry.url)
continue;
history.addEntry(this.deserializeEntry(tabData.entries[i],
idMap, docIdentMap), true);
let persist = "persist" in entry ? entry.persist : true;
history.addEntry(this.deserializeEntry(entry, idMap, docIdentMap), persist);
}
},

View File

@ -192,6 +192,16 @@ this.SessionStore = {
SessionStoreInternal.setTabState(aTab, aState);
},
// This should not be used by external code, the intention is to remove it
// once a better fix is in place for process switching in e10s.
// See bug 1075658 for context.
_restoreTabAndLoad: function ss_restoreTabAndLoad(aTab, aState, aLoadArguments) {
SessionStoreInternal.setTabState(aTab, aState, {
restoreImmediately: true,
loadArguments: aLoadArguments
});
},
duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) {
return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
},
@ -1572,7 +1582,7 @@ let SessionStoreInternal = {
return this._toJSONString(tabState);
},
setTabState: function ssi_setTabState(aTab, aState) {
setTabState: function ssi_setTabState(aTab, aState, aOptions) {
// Remove the tab state from the cache.
// Note that we cannot simply replace the contents of the cache
// as |aState| can be an incomplete state that will be completed
@ -1597,7 +1607,7 @@ let SessionStoreInternal = {
this._resetTabRestoringState(aTab);
}
this.restoreTab(aTab, tabState);
this.restoreTab(aTab, tabState, aOptions);
},
duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0) {
@ -1623,7 +1633,9 @@ let SessionStoreInternal = {
aWindow.gBrowser.addTab(null, {relatedToCurrent: true, ownerTab: aTab}) :
aWindow.gBrowser.addTab();
this.restoreTab(newTab, tabState, true /* Load this tab right away. */);
this.restoreTab(newTab, tabState, {
restoreImmediately: true /* Load this tab right away. */
});
return newTab;
},
@ -2527,7 +2539,9 @@ let SessionStoreInternal = {
},
// Restores the given tab state for a given tab.
restoreTab(tab, tabData, restoreImmediately = false) {
restoreTab(tab, tabData, options = {}) {
let restoreImmediately = options.restoreImmediately;
let loadArguments = options.loadArguments;
let browser = tab.linkedBrowser;
let window = tab.ownerDocument.defaultView;
let tabbrowser = window.gBrowser;
@ -2595,6 +2609,9 @@ let SessionStoreInternal = {
// attribute so that it runs in a content process.
let activePageData = tabData.entries[activeIndex] || null;
let uri = activePageData ? activePageData.url || null : null;
if (loadArguments) {
uri = loadArguments.uri;
}
tabbrowser.updateBrowserRemotenessByURL(browser, uri);
// Start a new epoch and include the epoch in the restoreHistory
@ -2630,8 +2647,8 @@ let SessionStoreInternal = {
// This could cause us to ignore MAX_CONCURRENT_TAB_RESTORES a bit, but
// it ensures each window will have its selected tab loaded.
if (restoreImmediately || tabbrowser.selectedBrowser == browser) {
this.restoreTabContent(tab);
if (restoreImmediately || tabbrowser.selectedBrowser == browser || loadArguments) {
this.restoreTabContent(tab, loadArguments);
} else {
TabRestoreQueue.add(tab);
this.restoreNextTab();
@ -2658,7 +2675,7 @@ let SessionStoreInternal = {
*
* @returns true/false indicating whether or not a load actually happened
*/
restoreTabContent: function (aTab) {
restoreTabContent: function (aTab, aLoadArguments = null) {
let window = aTab.ownerDocument.defaultView;
let browser = aTab.linkedBrowser;
let tabData = browser.__SS_data;
@ -2687,7 +2704,8 @@ let SessionStoreInternal = {
browser.__SS_restore_tab = aTab;
browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent");
browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
{loadArguments: aLoadArguments});
},
/**

View File

@ -145,7 +145,7 @@ let MessageListener = {
};
// We need to pass the value of didStartLoad back to SessionStore.jsm.
let didStartLoad = gContentRestore.restoreTabContent(finishCallback);
let didStartLoad = gContentRestore.restoreTabContent(data.loadArguments, finishCallback);
sendAsyncMessage("SessionStore:restoreTabContentStarted", {epoch: epoch});

View File

@ -66,7 +66,6 @@ support-files =
[browser_capabilities.js]
[browser_cleaner.js]
[browser_dying_cache.js]
skip-if = e10s
[browser_dynamic_frames.js]
[browser_form_restore_events.js]
[browser_formdata.js]
@ -77,19 +76,19 @@ skip-if = buildapp == 'mulet'
[browser_frame_history.js]
[browser_global_store.js]
[browser_history_cap.js]
[browser_history_persist.js]
[browser_label_and_icon.js]
[browser_merge_closed_tabs.js]
[browser_pageStyle.js]
[browser_privatetabs.js]
[browser_scrollPositions.js]
[browser_sessionHistory.js]
# Disabled because of bug 1077581
skip-if = e10s
[browser_sessionStorage.js]
skip-if = e10s
[browser_swapDocShells.js]
skip-if = e10s # See bug 918634
[browser_telemetry.js]
skip-if = e10s
[browser_upgrade_backup.js]
[browser_windowRestore_perwindowpb.js]
[browser_248970_b_perwindowpb.js]
@ -107,10 +106,8 @@ skip-if = true
skip-if = true
[browser_394759_behavior.js]
[browser_394759_perwindowpb.js]
skip-if = e10s
[browser_394759_purge.js]
[browser_423132.js]
skip-if = e10s
[browser_447951.js]
[browser_454908.js]
[browser_456342.js]
@ -124,7 +121,6 @@ skip-if = e10s
[browser_467409-backslashplosion.js]
[browser_477657.js]
[browser_480893.js]
skip-if = e10s
[browser_485482.js]
[browser_485563.js]
[browser_490040.js]
@ -197,6 +193,6 @@ skip-if = true
# Disabled on OS X:
[browser_625016.js]
skip-if = os == "mac" || e10s
skip-if = os == "mac"
[browser_911547.js]

View File

@ -25,48 +25,51 @@ function test() {
newWin.addEventListener("load", function (aEvent) {
newWin.removeEventListener("load", arguments.callee, false);
newWin.gBrowser.loadURI(testURL, null, null);
// Wait for sessionstore to be ready to restore this window
executeSoon(function() {
newWin.gBrowser.loadURI(testURL, null, null);
whenBrowserLoaded(newWin.gBrowser.selectedBrowser, function() {
// get the sessionstore state for the window
TabState.flush(newWin.gBrowser.selectedBrowser);
let state = ss.getWindowState(newWin);
whenBrowserLoaded(newWin.gBrowser.selectedBrowser, function() {
// get the sessionstore state for the window
TabState.flush(newWin.gBrowser.selectedBrowser);
let state = ss.getWindowState(newWin);
// verify our cookie got set during pageload
let e = cs.enumerator;
let cookie;
let i = 0;
while (e.hasMoreElements()) {
cookie = e.getNext().QueryInterface(Ci.nsICookie);
i++;
}
is(i, 1, "expected one cookie");
// verify our cookie got set during pageload
let e = cs.enumerator;
let cookie;
let i = 0;
while (e.hasMoreElements()) {
cookie = e.getNext().QueryInterface(Ci.nsICookie);
i++;
}
is(i, 1, "expected one cookie");
// remove the cookie
cs.removeAll();
// remove the cookie
cs.removeAll();
// restore the window state
ss.setWindowState(newWin, state, true);
// restore the window state
ss.setWindowState(newWin, state, true);
// at this point, the cookie should be restored...
e = cs.enumerator;
let cookie2;
while (e.hasMoreElements()) {
cookie2 = e.getNext().QueryInterface(Ci.nsICookie);
if (cookie.name == cookie2.name)
break;
}
is(cookie.name, cookie2.name, "cookie name successfully restored");
is(cookie.value, cookie2.value, "cookie value successfully restored");
is(cookie.path, cookie2.path, "cookie path successfully restored");
// at this point, the cookie should be restored...
e = cs.enumerator;
let cookie2;
while (e.hasMoreElements()) {
cookie2 = e.getNext().QueryInterface(Ci.nsICookie);
if (cookie.name == cookie2.name)
break;
}
is(cookie.name, cookie2.name, "cookie name successfully restored");
is(cookie.value, cookie2.value, "cookie value successfully restored");
is(cookie.path, cookie2.path, "cookie path successfully restored");
// clean up
if (gPrefService.prefHasUserValue("browser.sessionstore.interval"))
gPrefService.clearUserPref("browser.sessionstore.interval");
cs.removeAll();
newWin.close();
finish();
}, true, testURL);
// clean up
if (gPrefService.prefHasUserValue("browser.sessionstore.interval"))
gPrefService.clearUserPref("browser.sessionstore.interval");
cs.removeAll();
newWin.close();
finish();
}, true, testURL);
});
}, false);
}

View File

@ -175,9 +175,9 @@ function testOnWindow(aIsPrivate, aCallback) {
function waitForTabLoad(aWin, aURL, aCallback) {
let browser = aWin.gBrowser.selectedBrowser;
browser.loadURI(aURL);
whenBrowserLoaded(browser, function () {
TabState.flush(browser);
executeSoon(aCallback);
});
browser.loadURI(aURL);
}

View File

@ -0,0 +1,79 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Ensure that history entries that should not be persisted are restored in the
* same state.
*/
add_task(function check_history_not_persisted() {
// Create an about:blank tab
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Retrieve the tab state.
TabState.flush(browser);
let state = JSON.parse(ss.getTabState(tab));
ok(!state.entries[0].persist, "Should have collected the persistence state");
gBrowser.removeTab(tab);
browser = null;
// Open a new tab to restore into.
tab = gBrowser.addTab("about:blank");
browser = tab.linkedBrowser;
yield promiseTabState(tab, state);
let sessionHistory = browser.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:blank", "Should be the right URL");
// Load a new URL into the tab, it should replace the about:blank history entry
browser.loadURI("about:robots");
yield promiseBrowserLoaded(browser);
sessionHistory = browser.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:robots", "Should be the right URL");
// Cleanup.
gBrowser.removeTab(tab);
});
/**
* Check that entries default to being persisted when the attribute doesn't
* exist
*/
add_task(function check_history_default_persisted() {
// Create an about:blank tab
let tab = gBrowser.addTab("about:blank");
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Retrieve the tab state.
TabState.flush(browser);
let state = JSON.parse(ss.getTabState(tab));
delete state.entries[0].persist;
gBrowser.removeTab(tab);
browser = null;
// Open a new tab to restore into.
tab = gBrowser.addTab("about:blank");
browser = tab.linkedBrowser;
yield promiseTabState(tab, state);
let sessionHistory = browser.sessionHistory;
is(sessionHistory.count, 1, "Should be a single history entry");
is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:blank", "Should be the right URL");
// Load a new URL into the tab, it should replace the about:blank history entry
browser.loadURI("about:robots");
yield promiseBrowserLoaded(browser);
sessionHistory = browser.sessionHistory;
is(sessionHistory.count, 2, "Should be two history entries");
is(sessionHistory.getEntryAtIndex(0, false).URI.spec, "about:blank", "Should be the right URL");
is(sessionHistory.getEntryAtIndex(1, false).URI.spec, "about:robots", "Should be the right URL");
// Cleanup.
gBrowser.removeTab(tab);
});

View File

@ -193,6 +193,10 @@ function waitForTabState(aTab, aState, aCallback) {
ss.setTabState(aTab, JSON.stringify(aState));
}
function promiseTabState(tab, state) {
return new Promise(resolve => waitForTabState(tab, state, resolve));
}
/**
* Wait for a content -> chrome message.
*/

View File

@ -74,10 +74,17 @@ let WindowMessageHandler = {
let isImageDocument = (content.document instanceof Ci.nsIImageDocument);
sendAsyncMessage(cx.name, {isImageDocument: isImageDocument});
}
},
waitForDocumentLoad: function WMH_waitForDocumentLoad() {
addEventListener("load", function listener() {
removeEventListener("load", listener, true);
sendAsyncMessage("Panorama:documentLoaded");
}, true);
},
};
// add message listeners
addMessageListener("Panorama:isDocumentLoaded", WindowMessageHandler.isDocumentLoaded);
addMessageListener("Panorama:isImageDocument", WindowMessageHandler.isImageDocument);
addMessageListener("Panorama:waitForDocumentLoad", WindowMessageHandler.waitForDocumentLoad);

View File

@ -17,6 +17,7 @@ support-files =
[browser_tabview_bug587231.js]
skip-if = buildapp == 'mulet'
[browser_tabview_bug587276.js]
skip-if = e10s # Bug 1091200
[browser_tabview_bug587351.js]
[browser_tabview_bug587503.js]
[browser_tabview_bug587990.js]

View File

@ -62,9 +62,13 @@ function test4() {
EventUtils.synthesizeKey("t", { accelKey: true, shiftKey: true }, contentWindow);
is(gBrowser.tabs.length, 2, "There are two tabs after restoring one");
gBrowser.tabs[0].linkedBrowser.loadURI("about:blank");
gBrowser.selectedTab = gBrowser.tabs[0];
test8();
gBrowser.tabs[1].linkedBrowser.addEventListener("load", function listener() {
gBrowser.tabs[1].linkedBrowser.removeEventListener("load", listener, true);
gBrowser.tabs[0].linkedBrowser.loadURI("about:blank");
gBrowser.selectedTab = gBrowser.tabs[0];
test8();
}, true);
});
};
gBrowser.tabContainer.addEventListener("TabClose", onTabClose, true);

View File

@ -797,10 +797,11 @@ let UI = {
if (this.restoredClosedTab) {
// when the tab view UI is being displayed, update the thumb for the
// restored closed tab after the page load
tab.linkedBrowser.addEventListener("load", function onLoad(event) {
tab.linkedBrowser.removeEventListener("load", onLoad, true);
tab.linkedBrowser.messageManager.addMessageListener("Panorama:documentLoaded", function onLoad() {
tab.linkedBrowser.messageManager.removeMessageListener("Panorama:documentLoaded", onLoad);
TabItems._update(tab);
}, true);
});
tab.linkedBrowser.messageManager.sendAsyncMessage("Panorama:waitForDocumentLoad");
}
this._closedLastVisibleTab = false;
this._closedSelectedTabInTabView = false;

View File

@ -18,6 +18,9 @@ XPCOMUtils.defineLazyModuleGetter(this, "promise",
XPCOMUtils.defineLazyModuleGetter(this, "console",
"resource://gre/modules/devtools/Console.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99;
@ -566,6 +569,13 @@ let gDevToolsBrowser = {
let webIDEEnabled = Services.prefs.getBoolPref("devtools.webide.enabled");
toggleCmd("Tools:WebIDE", webIDEEnabled);
let showWebIDEWidget = Services.prefs.getBoolPref("devtools.webide.widget.enabled");
if (webIDEEnabled && showWebIDEWidget) {
gDevToolsBrowser.installWebIDEWidget();
} else {
gDevToolsBrowser.uninstallWebIDEWidget();
}
// Enable App Manager?
let appMgrEnabled = Services.prefs.getBoolPref("devtools.appmanager.enabled");
toggleCmd("Tools:DevAppMgr", !webIDEEnabled && appMgrEnabled);
@ -667,11 +677,59 @@ let gDevToolsBrowser = {
if (win) {
win.focus();
} else {
let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].getService(Ci.nsIWindowWatcher);
ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
Services.ww.openWindow(null, "chrome://webide/content/", "webide", "chrome,centerscreen,resizable", null);
}
},
/**
* Install WebIDE widget
*/
installWebIDEWidget: function() {
if (this.isWebIDEWidgetInstalled()) {
return;
}
let defaultArea;
if (Services.prefs.getBoolPref("devtools.webide.widget.inNavbarByDefault")) {
defaultArea = CustomizableUI.AREA_NAVBAR;
} else {
defaultArea = CustomizableUI.AREA_PANEL;
}
CustomizableUI.createWidget({
id: "webide-button",
shortcutId: "key_webide",
label: "devtools-webide-button2.label",
tooltiptext: "devtools-webide-button2.tooltiptext",
defaultArea: defaultArea,
onCommand: function(aEvent) {
gDevToolsBrowser.openWebIDE();
}
});
},
isWebIDEWidgetInstalled: function() {
let widgetWrapper = CustomizableUI.getWidget("webide-button");
return !!(widgetWrapper && widgetWrapper.instances.some(i => !!i.node));
},
/**
* Uninstall WebIDE widget
*/
uninstallWebIDEWidget: function() {
if (this.isWebIDEWidgetInstalled()) {
CustomizableUI.removeWidgetFromArea("webide-button");
}
CustomizableUI.destroyWidget("webide-button");
},
/**
* Move WebIDE widget to the navbar
*/
moveWebIDEWidgetInNavbar: function() {
CustomizableUI.addWidgetToArea("webide-button", CustomizableUI.AREA_NAVBAR);
},
/**
* Add this DevTools's presence to a browser window's document
*

View File

@ -89,6 +89,12 @@ let UI = {
this.lastConnectedRuntime = Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime");
if (Services.prefs.getBoolPref("devtools.webide.widget.autoinstall") &&
!Services.prefs.getBoolPref("devtools.webide.widget.enabled")) {
Services.prefs.setBoolPref("devtools.webide.widget.enabled", true);
gDevToolsBrowser.moveWebIDEWidgetInNavbar();
}
this.setupDeck();
},

View File

@ -8,3 +8,4 @@ support-files =
[browser_tabs.js]
skip-if = e10s # Bug 1072167 - browser_tabs.js test fails under e10s
[browser_widget.js]

View File

@ -0,0 +1,15 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function test() {
waitForExplicitFinish();
Task.spawn(function() {
let win = yield openWebIDE();
ok(document.querySelector("#webide-button"), "Found WebIDE button");
Services.prefs.setBoolPref("devtools.webide.widget.enabled", false);
ok(!document.querySelector("#webide-button"), "WebIDE button uninstalled");
yield closeWebIDE(win);
Services.prefs.clearUserPref("devtools.webide.widget.enabled");
}).then(finish, handleError);
}

View File

@ -64,6 +64,8 @@ function closeWebIDE(win) {
let deferred = promise.defer();
Services.prefs.clearUserPref("devtools.webide.widget.enabled");
win.addEventListener("unload", function onUnload() {
win.removeEventListener("unload", onUnload);
info("WebIDE closed");

View File

@ -18,3 +18,6 @@ pref("devtools.webide.adaptersAddonID", "fxdevtools-adapters@mozilla.org");
pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
pref("devtools.webide.lastConnectedRuntime", "");
pref("devtools.webide.lastSelectedProject", "");
pref("devtools.webide.widget.autoinstall", true);
pref("devtools.webide.widget.enabled", false);
pref("devtools.webide.widget.inNavbarByDefault", false);

View File

@ -113,5 +113,6 @@ web-apps-button.tooltiptext = Discover Apps
# LOCALIZATION NOTE(devtools-webide-button.label, devtools-webide-button.tooltiptext):
# widget is only visible after WebIDE has been started once (Tools > Web Developers > WebIDE)
devtools-webide-button.label = WebIDE
devtools-webide-button.tooltiptext = Open WebIDE
# %S is the keyboard shortcut
devtools-webide-button2.label = WebIDE
devtools-webide-button2.tooltiptext = Open WebIDE (%S)

View File

@ -51,8 +51,11 @@ this.E10SUtils = {
let sessionHistory = aDocShell.getInterface(Ci.nsIWebNavigation).sessionHistory;
messageManager.sendAsyncMessage("Browser:LoadURI", {
uri: aURI.spec,
referrer: aReferrer ? aReferrer.spec : null,
loadOptions: {
uri: aURI.spec,
flags: Ci.nsIWebNavigation.LOAD_FLAGS_NONE,
referrer: aReferrer ? aReferrer.spec : null,
},
historyIndex: sessionHistory.requestedIndex,
});
return false;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -765,6 +765,10 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
-moz-image-region: rect(18px, 342px, 36px, 324px);
}
#webide-button@toolbarButtonPressed@ {
-moz-image-region: rect(18px, 342px, 36px, 324px);
}
#new-tab-button@toolbarButtonPressed@ {
-moz-image-region: rect(18px, 360px, 36px, 342px);
}
@ -1012,6 +1016,14 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
transform: scaleY(-1);
}
#webide-button[cui-areatype="toolbar"] {
-moz-image-region: rect(0, 1476px, 36px, 1440px);
}
#webide-button[cui-areatype="toolbar"]:hover:active:not([disabled="true"]) {
-moz-image-region: rect(36px, 1476px, 72px, 1440px);
}
#new-tab-button[cui-areatype="toolbar"] {
-moz-image-region: rect(0, 720px, 36px, 684px);
}
@ -1281,6 +1293,11 @@ toolbar .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker > .dropmarker-ic
-moz-image-region: rect(0px, 1024px, 64px, 960px);
}
#webide-button[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > #webide-button {
-moz-image-region: rect(0px, 1920px, 64px, 1856px);
}
#new-tab-button[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > #new-tab-button {
-moz-image-region: rect(0px, 1088px, 64px, 1024px);

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -2,7 +2,7 @@
% Note that zoom-reset-button is a bit different since it doesn't use an image and thus has the image with display: none.
%define nestedButtons #zoom-out-button, #zoom-reset-button, #zoom-in-button, #cut-button, #copy-button, #paste-button
%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #tabview-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@, #e10s-button, #panic-button, #web-apps-button
%define primaryToolbarButtons #back-button, #forward-button, #home-button, #print-button, #downloads-button, #bookmarks-menu-button, #new-tab-button, #new-window-button, #fullscreen-button, #sync-button, #feed-button, #tabview-button, #social-share-button, #open-file-button, #find-button, #developer-button, #preferences-button, #privatebrowsing-button, #save-page-button, #switch-to-metro-button, #add-ons-button, #history-panelmenu, #nav-bar-overflow-button, #PanelUI-menu-button, #characterencoding-button, #email-link-button, #sidebar-button, @nestedButtons@, #e10s-button, #panic-button, #web-apps-button, #webide-button
%ifdef XP_MACOSX
% Prior to 10.7 there wasn't a native fullscreen button so we use #restore-button to exit fullscreen

View File

@ -166,6 +166,11 @@ toolbarpaletteitem[place="palette"] > #web-apps-button {
-moz-image-region: rect(0, 928px, 32px, 896px);
}
#webide-button[cui-areatype="menu-panel"],
toolbarpaletteitem[place="palette"] > #webide-button {
-moz-image-region: rect(0px, 960px, 32px, 928px);
}
toolbaritem[sdkstylewidget="true"] > toolbarbutton {
-moz-image-region: rect(0, 832px, 32px, 800px);
}
@ -243,4 +248,4 @@ toolbarpaletteitem[place="palette"] > #zoom-controls > #zoom-in-button {
#add-share-provider {
list-style-image: url(chrome://browser/skin/menuPanel-small.png);
-moz-image-region: rect(0px, 96px, 16px, 80px);
}
}

View File

@ -233,3 +233,7 @@ toolbar[brighttext] #loop-call-button > .toolbarbutton-badge-container {
#loop-call-button:not([disabled="true"])[state="active"]:-moz-any(:hover,:hover:active,[open]) > .toolbarbutton-badge-container {
-moz-image-region: rect(0, 126px, 18px, 108px);
}
#webide-button[cui-areatype="toolbar"] {
-moz-image-region: rect(0, 738px, 18px, 720px);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -43,8 +43,6 @@ public class ReadingListRow extends LinearLayout {
LayoutInflater.from(context).inflate(R.layout.reading_list_row_view, this);
setOrientation(LinearLayout.VERTICAL);
resources = context.getResources();
title = (TextView) findViewById(R.id.title);

View File

@ -360,10 +360,10 @@ size. -->
<!ENTITY reading_list_added "Page added to your Reading List">
<!-- Localization note (reading_list_time_minutes) : This string is used in the "Reading List"
<!-- Localization note (reading_list_time_minutes2) : This string is used in the "Reading List"
panel on the home page to give the user an estimate of how many minutes it will take to
read an article. The word "minute" should be abbreviated if possible. -->
<!ENTITY reading_list_time_minutes "&formatD;min">
<!ENTITY reading_list_time_minutes2 "&formatD; min">
<!ENTITY reading_list_time_over_an_hour "Over an hour">
<!-- Localization note : These strings are used as alternate text for accessibility.

View File

@ -4,7 +4,6 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<org.mozilla.gecko.home.ReadingListRow xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/Widget.BookmarkItemView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"/>
android:layout_height="@dimen/reading_list_row_height"
android:layout_gravity="center_vertical"/>

View File

@ -6,29 +6,34 @@
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1"
android:paddingLeft="@dimen/reading_list_row_padding_left"
android:paddingRight="@dimen/reading_list_row_padding_right"
android:orientation="vertical"
android:gravity="center_vertical">
<TextView
android:id="@+id/title"
android:layout_width="0dip"
android:layout_height="match_parent"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
style="@style/Widget.ReadingListRow.Title" />
<TextView
android:id="@+id/read_time"
android:layout_width="wrap_content"
android:layout_height="match_parent"
style="@style/Widget.ReadingListRow.ReadTime" />
android:id="@+id/excerpt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.ReadingListRow.Description" />
</LinearLayout>
<TextView
android:id="@+id/excerpt"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.ReadingListRow.Description" />
android:id="@+id/read_time"
android:layout_width="64dp"
android:layout_height="match_parent"
android:gravity="center"
style="@style/Widget.ReadingListRow.ReadTime" />
</merge>

View File

@ -11,4 +11,7 @@
<dimen name="tabs_counter_size">26sp</dimen>
<dimen name="panel_grid_view_column_width">200dp</dimen>
<dimen name="reading_list_row_height">96dp</dimen>
<dimen name="reading_list_row_padding_right">15dp</dimen>
</resources>

View File

@ -132,6 +132,13 @@
<style name="Widget.ReadingListView" parent="Widget.BookmarksListView"/>
<style name="Widget.ReadingListRow.Description">
<item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
<item name="android:maxLines">2</item>
<item name="android:ellipsize">end</item>
<item name="android:lineSpacingMultiplier">1.3</item>
</style>
<style name="Widget.HomeBanner">
<item name="android:paddingLeft">32dp</item>
<item name="android:paddingRight">32dp</item>

View File

@ -17,12 +17,6 @@
<item name="android:fontFamily">sans-serif-light</item>
</style>
<style name="Widget.ReadingListRow.ReadTime">
<item name="android:textStyle">italic</item>
<item name="android:textColor">#FF9400</item>
<item name="android:fontFamily">sans-serif-condensed</item>
</style>
<style name="OnboardStartTextAppearance.Subtext">
<item name="android:textSize">18sp</item>
<item name="android:fontFamily">sans-serif-light</item>

View File

@ -61,9 +61,14 @@
<dimen name="new_tablet_site_security_unknown_inset_top">1dp</dimen>
<dimen name="new_tablet_site_security_unknown_inset_bottom">-1dp</dimen>
<!-- Page Row height -->
<!-- Regular page row on about:home -->
<dimen name="page_row_height">64dp</dimen>
<!-- Reading list row on about:home -->
<dimen name="reading_list_row_height">128dp</dimen>
<dimen name="reading_list_row_padding_left">15dp</dimen>
<dimen name="reading_list_row_padding_right">10dp</dimen>
<!-- Remote Tabs static view top padding. Less in landscape on phones. -->
<dimen name="home_remote_tabs_top_padding">48dp</dimen>

View File

@ -137,12 +137,12 @@
<style name="Widget.ReadingListRow.Description">
<item name="android:textAppearance">@style/TextAppearance.Widget.Home.ItemDescription</item>
<item name="android:maxLines">4</item>
<item name="android:maxLines">3</item>
<item name="android:ellipsize">end</item>
<item name="android:lineSpacingMultiplier">1.3</item>
</style>
<style name="Widget.ReadingListRow.ReadTime">
<item name="android:textStyle">italic</item>
<item name="android:textColor">@color/text_color_highlight</item>
</style>

View File

@ -294,7 +294,7 @@
<string name="site_settings_no_settings">&site_settings_no_settings;</string>
<string name="reading_list_added">&reading_list_added;</string>
<string name="reading_list_time_minutes">&reading_list_time_minutes;</string>
<string name="reading_list_time_minutes">&reading_list_time_minutes2;</string>
<string name="reading_list_time_over_an_hour">&reading_list_time_over_an_hour;</string>
<string name="page_action_dropmarker_description">&page_action_dropmarker_description;</string>

View File

@ -4,11 +4,9 @@
# disabled on Android 2.3; bug 975187
skip-if = android_version == "10"
[testAddonManager]
# disabled on x86 only; bug 936216
#skip-if = processor == "x86"
# disabled across the board - bug 941624, bug 1063509, bug 1073374, bug 1087221,
# bug 1088023, bug 1088027, bug 1090206
skip-if = true
# disabled on x86; bug 936216
# disabled on 2.3; bug 941624, bug 1063509, bug 1073374, bug 1087221, bug 1088023, bug 1088027, bug 1090206
skip-if = android_version == "10" || processor == "x86"
[testAddSearchEngine]
# disabled on Android 2.3; bug 979552
skip-if = android_version == "10"

View File

@ -252,6 +252,7 @@ user_pref("loop.enabled", true);
user_pref("loop.throttled", false);
user_pref("loop.oauth.google.URL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=");
user_pref("loop.oauth.google.getContactsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=contacts");
user_pref("loop.oauth.google.getGroupsURL", "http://%(server)s/browser/browser/components/loop/test/mochitest/google_service.sjs?action=groups");
user_pref("loop.CSP","default-src 'self' about: file: chrome: data: wss://* http://* https://*");
// Ensure UITour won't hit the network

View File

@ -133,14 +133,6 @@
if (!aURI)
aURI = "about:blank";
if (aCharset) {
try {
this.docShell.parentCharset = aCharset;
}
catch (e) {
}
}
if (!(aFlags & this.webNavigation.LOAD_FLAGS_FROM_EXTERNAL))
this.userTypedClear++;