merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2014-12-18 13:21:40 +01:00
commit 6b6444d657
55 changed files with 1626 additions and 330 deletions

View File

@ -1663,6 +1663,7 @@ pref("loop.server", "https://loop.services.mozilla.com/v0");
pref("loop.seenToS", "unseen"); pref("loop.seenToS", "unseen");
pref("loop.gettingStarted.seen", false); pref("loop.gettingStarted.seen", false);
pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start"); pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start");
pref("loop.gettingStarted.resumeOnFirstJoin", false);
pref("loop.learnMoreUrl", "https://www.firefox.com/hello/"); pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/"); pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/"); pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/");

View File

@ -331,7 +331,9 @@ let gFxAccounts = {
fxaMigrator.createFxAccount(window); fxaMigrator.createFxAccount(window);
break; break;
case "migrate-verify": case "migrate-verify":
fxaMigrator.resendVerificationMail(); // Instead of using the migrator module directly here the UX calls for
// us to open prefs which has a "resend" button.
this.openPreferences();
break; break;
default: default:
this.openAccountsPage(null, { entryPoint: "menupanel" }); this.openAccountsPage(null, { entryPoint: "menupanel" });

View File

@ -6,6 +6,7 @@
let LoopUI; let LoopUI;
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
@ -84,8 +85,70 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel
// Used to clear the temporary "login" state from the button. // Used to clear the temporary "login" state from the button.
Services.obs.notifyObservers(null, "loop-status-changed", null); Services.obs.notifyObservers(null, "loop-status-changed", null);
PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node, this.shouldResumeTour().then((resume) => {
"loop", null, "about:looppanel", null, callback); if (resume) {
// Assume the conversation with the visitor wasn't open since we would
// have resumed the tour as soon as the visitor joined if it was (and
// the pref would have been set to false already.
MozLoopService.resumeTour("waiting");
resolve();
return;
}
PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
"loop", null, "about:looppanel", null, callback);
});
});
},
/**
* Method to know whether actions to open the panel should instead resume the tour.
*
* We need the panel to be opened via UITour so that it gets @noautohide.
*
* @return {Promise} resolving with a {Boolean} of whether the tour should be resumed instead of
* opening the panel.
*/
shouldResumeTour: Task.async(function* () {
// Resume the FTU tour if this is the first time a room was joined by
// someone else since the tour.
if (!Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin")) {
return false;
}
if (!LoopRooms.participantsCount) {
// Nobody is in the rooms
return false;
}
let roomsWithNonOwners = yield this.roomsWithNonOwners();
if (!roomsWithNonOwners.length) {
// We were the only one in a room but we want to know about someone else joining.
return false;
}
return true;
}),
/**
* @return {Promise} resolved with an array of Rooms with participants (excluding owners)
*/
roomsWithNonOwners: function() {
return new Promise(resolve => {
LoopRooms.getAll((error, rooms) => {
let roomsWithNonOwners = [];
for (let room of rooms) {
if (!("participants" in room)) {
continue;
}
let numNonOwners = room.participants.filter(participant => !participant.owner).length;
if (!numNonOwners) {
continue;
}
roomsWithNonOwners.push(room);
}
resolve(roomsWithNonOwners);
});
}); });
}, },

View File

@ -7476,6 +7476,7 @@ function switchToTabHavingURI(aURI, aOpenNew, aOpenParams={}) {
// Certain URLs can be switched to irrespective of the source or destination // Certain URLs can be switched to irrespective of the source or destination
// window being in private browsing mode: // window being in private browsing mode:
const kPrivateBrowsingWhitelist = new Set([ const kPrivateBrowsingWhitelist = new Set([
"about:addons",
"about:customizing", "about:customizing",
]); ]);
@ -7737,7 +7738,8 @@ XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() {
}); });
function openEyedropper() { function openEyedropper() {
var eyedropper = new this.Eyedropper(this); var eyedropper = new this.Eyedropper(this, { context: "menu",
copyOnSelect: true });
eyedropper.open(); eyedropper.open();
} }

View File

@ -486,3 +486,4 @@ skip-if = e10s # bug 1100687 - test directly manipulates content (content.docume
[browser_mcb_redirect.js] [browser_mcb_redirect.js]
skip-if = e10s # bug 1084504 - [e10s] Mixed content detection does not take redirection into account skip-if = e10s # bug 1084504 - [e10s] Mixed content detection does not take redirection into account
[browser_windowactivation.js] [browser_windowactivation.js]
[browser_bug963945.js]

View File

@ -0,0 +1,30 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/*
* This test ensures the about:addons tab is only
* opened one time when in private browsing.
*/
function test() {
waitForExplicitFinish();
var win = OpenBrowserWindow({private: true});
whenDelayedStartupFinished(win, function() {
win.gBrowser.loadURI("about:addons");
waitForFocus(function() {
EventUtils.synthesizeKey("a", { ctrlKey: true, shiftKey: true }, win);
is(win.gBrowser.tabs.length, 1, "about:addons tab was re-focused.");
is(win.gBrowser.currentURI.spec, "about:addons", "Addons tab was opened.");
win.close();
finish();
});
});
}

View File

@ -939,12 +939,24 @@
this.closePopup(); this.closePopup();
controller.handleEscape(); controller.handleEscape();
// Fill in the search bar's value
searchBar.value = search;
// open the search results according to the clicking subtlety // open the search results according to the clicking subtlety
var where = whereToOpenLink(aEvent, false, true); var where = whereToOpenLink(aEvent, false, true);
// But open ctrl/cmd clicks on autocomplete items in a new background tab.
if (where == "tab" && (aEvent instanceof MouseEvent) &&
(aEvent.button == 1 ||
#ifdef XP_MACOSX
aEvent.metaKey))
#else
aEvent.ctrlKey))
#endif
where = "tab-background";
searchBar.doSearch(search, where); searchBar.doSearch(search, where);
if (where == "tab-background")
searchBar.focus();
else
searchBar.value = search;
} }
]]></body> ]]></body>
</method> </method>

View File

@ -155,7 +155,7 @@ let LoopRoomsInternal = {
* `Error` object or `null`. The second argument will * `Error` object or `null`. The second argument will
* be the list of rooms, if it was fetched successfully. * be the list of rooms, if it was fetched successfully.
*/ */
getAll: function(version = null, callback) { getAll: function(version = null, callback = null) {
if (!callback) { if (!callback) {
callback = version; callback = version;
version = null; version = null;

View File

@ -1099,6 +1099,33 @@ this.MozLoopService = {
} }
}); });
// Resume the tour (re-opening the tab, if necessary) if someone else joins
// a room of ours and it's currently open.
LoopRooms.on("joined", (e, room, participant) => {
let isOwnerInRoom = false;
let isOtherInRoom = false;
if (!room.participants) {
return;
}
// The particpant that joined isn't necessarily included in room.participants (depending on
// when the broadcast happens) so concatenate.
for (let participant of room.participants.concat(participant)) {
if (participant.owner) {
isOwnerInRoom = true;
} else {
isOtherInRoom = true;
}
}
if (!isOwnerInRoom || !isOtherInRoom) {
return;
}
this.resumeTour("open");
});
// If expiresTime is not in the future and the user hasn't // If expiresTime is not in the future and the user hasn't
// previously authenticated then skip registration. // previously authenticated then skip registration.
if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() && if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
@ -1476,23 +1503,64 @@ this.MozLoopService = {
} }
}), }),
/**
* Gets the tour URL.
*
* @param {String} aSrc A string representing the entry point to begin the tour, optional.
* @param {Object} aAdditionalParams An object with keys used as query parameter names
*/
getTourURL: function(aSrc = null, aAdditionalParams = {}) {
let urlStr = this.getLoopPref("gettingStarted.url");
let url = new URL(Services.urlFormatter.formatURL(urlStr));
for (let paramName in aAdditionalParams) {
url.searchParams.append(paramName, aAdditionalParams[paramName]);
}
if (aSrc) {
url.searchParams.set("utm_source", "firefox-browser");
url.searchParams.set("utm_medium", "firefox-browser");
url.searchParams.set("utm_campaign", aSrc);
}
return url;
},
resumeTour: function(aIncomingConversationState) {
let url = this.getTourURL("resume-with-conversation", {
incomingConversation: aIncomingConversationState,
});
let win = Services.wm.getMostRecentWindow("navigator:browser");
this.setLoopPref("gettingStarted.resumeOnFirstJoin", false);
// The query parameters of the url can vary but we always want to re-use a Loop tour tab that's
// already open so we ignore the fragment and query string.
let hadExistingTab = win.switchToTabHavingURI(url, true, {
ignoreFragment: true,
ignoreQueryString: true,
});
// If the tab was already open, send an event instead of using the query
// parameter above (that we don't replace on existing tabs to avoid a reload).
if (hadExistingTab) {
UITour.notify("Loop:IncomingConversation", {
conversationOpen: aIncomingConversationState === "open",
});
}
},
/** /**
* Opens the Getting Started tour in the browser. * Opens the Getting Started tour in the browser.
* *
* @param {String} aSrc * @param {String} [aSrc] A string representing the entry point to begin the tour, optional.
* - The UI element that the user used to begin the tour, optional.
*/ */
openGettingStartedTour: Task.async(function(aSrc = null) { openGettingStartedTour: Task.async(function(aSrc = null) {
try { try {
let urlStr = Services.prefs.getCharPref("loop.gettingStarted.url"); let url = this.getTourURL(aSrc);
let url = new URL(Services.urlFormatter.formatURL(urlStr));
if (aSrc) {
url.searchParams.set("utm_source", "firefox-browser");
url.searchParams.set("utm_medium", "firefox-browser");
url.searchParams.set("utm_campaign", aSrc);
}
let win = Services.wm.getMostRecentWindow("navigator:browser"); let win = Services.wm.getMostRecentWindow("navigator:browser");
win.switchToTabHavingURI(url, true, {replaceQueryString: true}); win.switchToTabHavingURI(url, true, {
ignoreFragment: true,
replaceQueryString: true,
});
} catch (ex) { } catch (ex) {
log.error("Error opening Getting Started tour", ex); log.error("Error opening Getting Started tour", ex);
} }

View File

@ -166,11 +166,14 @@ loop.panel = (function(_, mozL10n) {
}); });
var GettingStartedView = React.createClass({displayName: 'GettingStartedView', var GettingStartedView = React.createClass({displayName: 'GettingStartedView',
mixins: [sharedMixins.WindowCloseMixin],
handleButtonClick: function() { handleButtonClick: function() {
navigator.mozLoop.openGettingStartedTour("getting-started"); navigator.mozLoop.openGettingStartedTour("getting-started");
navigator.mozLoop.setLoopPref("gettingStarted.seen", true); navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
var event = new CustomEvent("GettingStartedSeen"); var event = new CustomEvent("GettingStartedSeen");
window.dispatchEvent(event); window.dispatchEvent(event);
this.closeWindow();
}, },
render: function() { render: function() {
@ -269,7 +272,7 @@ loop.panel = (function(_, mozL10n) {
* Panel settings (gear) menu. * Panel settings (gear) menu.
*/ */
var SettingsDropdown = React.createClass({displayName: 'SettingsDropdown', var SettingsDropdown = React.createClass({displayName: 'SettingsDropdown',
mixins: [sharedMixins.DropdownMenuMixin], mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
handleClickSettingsEntry: function() { handleClickSettingsEntry: function() {
// XXX to be implemented at the same time as unhiding the entry // XXX to be implemented at the same time as unhiding the entry
@ -300,6 +303,7 @@ loop.panel = (function(_, mozL10n) {
openGettingStartedTour: function() { openGettingStartedTour: function() {
navigator.mozLoop.openGettingStartedTour("settings-menu"); navigator.mozLoop.openGettingStartedTour("settings-menu");
this.closeWindow();
}, },
render: function() { render: function() {

View File

@ -166,11 +166,14 @@ loop.panel = (function(_, mozL10n) {
}); });
var GettingStartedView = React.createClass({ var GettingStartedView = React.createClass({
mixins: [sharedMixins.WindowCloseMixin],
handleButtonClick: function() { handleButtonClick: function() {
navigator.mozLoop.openGettingStartedTour("getting-started"); navigator.mozLoop.openGettingStartedTour("getting-started");
navigator.mozLoop.setLoopPref("gettingStarted.seen", true); navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
var event = new CustomEvent("GettingStartedSeen"); var event = new CustomEvent("GettingStartedSeen");
window.dispatchEvent(event); window.dispatchEvent(event);
this.closeWindow();
}, },
render: function() { render: function() {
@ -269,7 +272,7 @@ loop.panel = (function(_, mozL10n) {
* Panel settings (gear) menu. * Panel settings (gear) menu.
*/ */
var SettingsDropdown = React.createClass({ var SettingsDropdown = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin], mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
handleClickSettingsEntry: function() { handleClickSettingsEntry: function() {
// XXX to be implemented at the same time as unhiding the entry // XXX to be implemented at the same time as unhiding the entry
@ -300,6 +303,7 @@ loop.panel = (function(_, mozL10n) {
openGettingStartedTour: function() { openGettingStartedTour: function() {
navigator.mozLoop.openGettingStartedTour("settings-menu"); navigator.mozLoop.openGettingStartedTour("settings-menu");
this.closeWindow();
}, },
render: function() { render: function() {

View File

@ -89,6 +89,9 @@ let gSubDialog = {
this._overlay.style.visibility = ""; this._overlay.style.visibility = "";
// Clear the sizing inline styles. // Clear the sizing inline styles.
this._frame.removeAttribute("style"); this._frame.removeAttribute("style");
// Clear the sizing attributes
this._box.removeAttribute("width");
this._box.removeAttribute("height");
setTimeout(() => { setTimeout(() => {
// Unload the dialog after the event listeners run so that the load of about:blank isn't // Unload the dialog after the event listeners run so that the load of about:blank isn't

View File

@ -9,6 +9,12 @@ XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {}); return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
}); });
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxaMigrator",
"resource://services-sync/FxaMigrator.jsm");
const PAGE_NO_ACCOUNT = 0; const PAGE_NO_ACCOUNT = 0;
const PAGE_HAS_ACCOUNT = 1; const PAGE_HAS_ACCOUNT = 1;
const PAGE_NEEDS_UPDATE = 2; const PAGE_NEEDS_UPDATE = 2;
@ -25,7 +31,6 @@ const FXA_LOGIN_UNVERIFIED = 1;
const FXA_LOGIN_FAILED = 2; const FXA_LOGIN_FAILED = 2;
let gSyncPane = { let gSyncPane = {
_stringBundle: null,
prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs", prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
"engine.tabs", "engine.history"], "engine.tabs", "engine.history"],
@ -91,6 +96,7 @@ let gSyncPane = {
"weave:service:setup-complete", "weave:service:setup-complete",
"weave:service:logout:finish", "weave:service:logout:finish",
FxAccountsCommon.ONVERIFIED_NOTIFICATION]; FxAccountsCommon.ONVERIFIED_NOTIFICATION];
let migrateTopic = "fxa-migration:state-changed";
// Add the observers now and remove them on unload // Add the observers now and remove them on unload
//XXXzpao This should use Services.obs.* but Weave's Obs does nice handling //XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
@ -98,14 +104,30 @@ let gSyncPane = {
topics.forEach(function (topic) { topics.forEach(function (topic) {
Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this); Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this);
}, this); }, this);
// The FxA migration observer is a special case.
Weave.Svc.Obs.add(migrateTopic, this.updateMigrationState, this);
window.addEventListener("unload", function() { window.addEventListener("unload", function() {
topics.forEach(function (topic) { topics.forEach(function (topic) {
Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this); Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this);
}, gSyncPane); }, gSyncPane);
Weave.Svc.Obs.remove(migrateTopic, gSyncPane.updateMigrationState, gSyncPane);
}, false); }, false);
this._stringBundle = // ask the migration module to broadcast its current state (and nothing will
Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties"); // happen if it's not loaded - which is good, as that means no migration
// is pending/necessary) - we don't want to suck that module in just to
// find there's nothing to do.
Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => {
return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
}),
XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => {
return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
}),
this.updateWeavePrefs(); this.updateWeavePrefs();
}, },
@ -192,6 +214,17 @@ let gSyncPane = {
}); });
setEventListener("tosPP-small-ToS", "click", gSyncPane.openToS); setEventListener("tosPP-small-ToS", "click", gSyncPane.openToS);
setEventListener("tosPP-small-PP", "click", gSyncPane.openPrivacyPolicy); setEventListener("tosPP-small-PP", "click", gSyncPane.openPrivacyPolicy);
setEventListener("sync-migrate-upgrade", "click", function () {
let win = Services.wm.getMostRecentWindow("navigator:browser");
fxaMigrator.createFxAccount(win);
});
setEventListener("sync-migrate-forget", "click", function () {
fxaMigrator.forgetFxAccount();
});
setEventListener("sync-migrate-resend", "click", function () {
let win = Services.wm.getMostRecentWindow("navigator:browser");
fxaMigrator.resendVerificationMail(win);
});
}, },
updateWeavePrefs: function () { updateWeavePrefs: function () {
@ -203,7 +236,6 @@ let gSyncPane = {
if (service.fxAccountsEnabled) { if (service.fxAccountsEnabled) {
// determine the fxa status... // determine the fxa status...
this.page = PAGE_PLEASE_WAIT; this.page = PAGE_PLEASE_WAIT;
Components.utils.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.getSignedInUser().then(data => { fxAccounts.getSignedInUser().then(data => {
if (!data) { if (!data) {
this.page = FXA_PAGE_LOGGED_OUT; this.page = FXA_PAGE_LOGGED_OUT;
@ -262,6 +294,45 @@ let gSyncPane = {
} }
}, },
updateMigrationState: function(subject, state) {
let selIndex;
switch (state) {
case fxaMigrator.STATE_USER_FXA: {
let sb = this._accountsStringBundle;
let button = document.getElementById("sync-migrate-upgrade");
button.setAttribute("label", sb.GetStringFromName("upgradeToFxA.label"));
button.setAttribute("accesskey", sb.GetStringFromName("upgradeToFxA.accessKey"));
selIndex = 0;
break;
}
case fxaMigrator.STATE_USER_FXA_VERIFIED: {
let sb = this._accountsStringBundle;
let email = subject.QueryInterface(Components.interfaces.nsISupportsString).data;
let label = sb.formatStringFromName("needVerifiedUserLong", [email], 1);
let elt = document.getElementById("sync-migrate-verify-label");
elt.setAttribute("value", label);
// The "resend" button.
let button = document.getElementById("sync-migrate-resend");
button.setAttribute("label", sb.GetStringFromName("resendVerificationEmail.label"));
button.setAttribute("accesskey", sb.GetStringFromName("resendVerificationEmail.accessKey"));
// The "forget" button.
button = document.getElementById("sync-migrate-forget");
button.setAttribute("label", sb.GetStringFromName("forgetMigration.label"));
button.setAttribute("accesskey", sb.GetStringFromName("forgetMigration.accessKey"));
selIndex = 1;
break;
}
default:
if (state) { // |null| is expected, but everything else is not.
Cu.reportError("updateMigrationState has unknown state: " + state);
}
document.getElementById("sync-migration").hidden = true;
return;
}
document.getElementById("sync-migration").hidden = false;
document.getElementById("sync-migration-deck").selectedIndex = selIndex;
},
startOver: function (showDialog) { startOver: function (showDialog) {
if (showDialog) { if (showDialog) {
let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING + let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
@ -375,14 +446,13 @@ let gSyncPane = {
}, },
verifyFirefoxAccount: function() { verifyFirefoxAccount: function() {
Components.utils.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.resendVerificationEmail().then(() => { fxAccounts.resendVerificationEmail().then(() => {
fxAccounts.getSignedInUser().then(data => { fxAccounts.getSignedInUser().then(data => {
let sb = this._stringBundle; let sb = this._accountsStringBundle;
let title = sb.GetStringFromName("firefoxAccountsVerificationSentTitle"); let title = sb.GetStringFromName("verificationSentTitle");
let heading = sb.formatStringFromName("firefoxAccountsVerificationSentHeading", let heading = sb.formatStringFromName("verificationSentHeading",
[data.email], 1); [data.email], 1);
let description = sb.GetStringFromName("firefoxAccountVerificationSentDescription"); let description = sb.GetStringFromName("verificationSentDescription");
let factory = Cc["@mozilla.org/prompter;1"] let factory = Cc["@mozilla.org/prompter;1"]
.getService(Ci.nsIPromptFactory); .getService(Ci.nsIPromptFactory);
@ -430,7 +500,6 @@ let gSyncPane = {
return; return;
} }
} }
Cu.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.signOut().then(() => { fxAccounts.signOut().then(() => {
this.updateWeavePrefs(); this.updateWeavePrefs();
}); });

View File

@ -37,6 +37,31 @@
<label class="header-name">&paneSync.title;</label> <label class="header-name">&paneSync.title;</label>
</hbox> </hbox>
<hbox id="sync-migration-container"
data-category="paneSync"
hidden="true">
<vbox id="sync-migration" flex="1" hidden="true">
<deck id="sync-migration-deck">
<!-- When we are in the "need FxA user" state -->
<hbox align="center">
<label>&migrate.upgradeNeeded;</label>
<spacer flex="1"/>
<button id="sync-migrate-upgrade"/>
</hbox>
<!-- When we are in the "need the user to be verified" state -->
<hbox align="center">
<label id="sync-migrate-verify-label"/>
<spacer flex="1"/>
<button id="sync-migrate-forget"/>
<button id="sync-migrate-resend"/>
</hbox>
</deck>
</vbox>
</hbox>
<deck id="weavePrefsDeck" data-category="paneSync" hidden="true"> <deck id="weavePrefsDeck" data-category="paneSync" hidden="true">
<!-- These panels are for the "legacy" sync provider --> <!-- These panels are for the "legacy" sync provider -->
<vbox id="noAccount" align="center"> <vbox id="noAccount" align="center">

View File

@ -285,11 +285,11 @@ let gSyncPane = {
Components.utils.import("resource://gre/modules/FxAccounts.jsm"); Components.utils.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.resendVerificationEmail().then(() => { fxAccounts.resendVerificationEmail().then(() => {
fxAccounts.getSignedInUser().then(data => { fxAccounts.getSignedInUser().then(data => {
let sb = this._stringBundle; let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
let title = sb.GetStringFromName("firefoxAccountsVerificationSentTitle"); let title = sb.GetStringFromName("verificationSentTitle");
let heading = sb.formatStringFromName("firefoxAccountsVerificationSentHeading", let heading = sb.formatStringFromName("verificationSentHeading",
[data.email], 1); [data.email], 1);
let description = sb.GetStringFromName("firefoxAccountVerificationSentDescription"); let description = sb.GetStringFromName("verificationSentDescription");
Services.prompt.alert(window, title, heading + "\n\n" + description); Services.prompt.alert(window, title, heading + "\n\n" + description);
}); });

View File

@ -472,6 +472,8 @@
var textValue = textBox.value; var textValue = textBox.value;
var where = "current"; var where = "current";
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") { if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
if (aEvent.button == 2) if (aEvent.button == 2)
return; return;
@ -479,11 +481,20 @@
} }
else { else {
var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab"); var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab");
if ((aEvent && aEvent.altKey) ^ newTabPref) if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
where = "tab"; where = "tab";
if ((aEvent instanceof MouseEvent) && (aEvent.button == 1 ||
#ifdef XP_MACOSX
aEvent.metaKey))
#else
aEvent.ctrlKey))
#endif
where = "tab-background";
} }
this.doSearch(textValue, where, aEngine); this.doSearch(textValue, where, aEngine);
if (where == "tab-background")
this.focus();
]]></body> ]]></body>
</method> </method>
@ -509,7 +520,13 @@
var submission = engine.getSubmission(aData, null, "searchbar"); var submission = engine.getSubmission(aData, null, "searchbar");
BrowserSearch.recordSearchInHealthReport(engine, "searchbar"); BrowserSearch.recordSearchInHealthReport(engine, "searchbar");
// null parameter below specifies HTML response for search // null parameter below specifies HTML response for search
openUILinkIn(submission.uri.spec, aWhere, null, submission.postData); let params = {
postData: submission.postData,
inBackground: aWhere == "tab-background"
};
openUILinkIn(submission.uri.spec,
aWhere == "tab-background" ? "tab" : aWhere,
params);
]]></body> ]]></body>
</method> </method>
</implementation> </implementation>

View File

@ -38,7 +38,9 @@ exports.items = [{
let chromeWindow = context.environment.chromeWindow; let chromeWindow = context.environment.chromeWindow;
let target = context.environment.target; let target = context.environment.target;
let dropper = EyedropperManager.createInstance(chromeWindow); let dropper = EyedropperManager.createInstance(chromeWindow,
{ context: "command",
copyOnSelect: true });
dropper.open(); dropper.open();
eventEmitter.emit("changed", { target: target }); eventEmitter.emit("changed", { target: target });

View File

@ -4,6 +4,7 @@
const {Cc, Ci, Cu} = require("chrome"); const {Cc, Ci, Cu} = require("chrome");
const {rgbToHsl} = require("devtools/css-color").colorUtils; const {rgbToHsl} = require("devtools/css-color").colorUtils;
const Telemetry = require("devtools/shared/telemetry");
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js"); const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {}); const {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
@ -61,13 +62,13 @@ let EyedropperManager = {
return this._instances.get(chromeWindow); return this._instances.get(chromeWindow);
}, },
createInstance: function(chromeWindow) { createInstance: function(chromeWindow, options) {
let dropper = this.getInstance(chromeWindow); let dropper = this.getInstance(chromeWindow);
if (dropper) { if (dropper) {
return dropper; return dropper;
} }
dropper = new Eyedropper(chromeWindow); dropper = new Eyedropper(chromeWindow, options);
this._instances.set(chromeWindow, dropper); this._instances.set(chromeWindow, dropper);
dropper.on("destroy", () => { dropper.on("destroy", () => {
@ -100,9 +101,9 @@ exports.EyedropperManager = EyedropperManager;
* @param {DOMWindow} chromeWindow * @param {DOMWindow} chromeWindow
* window to inspect * window to inspect
* @param {object} opts * @param {object} opts
* optional options object, with 'copyOnSelect' * optional options object, with 'copyOnSelect', 'context'
*/ */
function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) { function Eyedropper(chromeWindow, opts = { copyOnSelect: true, context: "other" }) {
this.copyOnSelect = opts.copyOnSelect; this.copyOnSelect = opts.copyOnSelect;
this._onFirstMouseMove = this._onFirstMouseMove.bind(this); this._onFirstMouseMove = this._onFirstMouseMove.bind(this);
@ -134,6 +135,18 @@ function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
let mm = this._contentTab.linkedBrowser.messageManager; let mm = this._contentTab.linkedBrowser.messageManager;
mm.loadFrameScript("resource:///modules/devtools/eyedropper/eyedropper-child.js", true); mm.loadFrameScript("resource:///modules/devtools/eyedropper/eyedropper-child.js", true);
// record if this was opened via the picker or standalone
var telemetry = new Telemetry();
if (opts.context == "command") {
telemetry.toolOpened("eyedropper");
}
else if (opts.context == "menu") {
telemetry.toolOpened("menueyedropper");
}
else if (opts.context == "picker") {
telemetry.toolOpened("pickereyedropper");
}
EventEmitter.decorate(this); EventEmitter.decorate(this);
} }

View File

@ -16,7 +16,7 @@ Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
const MAX_LABEL_LENGTH = 40; const MAX_LABEL_LENGTH = 40;
let promise = require("devtools/toolkit/deprecated-sync-thenables"); let promise = require("resource://gre/modules/Promise.jsm").Promise;
const LOW_PRIORITY_ELEMENTS = { const LOW_PRIORITY_ELEMENTS = {
"HEAD": true, "HEAD": true,
@ -138,13 +138,14 @@ HTMLBreadcrumbs.prototype = {
}, },
/** /**
* Print any errors (except selection guard errors). * Warn if rejection was caused by selection change, print an error otherwise.
*/ */
selectionGuardEnd: function(err) { selectionGuardEnd: function(err) {
if (err != "selection-changed") { if (err === "selection-changed") {
console.warn("Asynchronous operation was aborted as selection changed.");
} else {
console.error(err); console.error(err);
} }
promise.reject(err);
}, },
/** /**

View File

@ -8,7 +8,7 @@ const {Cc, Ci, Cu, Cr} = require("chrome");
Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/Services.jsm");
let promise = require("devtools/toolkit/deprecated-sync-thenables"); let promise = require("resource://gre/modules/Promise.jsm").Promise;
let EventEmitter = require("devtools/toolkit/event-emitter"); let EventEmitter = require("devtools/toolkit/event-emitter");
let clipboard = require("sdk/clipboard"); let clipboard = require("sdk/clipboard");

View File

@ -4,7 +4,7 @@
"use strict"; "use strict";
const promise = require("devtools/toolkit/deprecated-sync-thenables"); const promise = require("resource://gre/modules/Promise.jsm").Promise;
loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter")); loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup); loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);

View File

@ -170,6 +170,18 @@ Telemetry.prototype = {
userHistogram: "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG", userHistogram: "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS" timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
}, },
eyedropper: {
histogram: "DEVTOOLS_EYEDROPPER_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_EYEDROPPER_OPENED_PER_USER_FLAG",
},
menueyedropper: {
histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_PER_USER_FLAG",
},
pickereyedropper: {
histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG",
},
developertoolbar: { developertoolbar: {
histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN", histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG", userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",

View File

@ -50,6 +50,7 @@ support-files =
[browser_tableWidget_keyboard_interaction.js] [browser_tableWidget_keyboard_interaction.js]
[browser_tableWidget_mouse_interaction.js] [browser_tableWidget_mouse_interaction.js]
skip-if = buildapp == 'mulet' skip-if = buildapp == 'mulet'
[browser_telemetry_button_eyedropper.js]
[browser_telemetry_button_paintflashing.js] [browser_telemetry_button_paintflashing.js]
[browser_telemetry_button_responsive.js] [browser_telemetry_button_responsive.js]
[browser_telemetry_button_scratchpad.js] [browser_telemetry_button_scratchpad.js]

View File

@ -0,0 +1,110 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
let Telemetry = require("devtools/shared/telemetry");
let { EyedropperManager } = require("devtools/eyedropper/eyedropper");
function init() {
Telemetry.prototype.telemetryInfo = {};
Telemetry.prototype._oldlog = Telemetry.prototype.log;
Telemetry.prototype.log = function(histogramId, value) {
if (!this.telemetryInfo) {
// Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
return;
}
if (histogramId) {
if (!this.telemetryInfo[histogramId]) {
this.telemetryInfo[histogramId] = [];
}
this.telemetryInfo[histogramId].push(value);
}
};
testButton("command-button-eyedropper");
}
function testButton(id) {
info("Testing " + id);
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
info("inspector opened");
let button = toolbox.doc.querySelector("#" + id);
ok(button, "Captain, we have the button");
// open the eyedropper
button.click();
checkResults("_EYEDROPPER_");
}).then(null, console.error);
}
function clickButton(node, clicks) {
for (let i = 0; i < clicks; i++) {
info("Clicking button " + node.id);
node.click();
}
}
function checkResults(histIdFocus) {
let result = Telemetry.prototype.telemetryInfo;
for (let [histId, value] of Iterator(result)) {
if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
!histId.contains(histIdFocus)) {
// Inspector stats are tested in
// browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
// because we only open the inspector once for this test.
continue;
}
if (histId.endsWith("OPENED_PER_USER_FLAG")) {
ok(value.length === 1 && value[0] === true,
"Per user value " + histId + " has a single value of true");
} else if (histId.endsWith("OPENED_BOOLEAN")) {
ok(value.length == 1, histId + " has one entry");
let okay = value.every(function(element) {
return element === true;
});
ok(okay, "All " + histId + " entries are === true");
}
}
finishUp();
}
function finishUp() {
gBrowser.removeCurrentTab();
Telemetry.prototype.log = Telemetry.prototype._oldlog;
delete Telemetry.prototype._oldlog;
delete Telemetry.prototype.telemetryInfo;
TargetFactory = Services = promise = require = null;
finish();
}
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function() {
gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
waitForFocus(init, content);
}, true);
content.location = TEST_URI;
}

View File

@ -1086,7 +1086,8 @@ SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.pr
chromeWindow = Services.wm.getMostRecentWindow("navigator:browser"); chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
chromeWindow.focus(); chromeWindow.focus();
} }
let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false }); let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false,
context: "picker" });
dropper.once("select", (event, color) => { dropper.once("select", (event, color) => {
if (toolboxWindow) { if (toolboxWindow) {

View File

@ -4,6 +4,19 @@
"use strict"; "use strict";
// So we can test collecting telemetry on the eyedropper
let oldCanRecord = Services.telemetry.canRecord;
Services.telemetry.canRecord = true;
registerCleanupFunction(function () {
Services.telemetry.canRecord = oldCanRecord;
});
const HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN";
const FLAG_HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG";
const EXPECTED_TELEMETRY = {
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": 2,
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": 1
}
const PAGE_CONTENT = [ const PAGE_CONTENT = [
'<style type="text/css">', '<style type="text/css">',
' body {', ' body {',
@ -34,6 +47,9 @@ const EXPECTED_COLOR = "rgb(255, 255, 85)"; // #ff5
// to close it, and clicking the page to select a color. // to close it, and clicking the page to select a color.
add_task(function*() { add_task(function*() {
// clear telemetry so we can get accurate counts
clearTelemetry();
yield addTab("data:text/html;charset=utf-8,rule view eyedropper test"); yield addTab("data:text/html;charset=utf-8,rule view eyedropper test");
content.document.body.innerHTML = PAGE_CONTENT; content.document.body.innerHTML = PAGE_CONTENT;
@ -57,6 +73,8 @@ add_task(function*() {
ok(dropper, "dropper opened"); ok(dropper, "dropper opened");
yield testSelect(swatch, dropper); yield testSelect(swatch, dropper);
checkTelemetry();
}); });
function testESC(swatch, dropper) { function testESC(swatch, dropper) {
@ -97,6 +115,23 @@ function testSelect(swatch, dropper) {
return deferred.promise; return deferred.promise;
} }
function clearTelemetry() {
for (let histogramId in EXPECTED_TELEMETRY) {
let histogram = Services.telemetry.getHistogramById(histogramId);
histogram.clear();
}
}
function checkTelemetry() {
for (let histogramId in EXPECTED_TELEMETRY) {
let expected = EXPECTED_TELEMETRY[histogramId];
let histogram = Services.telemetry.getHistogramById(histogramId);
let snapshot = histogram.snapshot();
is (snapshot.counts[1], expected,
"eyedropper telemetry value correct for " + histogramId);
}
}
/* Helpers */ /* Helpers */

View File

@ -28,12 +28,20 @@ function spawnTest() {
"curve": "Float32Array" "curve": "Float32Array"
}, "WaveShaper's `curve` is listed as an `Float32Array`."); }, "WaveShaper's `curve` is listed as an `Float32Array`.");
let aVar = gVars.getScopeAtIndex(0).get("curve")
let state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
ok(state, "Float32Array property should not have a dropdown.");
click(panelWin, findGraphNode(panelWin, nodeIds[1])); click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET); yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
checkVariableView(gVars, 0, { checkVariableView(gVars, 0, {
"buffer": "AudioBuffer" "buffer": "AudioBuffer"
}, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`."); }, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`.");
aVar = gVars.getScopeAtIndex(0).get("buffer")
state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
ok(state, "AudioBuffer property should not have a dropdown.");
yield teardown(panel); yield teardown(panel);
finish(); finish();
} }

View File

@ -150,7 +150,10 @@ let InspectorView = {
value: value, value: value,
writable: !flags || !flags.readonly, writable: !flags || !flags.readonly,
}; };
audioParamsScope.addItem(param, descriptor); let item = audioParamsScope.addItem(param, descriptor);
// No items should currently display a dropdown
item.twisty = false;
}); });
audioParamsScope.expanded = true; audioParamsScope.expanded = true;

View File

@ -17,3 +17,17 @@ needVerifiedUserLong = Please click the verification link in the email sent to %
resendVerificationEmail.label = Resend resendVerificationEmail.label = Resend
resendVerificationEmail.accessKey = R resendVerificationEmail.accessKey = R
forgetMigration.label = Forget
forgetMigration.accessKey = F
# These strings are used in a dialog we display after the user requests we resend
# a verification email.
verificationSentTitle = Verification Sent
# LOCALIZATION NOTE (verificationSentHeading) - %S = Email address of user's Firefox Account
verificationSentHeading = A verification link has been sent to %S
verificationSentDescription = Please check your email and click the link to begin syncing.
verificationNotSentTitle = Unable to Send Verification
verificationNotSentHeading = We are unable to send a verification mail at this time
verificationNotSentDescription = Please try again later.

View File

@ -137,9 +137,3 @@ syncUnlinkConfirm.label=Unlink
featureEnableRequiresRestart=%S must restart to enable this feature. featureEnableRequiresRestart=%S must restart to enable this feature.
featureDisableRequiresRestart=%S must restart to disable this feature. featureDisableRequiresRestart=%S must restart to disable this feature.
shouldRestartTitle=Restart %S shouldRestartTitle=Restart %S
###Preferences::Sync::Firefox Accounts
firefoxAccountsVerificationSentTitle=Verification Sent
# LOCALIZATION NOTE: %S = user's email address.
firefoxAccountsVerificationSentHeading=A verification link has been sent to %S
firefoxAccountVerificationSentDescription=Please check your email and click the link to begin syncing.

View File

@ -78,3 +78,6 @@ both, to better adapt this sentence to their language.
<!ENTITY welcome.createAccount.label "Create Account"> <!ENTITY welcome.createAccount.label "Create Account">
<!ENTITY welcome.useOldSync.label "Using an older version of Sync?"> <!ENTITY welcome.useOldSync.label "Using an older version of Sync?">
<!-- Sync Migration -->
<!ENTITY migrate.upgradeNeeded "The sync account system is being discontinued. A new Firefox Account is required to sync.">

View File

@ -543,6 +543,16 @@ this.UITour = {
break; break;
} }
case "setConfiguration": {
if (typeof data.configuration != "string") {
log.warn("setConfiguration: No configuration option specified");
return false;
}
this.setConfiguration(data.configuration, data.value);
break;
}
case "showFirefoxAccounts": { case "showFirefoxAccounts": {
// 'signup' is the only action that makes sense currently, so we don't // 'signup' is the only action that makes sense currently, so we don't
// accept arbitrary actions just to be safe... // accept arbitrary actions just to be safe...
@ -1321,7 +1331,7 @@ this.UITour = {
// An event object is expected but we don't want to toggle the panel with a click if the panel // An event object is expected but we don't want to toggle the panel with a click if the panel
// is already open. // is already open.
aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }).then(() => { aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }, "rooms").then(() => {
if (aOpenCallback) { if (aOpenCallback) {
aOpenCallback(); aOpenCallback();
} }
@ -1479,6 +1489,18 @@ this.UITour = {
} }
}, },
setConfiguration: function(aConfiguration, aValue) {
switch (aConfiguration) {
case "Loop:ResumeTourOnFirstJoin":
// Ignore aValue in this case to avoid accidentally setting it to false.
Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
break;
default:
log.error("setConfiguration: Unknown configuration requested: " + aConfiguration);
break;
}
},
getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) { getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) {
Task.spawn(function*() { Task.spawn(function*() {
let window = aChromeWindow; let window = aChromeWindow;

View File

@ -173,6 +173,68 @@ let tests = [
yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be underneath"); yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be underneath");
is(popup.popupBoxObject.alignmentPosition, "after_end", "Check " + currentTarget + " position"); is(popup.popupBoxObject.alignmentPosition, "after_end", "Check " + currentTarget + " position");
}), }),
taskify(function* test_setConfiguration() {
is(Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin"), false, "pref should be false but exist");
gContentAPI.setConfiguration("Loop:ResumeTourOnFirstJoin", true);
yield waitForConditionPromise(() => {
return Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin");
}, "Pref should change to true via setConfiguration");
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
}),
taskify(function* test_resumeViaMenuPanel_roomClosedTabOpen() {
Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
// Create a fake room and then add a fake non-owner participant
let roomsMap = setupFakeRoom();
roomsMap.get("fakeTourRoom").participants = [{
owner: false,
}];
// Set the tour URL to be the current page with a different query param
let gettingStartedURL = gTestTab.linkedBrowser.currentURI.resolve("?gettingstarted=1");
Services.prefs.setCharPref("loop.gettingStarted.url", gettingStartedURL);
let observationPromise = new Promise((resolve) => {
gContentAPI.observe((event, params) => {
is(event, "Loop:IncomingConversation", "Page should have been notified about incoming conversation");
ise(params.conversationOpen, false, "conversationOpen should be false");
is(gBrowser.selectedTab, gTestTab, "The same tab should be selected");
resolve();
});
});
// Now open the menu while that non-owner is in the fake room to trigger resuming the tour
yield showMenuPromise("loop");
yield observationPromise;
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
}),
taskify(function* test_resumeViaMenuPanel_roomClosedTabClosed() {
Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
// Create a fake room and then add a fake non-owner participant
let roomsMap = setupFakeRoom();
roomsMap.get("fakeTourRoom").participants = [{
owner: false,
}];
// Set the tour URL to a page that's not open yet
Services.prefs.setCharPref("loop.gettingStarted.url", gBrowser.currentURI.prePath);
let newTabPromise = waitForConditionPromise(() => {
return gBrowser.currentURI.path.contains("incomingConversation=waiting");
}, "New tab with incomingConversation=waiting should have opened");
// Now open the menu while that non-owner is in the fake room to trigger resuming the tour
yield showMenuPromise("loop");
yield newTabPromise;
yield gBrowser.removeCurrentTab();
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
}),
]; ];
// End tests // End tests
@ -188,9 +250,11 @@ function setupFakeRoom() {
let room = {}; let room = {};
for (let prop of ["roomToken", "roomName", "roomOwner", "roomUrl", "participants"]) for (let prop of ["roomToken", "roomName", "roomOwner", "roomUrl", "participants"])
room[prop] = "fakeTourRoom"; room[prop] = "fakeTourRoom";
LoopRooms.stubCache(new Map([ let roomsMap = new Map([
[room.roomToken, room] [room.roomToken, room]
])); ]);
LoopRooms.stubCache(roomsMap);
return roomsMap;
} }
if (Services.prefs.getBoolPref("loop.enabled")) { if (Services.prefs.getBoolPref("loop.enabled")) {
@ -201,7 +265,9 @@ if (Services.prefs.getBoolPref("loop.enabled")) {
Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/"); Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
registerCleanupFunction(() => { registerCleanupFunction(() => {
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
Services.prefs.clearUserPref("loop.gettingStarted.seen"); Services.prefs.clearUserPref("loop.gettingStarted.seen");
Services.prefs.clearUserPref("loop.gettingStarted.url");
Services.prefs.clearUserPref("loop.server"); Services.prefs.clearUserPref("loop.server");
Services.prefs.clearUserPref("services.push.serverURL"); Services.prefs.clearUserPref("services.push.serverURL");
@ -214,6 +280,9 @@ if (Services.prefs.getBoolPref("loop.enabled")) {
if (frame) { if (frame) {
frame.remove(); frame.remove();
} }
// Remove the stubbed rooms
LoopRooms.stubCache(null);
}); });
} else { } else {
ok(true, "Loop is disabled so skip the UITour Loop tests"); ok(true, "Loop is disabled so skip the UITour Loop tests");

View File

@ -223,6 +223,13 @@ if (typeof Mozilla == 'undefined') {
}); });
}; };
Mozilla.UITour.setConfiguration = function(configName, configValue) {
_sendEvent('setConfiguration', {
configuration: configName,
value: configValue,
});
};
Mozilla.UITour.showFirefoxAccounts = function() { Mozilla.UITour.showFirefoxAccounts = function() {
_sendEvent('showFirefoxAccounts'); _sendEvent('showFirefoxAccounts');
}; };

View File

@ -87,6 +87,18 @@ label.small {
-moz-box-flex: 1; -moz-box-flex: 1;
} }
/* Privacy Pane */
/* styles for the link elements copied from .text-link in global.css */
.inline-link {
color: -moz-nativehyperlinktext;
text-decoration: none;
}
.inline-link:hover {
text-decoration: underline;
}
/* Modeless Window Dialogs */ /* Modeless Window Dialogs */
.windowDialog, .windowDialog,
.windowDialog prefpane { .windowDialog prefpane {

View File

@ -184,6 +184,24 @@ caption {
font-weight: bold; font-weight: bold;
} }
/**
* Privacy Pane
*/
html|a.inline-link {
color: -moz-nativehyperlinktext;
text-decoration: none;
}
html|a.inline-link:hover {
text-decoration: underline;
}
html|a.inline-link:-moz-focusring {
outline-width: 0;
box-shadow: @focusRingShadow@;
}
/** /**
* Update Preferences * Update Preferences
*/ */

View File

@ -173,16 +173,6 @@ description > html|a {
cursor: pointer; cursor: pointer;
} }
#offlineAppsList,
#syncEnginesList {
-moz-appearance: none;
color: #333333;
padding: 10px;
border: 1px solid #C1C1C1;
border-radius: 2px;
background-color: #FBFBFB;
}
#noFxaAccount { #noFxaAccount {
/* Overriding the margins from the base preferences.css theme file. /* Overriding the margins from the base preferences.css theme file.
These overrides can be simplified by fixing bug 1027174 */ These overrides can be simplified by fixing bug 1027174 */
@ -293,3 +283,20 @@ description > html|a {
/** /**
* End Dialog * End Dialog
*/ */
/**
* Sync migration
*/
#sync-migration {
border: 1px solid rgba(0, 0, 0, 0.32);
background-color: InfoBackground;
color: InfoText;
text-shadow: none;
margin: 5px 0 0 0;
animation: fadein 3000ms;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@ -83,6 +83,18 @@ label.small {
-moz-box-flex: 1; -moz-box-flex: 1;
} }
/* Privacy Pane */
/* styles for the link elements copied from .text-link in global.css */
.inline-link {
color: -moz-nativehyperlinktext;
text-decoration: none;
}
.inline-link:hover {
text-decoration: underline;
}
/* Modeless Window Dialogs */ /* Modeless Window Dialogs */
.windowDialog, .windowDialog,
.windowDialog prefpane { .windowDialog prefpane {

View File

@ -16,6 +16,7 @@ let URL = '<!DOCTYPE html><style>' +
let TESTS = [{ let TESTS = [{
desc: "Changing the width of the test element", desc: "Changing the width of the test element",
searchFor: "Paint",
setup: function(div) { setup: function(div) {
div.setAttribute("class", "resize-change-color"); div.setAttribute("class", "resize-change-color");
}, },
@ -35,6 +36,7 @@ let TESTS = [{
} }
}, { }, {
desc: "Changing the test element's background color", desc: "Changing the test element's background color",
searchFor: "Paint",
setup: function(div) { setup: function(div) {
div.setAttribute("class", "change-color"); div.setAttribute("class", "change-color");
}, },
@ -52,6 +54,7 @@ let TESTS = [{
} }
}, { }, {
desc: "Changing the test element's classname", desc: "Changing the test element's classname",
searchFor: "Paint",
setup: function(div) { setup: function(div) {
div.setAttribute("class", "change-color add-class"); div.setAttribute("class", "change-color add-class");
}, },
@ -63,6 +66,7 @@ let TESTS = [{
} }
}, { }, {
desc: "sync console.time/timeEnd", desc: "sync console.time/timeEnd",
searchFor: "ConsoleTime",
setup: function(div, docShell) { setup: function(div, docShell) {
content.console.time("FOOBAR"); content.console.time("FOOBAR");
content.console.timeEnd("FOOBAR"); content.console.timeEnd("FOOBAR");
@ -102,7 +106,7 @@ let test = Task.async(function*() {
info("Start recording"); info("Start recording");
docShell.recordProfileTimelineMarkers = true; docShell.recordProfileTimelineMarkers = true;
for (let {desc, setup, check} of TESTS) { for (let {desc, searchFor, setup, check} of TESTS) {
info("Running test: " + desc); info("Running test: " + desc);
@ -110,7 +114,7 @@ let test = Task.async(function*() {
docShell.popProfileTimelineMarkers(); docShell.popProfileTimelineMarkers();
info("Running the test setup function"); info("Running the test setup function");
let onMarkers = waitForMarkers(docShell); let onMarkers = waitForMarkers(docShell, searchFor);
setup(div, docShell); setup(div, docShell);
info("Waiting for new markers on the docShell"); info("Waiting for new markers on the docShell");
let markers = yield onMarkers; let markers = yield onMarkers;
@ -140,21 +144,20 @@ function openUrl(url) {
}); });
} }
function waitForMarkers(docshell) { function waitForMarkers(docshell, searchFor) {
return new Promise(function(resolve, reject) { return new Promise(function(resolve, reject) {
let waitIterationCount = 0; let waitIterationCount = 0;
let maxWaitIterationCount = 10; // Wait for 2sec maximum let maxWaitIterationCount = 10; // Wait for 2sec maximum
let markers = [];
let interval = setInterval(() => { let interval = setInterval(() => {
let markers = docshell.popProfileTimelineMarkers(); let newMarkers = docshell.popProfileTimelineMarkers();
if (markers.length > 0) { markers = [...markers, ...newMarkers];
if (newMarkers.some(m => m.name == searchFor) ||
waitIterationCount > maxWaitIterationCount) {
clearInterval(interval); clearInterval(interval);
resolve(markers); resolve(markers);
} }
if (waitIterationCount > maxWaitIterationCount) {
clearInterval(interval);
resolve([]);
}
waitIterationCount++; waitIterationCount++;
}, 200); }, 200);
}); });

View File

@ -7,8 +7,8 @@
xmlns:gecko="http://schemas.android.com/apk/res-auto"> xmlns:gecko="http://schemas.android.com/apk/res-auto">
<ImageView android:id="@+id/icon" <ImageView android:id="@+id/icon"
android:layout_width="48dip" android:layout_width="@dimen/home_banner_icon_width"
android:layout_height="48dip" android:layout_height="@dimen/home_banner_icon_height"
android:layout_marginLeft="10dp" android:layout_marginLeft="10dp"
android:scaleType="centerInside"/> android:scaleType="centerInside"/>
@ -26,7 +26,7 @@
gecko:ellipsizeAtLine="3"/> gecko:ellipsizeAtLine="3"/>
<ImageButton android:id="@+id/close" <ImageButton android:id="@+id/close"
android:layout_width="34dip" android:layout_width="@dimen/home_banner_close_width"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@drawable/home_banner" android:background="@drawable/home_banner"
android:scaleType="center" android:scaleType="center"

View File

@ -148,6 +148,9 @@
<!-- Banner --> <!-- Banner -->
<dimen name="home_banner_height">72dp</dimen> <dimen name="home_banner_height">72dp</dimen>
<dimen name="home_banner_close_width">42dp</dimen>
<dimen name="home_banner_icon_height">48dip</dimen>
<dimen name="home_banner_icon_width">48dip</dimen>
<!-- Icon Grid --> <!-- Icon Grid -->
<dimen name="icongrid_columnwidth">128dp</dimen> <dimen name="icongrid_columnwidth">128dp</dimen>

View File

@ -765,10 +765,11 @@ class GTestCommands(MachCommandBase):
# current OS. # current OS.
debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking) debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args) if debugger:
if not debuggerInfo: debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
print("Could not find a suitable debugger in your PATH.") if not debuggerInfo:
return 1 print("Could not find a suitable debugger in your PATH.")
return 1
# Parameters come from the CLI. We need to convert them before # Parameters come from the CLI. We need to convert them before
# their use. # their use.
@ -942,10 +943,11 @@ class RunProgram(MachCommandBase):
# current OS. # current OS.
debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking) debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugparams) if debugger:
if not self.debuggerInfo: self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugparams)
print("Could not find a suitable debugger in your PATH.") if not self.debuggerInfo:
return 1 print("Could not find a suitable debugger in your PATH.")
return 1
# Parameters come from the CLI. We need to convert them before # Parameters come from the CLI. We need to convert them before
# their use. # their use.

View File

@ -350,12 +350,35 @@ Migrator.prototype = {
// STATE_USER_FXA_VERIFIED state. When the user clicks on the link in // STATE_USER_FXA_VERIFIED state. When the user clicks on the link in
// the mail we should see an ONVERIFIED_NOTIFICATION which will cause us // the mail we should see an ONVERIFIED_NOTIFICATION which will cause us
// to complete the migration. // to complete the migration.
resendVerificationMail: Task.async(function * () { resendVerificationMail: Task.async(function * (win) {
// warn if we aren't in the expected state - but go ahead anyway! // warn if we aren't in the expected state - but go ahead anyway!
if (this._state != this.STATE_USER_FXA_VERIFIED) { if (this._state != this.STATE_USER_FXA_VERIFIED) {
this.log.warn("createFxAccount called in an unexpected state: ${}", this._state); this.log.warn("createFxAccount called in an unexpected state: ${}", this._state);
} }
return fxAccounts.resendVerificationEmail(); let ok = true;
try {
yield fxAccounts.resendVerificationEmail();
} catch (ex) {
this.log.error("Failed to resend verification mail: ${}", ex);
ok = false;
}
let fxauser = yield fxAccounts.getSignedInUser();
let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
let heading = ok ?
sb.formatStringFromName("verificationSentHeading", [fxauser.email], 1) :
sb.GetStringFromName("verificationNotSentHeading");
let title = sb.GetStringFromName(ok ? "verificationSentTitle" : "verificationNotSentTitle");
let description = sb.GetStringFromName(ok ? "verificationSentDescription"
: "verificationNotSentDescription");
let factory = Cc["@mozilla.org/prompter;1"]
.getService(Ci.nsIPromptFactory);
let prompt = factory.getPrompt(win, Ci.nsIPrompt);
let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
bag.setPropertyAsBool("allowTabModal", true);
prompt.alert(title, heading + "\n\n" + description);
}), }),
// "forget" about the current Firefox account. This should only be called // "forget" about the current Firefox account. This should only be called

View File

@ -1801,9 +1801,11 @@ class Mochitest(MochitestUtilsMixin):
# TODO: use mozrunner.local.debugger_arguments: # TODO: use mozrunner.local.debugger_arguments:
# https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42 # https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
debuggerInfo = mozdebug.get_debugger_info(options.debugger, debuggerInfo = None
options.debuggerArgs, if options.debugger:
options.debuggerInteractive) debuggerInfo = mozdebug.get_debugger_info(options.debugger,
options.debuggerArgs,
options.debuggerInteractive)
if options.useTestMediaDevices: if options.useTestMediaDevices:
devices = findTestMediaDevices(self.log) devices = findTestMediaDevices(self.log)

View File

@ -135,6 +135,7 @@ add_task(function* test_emptytitle_export()
// 9. empty bookmarks db and continue // 9. empty bookmarks db and continue
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates();
const NOTITLE_URL = "http://notitle.mozilla.org/"; const NOTITLE_URL = "http://notitle.mozilla.org/";
let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
@ -144,6 +145,7 @@ add_task(function* test_emptytitle_export()
test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL }); test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL });
yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
yield promiseAsyncUpdates();
remove_all_bookmarks(); remove_all_bookmarks();
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
@ -177,6 +179,7 @@ add_task(function* test_import_chromefavicon()
const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png"); const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png");
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates();
let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId, let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
PAGE_URI, PAGE_URI,
PlacesUtils.bookmarks.DEFAULT_INDEX, PlacesUtils.bookmarks.DEFAULT_INDEX,
@ -201,6 +204,7 @@ add_task(function* test_import_chromefavicon()
{ title: "Test", url: PAGE_URI.spec, icon: base64Icon }); { title: "Test", url: PAGE_URI.spec, icon: base64Icon });
yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
yield promiseAsyncUpdates();
// Change the favicon to check it's really imported again later. // Change the favicon to check it's really imported again later.
deferred = Promise.defer(); deferred = Promise.defer();
@ -236,7 +240,10 @@ add_task(function* test_import_ontop()
// 4. run the test-suite // 4. run the test-suite
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates();
yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew); yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
yield promiseAsyncUpdates();
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true); yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates(); yield promiseAsyncUpdates();
yield testImportedBookmarks(); yield testImportedBookmarks();

View File

@ -5922,6 +5922,21 @@
"kind": "boolean", "kind": "boolean",
"description": "How many times has the devtool's Responsive View been opened via the toolbox button?" "description": "How many times has the devtool's Responsive View been opened via the toolbox button?"
}, },
"DEVTOOLS_EYEDROPPER_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's Eyedropper tool been opened?"
},
"DEVTOOLS_MENU_EYEDROPPER_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's eyedropper been opened via the devtools menu?"
},
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's eyedropper been opened via the color picker?"
},
"DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN": { "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN": {
"expires_in_version": "never", "expires_in_version": "never",
"kind": "boolean", "kind": "boolean",
@ -6047,6 +6062,21 @@
"kind": "flag", "kind": "flag",
"description": "How many users have opened the devtool's Responsive View been opened via the toolbox button?" "description": "How many users have opened the devtool's Responsive View been opened via the toolbox button?"
}, },
"DEVTOOLS_EYEDROPPER_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Eyedropper via the toolbox button?"
},
"DEVTOOLS_MENU_EYEDROPPER_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Eyedropper via the devtools menu?"
},
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Eyedropper via the color picker?"
},
"DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG": { "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG": {
"expires_in_version": "never", "expires_in_version": "never",
"kind": "flag", "kind": "flag",

View File

@ -6,6 +6,9 @@
// Test that the timeline front's start/stop/isRecording methods work in a // Test that the timeline front's start/stop/isRecording methods work in a
// simple use case, and that markers events are sent when operations occur. // simple use case, and that markers events are sent when operations occur.
// Note that this test isn't concerned with which markers are actually recorded,
// just that markers are recorded at all.
// Trying to check marker types here may lead to intermittents, see bug 1066474.
const {TimelineFront} = require("devtools/server/actors/timeline"); const {TimelineFront} = require("devtools/server/actors/timeline");
@ -14,37 +17,40 @@ add_task(function*() {
initDebuggerServer(); initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe()); let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client); let form = yield connectDebuggerClient(client);
let front = TimelineFront(client, form); let front = TimelineFront(client, form);
ok(front, "The TimelineFront was created");
let isActive = yield front.isRecording(); let isActive = yield front.isRecording();
ok(!isActive, "Not initially recording"); ok(!isActive, "The TimelineFront is not initially recording");
doc.body.innerHeight; // flush any pending reflow info("Flush any pending reflows");
let forceSyncReflow = doc.body.innerHeight;
info("Start recording");
yield front.start(); yield front.start();
isActive = yield front.isRecording(); isActive = yield front.isRecording();
ok(isActive, "Recording after start()"); ok(isActive, "The TimelineFront is now recording");
info("Change some style on the page to cause style/reflow/paint");
let onMarkers = once(front, "markers");
doc.body.style.padding = "10px"; doc.body.style.padding = "10px";
let markers = yield onMarkers;
let markers = yield once(front, "markers"); ok(true, "The markers event was fired");
ok(markers.length > 0, "Markers were returned");
ok(markers.length > 0, "markers were returned"); info("Flush pending reflows again");
ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow"); forceSyncReflow = doc.body.innerHeight;
ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
info("Change some style on the page to cause style/paint");
onMarkers = once(front, "markers");
doc.body.style.backgroundColor = "red"; doc.body.style.backgroundColor = "red";
markers = yield onMarkers;
markers = yield once(front, "markers");
ok(markers.length > 0, "markers were returned"); ok(markers.length > 0, "markers were returned");
ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
yield front.stop(); yield front.stop();

View File

@ -83,6 +83,11 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
throw new Error('Unsupported version: ' + version); throw new Error('Unsupported version: ' + version);
} }
// Some source maps produce relative source paths like "./foo.js" instead of
// "foo.js". Normalize these first so that future comparisons will succeed.
// See bugzil.la/1090768.
sources = sources.map(util.normalize);
// Pass `true` below to allow duplicate names and sources. While source maps // Pass `true` below to allow duplicate names and sources. While source maps
// are intended to be compressed and deduplicated, the TypeScript compiler // are intended to be compressed and deduplicated, the TypeScript compiler
// sometimes generates source maps with duplicates in them. See Github issue // sometimes generates source maps with duplicates in them. See Github issue
@ -307,6 +312,33 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
return binarySearch.search(aNeedle, aMappings, aComparator); return binarySearch.search(aNeedle, aMappings, aComparator);
}; };
/**
* Compute the last column for each generated mapping. The last column is
* inclusive.
*/
SourceMapConsumer.prototype.computeColumnSpans =
function SourceMapConsumer_computeColumnSpans() {
for (var index = 0; index < this._generatedMappings.length; ++index) {
var mapping = this._generatedMappings[index];
// Mappings do not contain a field for the last generated columnt. We
// can come up with an optimistic estimate, however, by assuming that
// mappings are contiguous (i.e. given two consecutive mappings, the
// first mapping ends where the second one starts).
if (index + 1 < this._generatedMappings.length) {
var nextMapping = this._generatedMappings[index + 1];
if (mapping.generatedLine === nextMapping.generatedLine) {
mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
continue;
}
}
// The last mapping for each line spans the entire line.
mapping.lastGeneratedColumn = Infinity;
}
};
/** /**
* Returns the original source, line, and column information for the generated * Returns the original source, line, and column information for the generated
* source's line and column positions provided. The only argument is an object * source's line and column positions provided. The only argument is an object
@ -329,23 +361,27 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
generatedColumn: util.getArg(aArgs, 'column') generatedColumn: util.getArg(aArgs, 'column')
}; };
var mapping = this._findMapping(needle, var index = this._findMapping(needle,
this._generatedMappings, this._generatedMappings,
"generatedLine", "generatedLine",
"generatedColumn", "generatedColumn",
util.compareByGeneratedPositions); util.compareByGeneratedPositions);
if (mapping && mapping.generatedLine === needle.generatedLine) { if (index >= 0) {
var source = util.getArg(mapping, 'source', null); var mapping = this._generatedMappings[index];
if (source != null && this.sourceRoot != null) {
source = util.join(this.sourceRoot, source); if (mapping.generatedLine === needle.generatedLine) {
var source = util.getArg(mapping, 'source', null);
if (source != null && this.sourceRoot != null) {
source = util.join(this.sourceRoot, source);
}
return {
source: source,
line: util.getArg(mapping, 'originalLine', null),
column: util.getArg(mapping, 'originalColumn', null),
name: util.getArg(mapping, 'name', null)
};
} }
return {
source: source,
line: util.getArg(mapping, 'originalLine', null),
column: util.getArg(mapping, 'originalColumn', null),
name: util.getArg(mapping, 'name', null)
};
} }
return { return {
@ -423,25 +459,82 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
needle.source = util.relative(this.sourceRoot, needle.source); needle.source = util.relative(this.sourceRoot, needle.source);
} }
var mapping = this._findMapping(needle, var index = this._findMapping(needle,
this._originalMappings, this._originalMappings,
"originalLine", "originalLine",
"originalColumn", "originalColumn",
util.compareByOriginalPositions); util.compareByOriginalPositions);
if (index >= 0) {
var mapping = this._originalMappings[index];
if (mapping) {
return { return {
line: util.getArg(mapping, 'generatedLine', null), line: util.getArg(mapping, 'generatedLine', null),
column: util.getArg(mapping, 'generatedColumn', null) column: util.getArg(mapping, 'generatedColumn', null),
lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
}; };
} }
return { return {
line: null, line: null,
column: null column: null,
lastColumn: null
}; };
}; };
/**
* Returns all generated line and column information for the original source
* and line provided. The only argument is an object with the following
* properties:
*
* - source: The filename of the original source.
* - line: The line number in the original source.
*
* and an array of objects is returned, each with the following properties:
*
* - line: The line number in the generated source, or null.
* - column: The column number in the generated source, or null.
*/
SourceMapConsumer.prototype.allGeneratedPositionsFor =
function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
// When there is no exact match, SourceMapConsumer.prototype._findMapping
// returns the index of the closest mapping less than the needle. By
// setting needle.originalColumn to Infinity, we thus find the last
// mapping for the given line, provided such a mapping exists.
var needle = {
source: util.getArg(aArgs, 'source'),
originalLine: util.getArg(aArgs, 'line'),
originalColumn: Infinity
};
if (this.sourceRoot != null) {
needle.source = util.relative(this.sourceRoot, needle.source);
}
var mappings = [];
var index = this._findMapping(needle,
this._originalMappings,
"originalLine",
"originalColumn",
util.compareByOriginalPositions);
if (index >= 0) {
var mapping = this._originalMappings[index];
while (mapping && mapping.originalLine === needle.originalLine) {
mappings.push({
line: util.getArg(mapping, 'generatedLine', null),
column: util.getArg(mapping, 'generatedColumn', null),
lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
});
mapping = this._originalMappings[--index];
}
}
return mappings.reverse();
};
SourceMapConsumer.GENERATED_ORDER = 1; SourceMapConsumer.GENERATED_ORDER = 1;
SourceMapConsumer.ORIGINAL_ORDER = 2; SourceMapConsumer.ORIGINAL_ORDER = 2;
@ -836,17 +929,17 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
// //
// 1. We find the exact element we are looking for. // 1. We find the exact element we are looking for.
// //
// 2. We did not find the exact element, but we can return the next // 2. We did not find the exact element, but we can return the index of
// closest element that is less than that element. // the next closest element that is less than that element.
// //
// 3. We did not find the exact element, and there is no next-closest // 3. We did not find the exact element, and there is no next-closest
// element which is less than the one we are searching for, so we // element which is less than the one we are searching for, so we
// return null. // return -1.
var mid = Math.floor((aHigh - aLow) / 2) + aLow; var mid = Math.floor((aHigh - aLow) / 2) + aLow;
var cmp = aCompare(aNeedle, aHaystack[mid], true); var cmp = aCompare(aNeedle, aHaystack[mid], true);
if (cmp === 0) { if (cmp === 0) {
// Found the element we are looking for. // Found the element we are looking for.
return aHaystack[mid]; return mid;
} }
else if (cmp > 0) { else if (cmp > 0) {
// aHaystack[mid] is greater than our needle. // aHaystack[mid] is greater than our needle.
@ -856,7 +949,7 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
} }
// We did not find an exact match, return the next closest one // We did not find an exact match, return the next closest one
// (termination case 2). // (termination case 2).
return aHaystack[mid]; return mid;
} }
else { else {
// aHaystack[mid] is less than our needle. // aHaystack[mid] is less than our needle.
@ -866,18 +959,16 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
} }
// The exact needle element was not found in this haystack. Determine if // The exact needle element was not found in this haystack. Determine if
// we are in termination case (2) or (3) and return the appropriate thing. // we are in termination case (2) or (3) and return the appropriate thing.
return aLow < 0 return aLow < 0 ? -1 : aLow;
? null
: aHaystack[aLow];
} }
} }
/** /**
* This is an implementation of binary search which will always try and return * This is an implementation of binary search which will always try and return
* the next lowest value checked if there is no exact hit. This is because * the index of next lowest value checked if there is no exact hit. This is
* mappings between original and generated line/col pairs are single points, * because mappings between original and generated line/col pairs are single
* and there is an implicit region between each of them, so a miss just means * points, and there is an implicit region between each of them, so a miss
* that you aren't on the very start of a region. * just means that you aren't on the very start of a region.
* *
* @param aNeedle The element you are looking for. * @param aNeedle The element you are looking for.
* @param aHaystack The array that is being searched. * @param aHaystack The array that is being searched.
@ -886,9 +977,10 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
* than, equal to, or greater than the element, respectively. * than, equal to, or greater than the element, respectively.
*/ */
exports.search = function search(aNeedle, aHaystack, aCompare) { exports.search = function search(aNeedle, aHaystack, aCompare) {
return aHaystack.length > 0 if (aHaystack.length === 0) {
? recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare) return -1;
: null; }
return recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
}; };
}); });

File diff suppressed because it is too large Load Diff

View File

@ -142,6 +142,22 @@ define('test/source-map/util', ['require', 'exports', 'module' , 'lib/source-ma
sourceRoot: '/the/root', sourceRoot: '/the/root',
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA' mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
}; };
exports.testMapRelativeSources = {
version: 3,
file: 'min.js',
names: ['bar', 'baz', 'n'],
sources: ['./one.js', './two.js'],
sourcesContent: [
' ONE.foo = function (bar) {\n' +
' return baz(bar);\n' +
' };',
' TWO.inc = function (n) {\n' +
' return n + 1;\n' +
' };'
],
sourceRoot: '/the/root',
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
};
exports.emptyMap = { exports.emptyMap = {
version: 3, version: 3,
file: 'min.js', file: 'min.js',

View File

@ -28,7 +28,7 @@ define("test/source-map/test-binary-search", ["require", "exports", "module"], f
binarySearch.search(needle, haystack, numberCompare); binarySearch.search(needle, haystack, numberCompare);
}); });
assert.equal(binarySearch.search(needle, haystack, numberCompare), 20); assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 20);
}; };
exports['test too low'] = function (assert, util) { exports['test too low'] = function (assert, util) {
@ -39,21 +39,21 @@ define("test/source-map/test-binary-search", ["require", "exports", "module"], f
binarySearch.search(needle, haystack, numberCompare); binarySearch.search(needle, haystack, numberCompare);
}); });
assert.equal(binarySearch.search(needle, haystack, numberCompare), null); assert.equal(binarySearch.search(needle, haystack, numberCompare), -1);
}; };
exports['test exact search'] = function (assert, util) { exports['test exact search'] = function (assert, util) {
var needle = 4; var needle = 4;
var haystack = [2,4,6,8,10,12,14,16,18,20]; var haystack = [2,4,6,8,10,12,14,16,18,20];
assert.equal(binarySearch.search(needle, haystack, numberCompare), 4); assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 4);
}; };
exports['test fuzzy search'] = function (assert, util) { exports['test fuzzy search'] = function (assert, util) {
var needle = 19; var needle = 19;
var haystack = [2,4,6,8,10,12,14,16,18,20]; var haystack = [2,4,6,8,10,12,14,16,18,20];
assert.equal(binarySearch.search(needle, haystack, numberCompare), 18); assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 18);
}; };
}); });

View File

@ -257,6 +257,25 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul
}, Error); }, Error);
}; };
exports['test that we can get the original source content with relative source paths'] = function (assert, util) {
var map = new SourceMapConsumer(util.testMapRelativeSources);
var sources = map.sources;
assert.equal(map.sourceContentFor(sources[0]), ' ONE.foo = function (bar) {\n return baz(bar);\n };');
assert.equal(map.sourceContentFor(sources[1]), ' TWO.inc = function (n) {\n return n + 1;\n };');
assert.equal(map.sourceContentFor("one.js"), ' ONE.foo = function (bar) {\n return baz(bar);\n };');
assert.equal(map.sourceContentFor("two.js"), ' TWO.inc = function (n) {\n return n + 1;\n };');
assert.throws(function () {
map.sourceContentFor("");
}, Error);
assert.throws(function () {
map.sourceContentFor("/the/root/three.js");
}, Error);
assert.throws(function () {
map.sourceContentFor("three.js");
}, Error);
};
exports['test sourceRoot + generatedPositionFor'] = function (assert, util) { exports['test sourceRoot + generatedPositionFor'] = function (assert, util) {
var map = new SourceMapGenerator({ var map = new SourceMapGenerator({
sourceRoot: 'foo/bar', sourceRoot: 'foo/bar',
@ -295,6 +314,158 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul
assert.equal(pos.column, 2); assert.equal(pos.column, 2);
}; };
exports['test allGeneratedPositionsFor'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 2, column: 1 },
generated: { line: 3, column: 2 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 2, column: 2 },
generated: { line: 3, column: 3 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 3, column: 1 },
generated: { line: 4, column: 2 },
source: 'bar.coffee'
});
map = new SourceMapConsumer(map.toString());
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'bar.coffee'
});
assert.equal(mappings.length, 2);
assert.equal(mappings[0].line, 3);
assert.equal(mappings[0].column, 2);
assert.equal(mappings[1].line, 3);
assert.equal(mappings[1].column, 3);
};
exports['test allGeneratedPositionsFor for line with no mappings'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 3, column: 1 },
generated: { line: 4, column: 2 },
source: 'bar.coffee'
});
map = new SourceMapConsumer(map.toString());
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'bar.coffee'
});
assert.equal(mappings.length, 0);
};
exports['test allGeneratedPositionsFor source map with no mappings'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map = new SourceMapConsumer(map.toString());
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'bar.coffee'
});
assert.equal(mappings.length, 0);
};
exports['test computeColumnSpans'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 1, column: 1 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 2, column: 1 },
generated: { line: 2, column: 1 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 2, column: 2 },
generated: { line: 2, column: 10 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 2, column: 3 },
generated: { line: 2, column: 20 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 3, column: 1 },
generated: { line: 3, column: 1 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 3, column: 2 },
generated: { line: 3, column: 2 },
source: 'foo.coffee'
});
map = new SourceMapConsumer(map.toString());
map.computeColumnSpans();
var mappings = map.allGeneratedPositionsFor({
line: 1,
source: 'foo.coffee'
});
assert.equal(mappings.length, 1);
assert.equal(mappings[0].lastColumn, Infinity);
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'foo.coffee'
});
assert.equal(mappings.length, 3);
assert.equal(mappings[0].lastColumn, 9);
assert.equal(mappings[1].lastColumn, 19);
assert.equal(mappings[2].lastColumn, Infinity);
var mappings = map.allGeneratedPositionsFor({
line: 3,
source: 'foo.coffee'
});
assert.equal(mappings.length, 2);
assert.equal(mappings[0].lastColumn, 1);
assert.equal(mappings[1].lastColumn, Infinity);
};
exports['test sourceRoot + originalPositionFor'] = function (assert, util) { exports['test sourceRoot + originalPositionFor'] = function (assert, util) {
var map = new SourceMapGenerator({ var map = new SourceMapGenerator({
sourceRoot: 'foo/bar', sourceRoot: 'foo/bar',

View File

@ -3,6 +3,7 @@ head = head_sourcemap.js
tail = tail =
support-files = Utils.jsm support-files = Utils.jsm
[test_util.js]
[test_source_node.js] [test_source_node.js]
[test_source_map_generator.js] [test_source_map_generator.js]
[test_source_map_consumer.js] [test_source_map_consumer.js]

View File

@ -584,20 +584,29 @@ xul|filefield + xul|button {
xul|richlistbox, xul|richlistbox,
xul|listbox { xul|listbox {
-moz-appearance: none; -moz-appearance: none;
background-color: #fff;
border: 1px solid #c1c1c1; border: 1px solid #c1c1c1;
color: #333;
} }
xul|treechildren::-moz-tree-row, xul|treechildren::-moz-tree-row,
xul|listbox xul|listitem { xul|listbox xul|listitem {
padding: 5px; padding: 0.3em;
margin: 0; margin: 0;
border: none; border: none;
border-radius: 0;
background-image: none; background-image: none;
} }
xul|treechildren::-moz-tree-row(hover),
xul|listbox xul|listitem:hover {
background-color: rgba(0,149,221,0.25);
}
xul|treechildren::-moz-tree-row(selected), xul|treechildren::-moz-tree-row(selected),
xul|listbox xul|listitem[selected="true"] { xul|listbox xul|listitem[selected="true"] {
background-color: #f1f1f1; background-color: #0095dd;
color: #fff;
} }
/* Trees */ /* Trees */
@ -652,4 +661,18 @@ xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection
xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] { xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] {
list-style-image: url("chrome://global/skin/in-content/sorter@2x.png"); list-style-image: url("chrome://global/skin/in-content/sorter@2x.png");
} }
}
/* This is the only way to increase the height of a tree row unfortunately */
xul|treechildren::-moz-tree-row {
min-height: 2em;
}
/* Color needs to be set on tree cell in order to be applied */
xul|treechildren::-moz-tree-cell-text {
color: #333;
}
xul|treechildren::-moz-tree-cell-text(selected) {
color: #fff;
} }