mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
9172674269
@ -1298,7 +1298,7 @@ pref("services.sync.prefs.sync.extensions.update.enabled", true);
|
||||
pref("services.sync.prefs.sync.intl.accept_languages", true);
|
||||
pref("services.sync.prefs.sync.javascript.enabled", true);
|
||||
pref("services.sync.prefs.sync.layout.spellcheckDefault", true);
|
||||
pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
|
||||
pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
|
||||
pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
|
||||
pref("services.sync.prefs.sync.network.cookie.cookieBehavior", true);
|
||||
pref("services.sync.prefs.sync.network.cookie.lifetimePolicy", true);
|
||||
@ -1884,3 +1884,4 @@ pref("browser.readinglist.enabled", true);
|
||||
pref("browser.readinglist.sidebarEverOpened", false);
|
||||
// Enable the readinglist engine by default.
|
||||
pref("readinglist.scheduler.enabled", true);
|
||||
pref("readinglist.server", "https://readinglist.services.mozilla.com/v1");
|
||||
|
@ -9,7 +9,7 @@
|
||||
let DevEdition = {
|
||||
_prefName: "browser.devedition.theme.enabled",
|
||||
_themePrefName: "general.skins.selectedSkin",
|
||||
_lwThemePrefName: "lightweightThemes.isThemeSelected",
|
||||
_lwThemePrefName: "lightweightThemes.selectedThemeID",
|
||||
_devtoolsThemePrefName: "devtools.theme",
|
||||
|
||||
styleSheetLocation: "chrome://browser/skin/devedition.css",
|
||||
@ -76,7 +76,7 @@ let DevEdition = {
|
||||
_updateStyleSheetFromPrefs: function() {
|
||||
let lightweightThemeSelected = false;
|
||||
try {
|
||||
lightweightThemeSelected = Services.prefs.getBoolPref(this._lwThemePrefName);
|
||||
lightweightThemeSelected = !!Services.prefs.getCharPref(this._lwThemePrefName);
|
||||
} catch(e) {}
|
||||
|
||||
let defaultThemeSelected = false;
|
||||
|
@ -457,7 +457,7 @@ let gSyncUI = {
|
||||
let lastSync, threshold, prolonged;
|
||||
try {
|
||||
lastSync = new Date(Services.prefs.getCharPref("readinglist.scheduler.lastSync"));
|
||||
threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout"));
|
||||
threshold = new Date(Date.now() - Services.prefs.getIntPref("services.sync.errorhandler.networkFailureReportTimeout") * 1000);
|
||||
prolonged = lastSync <= threshold;
|
||||
} catch (ex) {
|
||||
// no pref, assume not prolonged.
|
||||
|
@ -6,14 +6,18 @@
|
||||
*/
|
||||
|
||||
const PREF_DEVEDITION_THEME = "browser.devedition.theme.enabled";
|
||||
const PREF_LWTHEME = "lightweightThemes.isThemeSelected";
|
||||
const PREF_LWTHEME = "lightweightThemes.selectedThemeID";
|
||||
const PREF_LWTHEME_USED_THEMES = "lightweightThemes.usedThemes";
|
||||
const PREF_DEVTOOLS_THEME = "devtools.theme";
|
||||
const {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
// Set preferences back to their original values
|
||||
LightweightThemeManager.currentTheme = null;
|
||||
Services.prefs.clearUserPref(PREF_DEVEDITION_THEME);
|
||||
Services.prefs.clearUserPref(PREF_LWTHEME);
|
||||
Services.prefs.clearUserPref(PREF_DEVTOOLS_THEME);
|
||||
Services.prefs.clearUserPref(PREF_LWTHEME_USED_THEMES);
|
||||
});
|
||||
|
||||
add_task(function* startTests() {
|
||||
@ -28,12 +32,12 @@ add_task(function* startTests() {
|
||||
ok (DevEdition.styleSheet, "There is a devedition stylesheet when no themes are applied and pref is set.");
|
||||
|
||||
info ("Adding a lightweight theme.");
|
||||
Services.prefs.setBoolPref(PREF_LWTHEME, true);
|
||||
LightweightThemeManager.currentTheme = dummyLightweightTheme("preview0");
|
||||
ok (!DevEdition.styleSheet, "The devedition stylesheet has been removed when a lightweight theme is applied.");
|
||||
|
||||
info ("Removing a lightweight theme.");
|
||||
let onAttributeAdded = waitForBrightTitlebarAttribute();
|
||||
Services.prefs.setBoolPref(PREF_LWTHEME, false);
|
||||
LightweightThemeManager.currentTheme = null;
|
||||
ok (DevEdition.styleSheet, "The devedition stylesheet has been added when a lightweight theme is removed.");
|
||||
yield onAttributeAdded;
|
||||
|
||||
@ -85,16 +89,14 @@ function dummyLightweightTheme(id) {
|
||||
return {
|
||||
id: id,
|
||||
name: id,
|
||||
headerURL: "http://lwttest.invalid/a.png",
|
||||
footerURL: "http://lwttest.invalid/b.png",
|
||||
headerURL: "resource:///chrome/browser/content/browser/defaultthemes/1.header.jpg",
|
||||
iconURL: "resource:///chrome/browser/content/browser/defaultthemes/1.icon.jpg",
|
||||
textcolor: "red",
|
||||
accentcolor: "blue"
|
||||
};
|
||||
}
|
||||
|
||||
add_task(function* testLightweightThemePreview() {
|
||||
let {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
|
||||
|
||||
info ("Turning the pref on, then previewing lightweight themes");
|
||||
Services.prefs.setBoolPref(PREF_DEVEDITION_THEME, true);
|
||||
ok (DevEdition.styleSheet, "The devedition stylesheet is enabled.");
|
||||
|
@ -49,7 +49,7 @@ add_task(function () {
|
||||
|
||||
let defaultTheme = header.nextSibling;
|
||||
defaultTheme.doCommand();
|
||||
is(Services.prefs.getBoolPref("lightweightThemes.isThemeSelected"), false, "No lwtheme should be selected");
|
||||
is(Services.prefs.prefHasUserValue("lightweightThemes.selectedThemeID"), false, "No lwtheme should be selected");
|
||||
});
|
||||
|
||||
add_task(function asyncCleanup() {
|
||||
|
@ -639,6 +639,13 @@ function injectLoopAPI(targetWindow) {
|
||||
}
|
||||
},
|
||||
|
||||
SHARING_STATE_CHANGE: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
return Cu.cloneInto(SHARING_STATE_CHANGE, targetWindow);
|
||||
}
|
||||
},
|
||||
|
||||
fxAEnabled: {
|
||||
enumerable: true,
|
||||
get: function() {
|
||||
|
@ -29,6 +29,19 @@ const TWO_WAY_MEDIA_CONN_LENGTH = {
|
||||
MORE_THAN_5M: "MORE_THAN_5M",
|
||||
};
|
||||
|
||||
/**
|
||||
* Buckets that we segment sharing state change telemetry probes into.
|
||||
*
|
||||
* @type {{WINDOW_ENABLED: String, WINDOW_DISABLED: String,
|
||||
* BROWSER_ENABLED: String, BROWSER_DISABLED: String}}
|
||||
*/
|
||||
const SHARING_STATE_CHANGE = {
|
||||
WINDOW_ENABLED: "WINDOW_ENABLED",
|
||||
WINDOW_DISABLED: "WINDOW_DISABLED",
|
||||
BROWSER_ENABLED: "BROWSER_ENABLED",
|
||||
BROWSER_DISABLED: "BROWSER_DISABLED"
|
||||
};
|
||||
|
||||
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
|
||||
const PREF_LOG_LEVEL = "loop.debug.loglevel";
|
||||
|
||||
@ -42,7 +55,8 @@ Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm");
|
||||
|
||||
Cu.importGlobalProperties(["URL"]);
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE", "TWO_WAY_MEDIA_CONN_LENGTH"];
|
||||
this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE",
|
||||
"TWO_WAY_MEDIA_CONN_LENGTH", "SHARING_STATE_CHANGE"];
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI",
|
||||
"resource:///modules/loop/MozLoopAPI.jsm");
|
||||
|
@ -361,7 +361,9 @@ loop.shared.actions = (function() {
|
||||
* XXX: should move to some roomActions module - refs bug 1079284
|
||||
*/
|
||||
RoomFailure: Action.define("roomFailure", {
|
||||
error: Object
|
||||
error: Object,
|
||||
// True when the failures occurs in the join room request to the loop-server.
|
||||
failedJoinRequest: Boolean
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -105,7 +105,7 @@ loop.store.ActiveRoomStore = (function() {
|
||||
});
|
||||
|
||||
this._leaveRoom(actionData.error.errno === REST_ERRNOS.ROOM_FULL ?
|
||||
ROOM_STATES.FULL : ROOM_STATES.FAILED);
|
||||
ROOM_STATES.FULL : ROOM_STATES.FAILED, actionData.failedJoinRequest);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -161,7 +161,10 @@ loop.store.ActiveRoomStore = (function() {
|
||||
this._mozLoop.rooms.get(actionData.roomToken,
|
||||
function(error, roomData) {
|
||||
if (error) {
|
||||
this.dispatchAction(new sharedActions.RoomFailure({error: error}));
|
||||
this.dispatchAction(new sharedActions.RoomFailure({
|
||||
error: error,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -293,7 +296,15 @@ loop.store.ActiveRoomStore = (function() {
|
||||
this._mozLoop.rooms.join(this._storeState.roomToken,
|
||||
function(error, responseData) {
|
||||
if (error) {
|
||||
this.dispatchAction(new sharedActions.RoomFailure({error: error}));
|
||||
this.dispatchAction(new sharedActions.RoomFailure({
|
||||
error: error,
|
||||
// This is an explicit flag to avoid the leave happening if join
|
||||
// fails. We can't track it on ROOM_STATES.JOINING as the user
|
||||
// might choose to leave the room whilst the XHR is in progress
|
||||
// which would then mean we'd run the race condition of not
|
||||
// notifying the server of a leave.
|
||||
failedJoinRequest: true
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -555,7 +566,10 @@ loop.store.ActiveRoomStore = (function() {
|
||||
this._storeState.sessionToken,
|
||||
function(error, responseData) {
|
||||
if (error) {
|
||||
this.dispatchAction(new sharedActions.RoomFailure({error: error}));
|
||||
this.dispatchAction(new sharedActions.RoomFailure({
|
||||
error: error,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -568,8 +582,11 @@ loop.store.ActiveRoomStore = (function() {
|
||||
* signals to the server the leave of the room.
|
||||
*
|
||||
* @param {ROOM_STATES} nextState The next state to switch to.
|
||||
* @param {Boolean} failedJoinRequest Optional. Set to true if the join
|
||||
* request to loop-server failed. It
|
||||
* will skip the leave message.
|
||||
*/
|
||||
_leaveRoom: function(nextState) {
|
||||
_leaveRoom: function(nextState, failedJoinRequest) {
|
||||
if (loop.standaloneMedia) {
|
||||
loop.standaloneMedia.multiplexGum.reset();
|
||||
}
|
||||
@ -592,10 +609,11 @@ loop.store.ActiveRoomStore = (function() {
|
||||
delete this._timeout;
|
||||
}
|
||||
|
||||
if (this._storeState.roomState === ROOM_STATES.JOINING ||
|
||||
if (!failedJoinRequest &&
|
||||
(this._storeState.roomState === ROOM_STATES.JOINING ||
|
||||
this._storeState.roomState === ROOM_STATES.JOINED ||
|
||||
this._storeState.roomState === ROOM_STATES.SESSION_CONNECTED ||
|
||||
this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS) {
|
||||
this._storeState.roomState === ROOM_STATES.HAS_PARTICIPANTS)) {
|
||||
this._mozLoop.rooms.leave(this._storeState.roomToken,
|
||||
this._storeState.sessionToken);
|
||||
}
|
||||
|
@ -165,6 +165,8 @@ loop.OTSdkDriver = (function() {
|
||||
config);
|
||||
this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
|
||||
this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
|
||||
|
||||
this._noteSharingState(options.videoSource, true);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -196,6 +198,7 @@ loop.OTSdkDriver = (function() {
|
||||
this.screenshare.off("accessAllowed accessDenied");
|
||||
this.screenshare.destroy();
|
||||
delete this.screenshare;
|
||||
this._noteSharingState(this._windowId ? "browser" : "window", false);
|
||||
delete this._windowId;
|
||||
return true;
|
||||
},
|
||||
@ -648,15 +651,15 @@ loop.OTSdkDriver = (function() {
|
||||
* @private
|
||||
*/
|
||||
_noteConnectionLength: function(callLengthSeconds) {
|
||||
var buckets = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH;
|
||||
|
||||
var bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.SHORTER_THAN_10S;
|
||||
|
||||
var bucket = buckets.SHORTER_THAN_10S;
|
||||
if (callLengthSeconds >= 10 && callLengthSeconds <= 30) {
|
||||
bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_10S_AND_30S;
|
||||
bucket = buckets.BETWEEN_10S_AND_30S;
|
||||
} else if (callLengthSeconds > 30 && callLengthSeconds <= 300) {
|
||||
bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M;
|
||||
bucket = buckets.BETWEEN_30S_AND_5M;
|
||||
} else if (callLengthSeconds > 300) {
|
||||
bucket = this.mozLoop.TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M;
|
||||
bucket = buckets.MORE_THAN_5M;
|
||||
}
|
||||
|
||||
this.mozLoop.telemetryAddKeyedValue("LOOP_TWO_WAY_MEDIA_CONN_LENGTH",
|
||||
@ -705,7 +708,32 @@ loop.OTSdkDriver = (function() {
|
||||
* If set to true, make it easy to test/verify 2-way media connection
|
||||
* telemetry code operation by viewing the logs.
|
||||
*/
|
||||
_debugTwoWayMediaTelemetry: false
|
||||
_debugTwoWayMediaTelemetry: false,
|
||||
|
||||
/**
|
||||
* Note the sharing state. If this.mozLoop is not defined, we're assumed to
|
||||
* be running in the standalone client and return immediately.
|
||||
*
|
||||
* @param {String} type Type of sharing that was flipped. May be 'window'
|
||||
* or 'tab'.
|
||||
* @param {Boolean} enabled Flag that tells us if the feature was flipped on
|
||||
* or off.
|
||||
* @private
|
||||
*/
|
||||
_noteSharingState: function(type, enabled) {
|
||||
if (!this.mozLoop) {
|
||||
return;
|
||||
}
|
||||
|
||||
var bucket = this.mozLoop.SHARING_STATE_CHANGE[type.toUpperCase() + "_" +
|
||||
(enabled ? "ENABLED" : "DISABLED")];
|
||||
if (!bucket) {
|
||||
console.error("No sharing state bucket found for '" + type + "'");
|
||||
return;
|
||||
}
|
||||
|
||||
this.mozLoop.telemetryAddKeyedValue("LOOP_SHARING_STATE_CHANGE", bucket);
|
||||
}
|
||||
};
|
||||
|
||||
return OTSdkDriver;
|
||||
|
@ -47,3 +47,29 @@ add_task(function* test_mozLoop_telemetryAdd_buckets() {
|
||||
is(snapshot["BETWEEN_30S_AND_5M"].sum, 3, "TWO_WAY_MEDIA_CONN_LENGTH.BETWEEN_30S_AND_5M");
|
||||
is(snapshot["MORE_THAN_5M"].sum, 4, "TWO_WAY_MEDIA_CONN_LENGTH.MORE_THAN_5M");
|
||||
});
|
||||
|
||||
add_task(function* test_mozLoop_telemetryAdd_sharing_buckets() {
|
||||
let histogramId = "LOOP_SHARING_STATE_CHANGE";
|
||||
let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
|
||||
const SHARING_STATES = gMozLoopAPI.SHARING_STATE_CHANGE;
|
||||
|
||||
histogram.clear();
|
||||
for (let value of [SHARING_STATES.WINDOW_ENABLED,
|
||||
SHARING_STATES.WINDOW_DISABLED,
|
||||
SHARING_STATES.WINDOW_DISABLED,
|
||||
SHARING_STATES.BROWSER_ENABLED,
|
||||
SHARING_STATES.BROWSER_ENABLED,
|
||||
SHARING_STATES.BROWSER_ENABLED,
|
||||
SHARING_STATES.BROWSER_DISABLED,
|
||||
SHARING_STATES.BROWSER_DISABLED,
|
||||
SHARING_STATES.BROWSER_DISABLED,
|
||||
SHARING_STATES.BROWSER_DISABLED]) {
|
||||
gMozLoopAPI.telemetryAddKeyedValue(histogramId, value);
|
||||
}
|
||||
|
||||
let snapshot = histogram.snapshot();
|
||||
Assert.strictEqual(snapshot["WINDOW_ENABLED"].sum, 1, "SHARING_STATE_CHANGE.WINDOW_ENABLED");
|
||||
Assert.strictEqual(snapshot["WINDOW_DISABLED"].sum, 2, "SHARING_STATE_CHANGE.WINDOW_DISABLED");
|
||||
Assert.strictEqual(snapshot["BROWSER_ENABLED"].sum, 3, "SHARING_STATE_CHANGE.BROWSER_ENABLED");
|
||||
Assert.strictEqual(snapshot["BROWSER_DISABLED"].sum, 4, "SHARING_STATE_CHANGE.BROWSER_DISABLED");
|
||||
});
|
||||
|
@ -95,7 +95,10 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
});
|
||||
|
||||
it("should log the error", function() {
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(console.error);
|
||||
sinon.assert.calledWith(console.error,
|
||||
@ -105,13 +108,19 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
it("should set the state to `FULL` on server error room full", function() {
|
||||
fakeError.errno = REST_ERRNOS.ROOM_FULL;
|
||||
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
expect(store._storeState.roomState).eql(ROOM_STATES.FULL);
|
||||
});
|
||||
|
||||
it("should set the state to `FAILED` on generic error", function() {
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
|
||||
expect(store._storeState.failureReason).eql(FAILURE_DETAILS.UNKNOWN);
|
||||
@ -121,7 +130,10 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
"invalid token", function() {
|
||||
fakeError.errno = REST_ERRNOS.INVALID_TOKEN;
|
||||
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
|
||||
expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
|
||||
@ -131,14 +143,20 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
"expired", function() {
|
||||
fakeError.errno = REST_ERRNOS.EXPIRED;
|
||||
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
expect(store._storeState.roomState).eql(ROOM_STATES.FAILED);
|
||||
expect(store._storeState.failureReason).eql(FAILURE_DETAILS.EXPIRED_OR_INVALID);
|
||||
});
|
||||
|
||||
it("should reset the multiplexGum", function() {
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(fakeMultiplexGum.reset);
|
||||
});
|
||||
@ -146,14 +164,20 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
it("should set screen sharing inactive", function() {
|
||||
store.setStoreState({windowId: "1234"});
|
||||
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(fakeMozLoop.setScreenShareState);
|
||||
sinon.assert.calledWithExactly(fakeMozLoop.setScreenShareState, "1234", false);
|
||||
});
|
||||
|
||||
it("should disconnect from the servers via the sdk", function() {
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(fakeSdkDriver.disconnectSession);
|
||||
});
|
||||
@ -162,7 +186,10 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
sandbox.stub(window, "clearTimeout");
|
||||
store._timeout = {};
|
||||
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(clearTimeout);
|
||||
});
|
||||
@ -174,18 +201,33 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
}));
|
||||
|
||||
// Now simulate room failure.
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(fakeMozLoop.removeBrowserSharingListener);
|
||||
});
|
||||
|
||||
it("should call mozLoop.rooms.leave", function() {
|
||||
store.roomFailure({error: fakeError});
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
|
||||
sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
|
||||
"fakeToken", "1627384950");
|
||||
});
|
||||
|
||||
it("should not call mozLoop.rooms.leave if failedJoinRequest is true", function() {
|
||||
store.roomFailure(new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: true
|
||||
}));
|
||||
|
||||
sinon.assert.notCalled(fakeMozLoop.rooms.leave);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setupWindowData", function() {
|
||||
@ -271,7 +313,8 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.RoomFailure({
|
||||
error: fakeError
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
});
|
||||
});
|
||||
@ -449,7 +492,10 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWith(dispatcher.dispatch,
|
||||
new sharedActions.RoomFailure({error: fakeError}));
|
||||
new sharedActions.RoomFailure({
|
||||
error: fakeError,
|
||||
failedJoinRequest: true
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@ -582,7 +628,8 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
sinon.assert.calledOnce(dispatcher.dispatch);
|
||||
sinon.assert.calledWith(dispatcher.dispatch,
|
||||
new sharedActions.RoomFailure({
|
||||
error: fakeError
|
||||
error: fakeError,
|
||||
failedJoinRequest: false
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
@ -71,6 +71,12 @@ describe("loop.OTSdkDriver", function () {
|
||||
BETWEEN_10S_AND_30S: "BETWEEN_10S_AND_30S",
|
||||
BETWEEN_30S_AND_5M: "BETWEEN_30S_AND_5M",
|
||||
MORE_THAN_5M: "MORE_THAN_5M"
|
||||
},
|
||||
SHARING_STATE_CHANGE: {
|
||||
WINDOW_ENABLED: "WINDOW_ENABLED",
|
||||
WINDOW_DISABLED: "WINDOW_DISABLED",
|
||||
BROWSER_ENABLED: "BROWSER_ENABLED",
|
||||
BROWSER_DISABLED: "BROWSER_DISABLED"
|
||||
}
|
||||
};
|
||||
|
||||
@ -189,6 +195,7 @@ describe("loop.OTSdkDriver", function () {
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
sandbox.stub(driver, "_noteSharingState");
|
||||
|
||||
fakeElement = {
|
||||
className: "fakeVideo"
|
||||
@ -214,6 +221,19 @@ describe("loop.OTSdkDriver", function () {
|
||||
sinon.assert.calledOnce(sdk.initPublisher);
|
||||
sinon.assert.calledWithMatch(sdk.initPublisher, fakeElement, options);
|
||||
});
|
||||
|
||||
it("should log a telemetry action", function() {
|
||||
var options = {
|
||||
videoSource: "browser",
|
||||
constraints: {
|
||||
browserWindow: 42,
|
||||
scrollWithPage: true
|
||||
}
|
||||
};
|
||||
driver.startScreenShare(options);
|
||||
|
||||
sinon.assert.calledWithExactly(driver._noteSharingState, "browser", true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#switchAcquiredWindow", function() {
|
||||
@ -251,26 +271,70 @@ describe("loop.OTSdkDriver", function () {
|
||||
beforeEach(function() {
|
||||
driver.getScreenShareElementFunc = function() {};
|
||||
|
||||
driver.startScreenShare({
|
||||
videoSource: "window"
|
||||
});
|
||||
|
||||
sandbox.stub(dispatcher, "dispatch");
|
||||
|
||||
driver.session = session;
|
||||
sandbox.stub(driver, "_noteSharingState");
|
||||
});
|
||||
|
||||
it("should unpublish the share", function() {
|
||||
driver.startScreenShare({
|
||||
videoSource: "window"
|
||||
});
|
||||
driver.session = session;
|
||||
|
||||
driver.endScreenShare(new sharedActions.EndScreenShare());
|
||||
|
||||
sinon.assert.calledOnce(session.unpublish);
|
||||
});
|
||||
|
||||
it("should log a telemetry action", function() {
|
||||
driver.startScreenShare({
|
||||
videoSource: "window"
|
||||
});
|
||||
driver.session = session;
|
||||
|
||||
driver.endScreenShare(new sharedActions.EndScreenShare());
|
||||
|
||||
sinon.assert.calledWithExactly(driver._noteSharingState, "window", false);
|
||||
});
|
||||
|
||||
it("should destroy the share", function() {
|
||||
driver.startScreenShare({
|
||||
videoSource: "window"
|
||||
});
|
||||
driver.session = session;
|
||||
|
||||
expect(driver.endScreenShare()).to.equal(true);
|
||||
|
||||
sinon.assert.calledOnce(publisher.destroy);
|
||||
});
|
||||
|
||||
it("should unpublish the share too when type is 'browser'", function() {
|
||||
driver.startScreenShare({
|
||||
videoSource: "browser",
|
||||
constraints: {
|
||||
browserWindow: 42
|
||||
}
|
||||
});
|
||||
driver.session = session;
|
||||
|
||||
driver.endScreenShare(new sharedActions.EndScreenShare());
|
||||
|
||||
sinon.assert.calledOnce(session.unpublish);
|
||||
});
|
||||
|
||||
it("should log a telemetry action too when type is 'browser'", function() {
|
||||
driver.startScreenShare({
|
||||
videoSource: "browser",
|
||||
constraints: {
|
||||
browserWindow: 42
|
||||
}
|
||||
});
|
||||
driver.session = session;
|
||||
|
||||
driver.endScreenShare(new sharedActions.EndScreenShare());
|
||||
|
||||
sinon.assert.calledWithExactly(driver._noteSharingState, "browser", false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#connectSession", function() {
|
||||
@ -431,6 +495,44 @@ describe("loop.OTSdkDriver", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_noteSharingState", function() {
|
||||
it("should record enabled sharing states for window", function() {
|
||||
driver._noteSharingState("window", true);
|
||||
|
||||
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
|
||||
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
|
||||
"LOOP_SHARING_STATE_CHANGE",
|
||||
mozLoop.SHARING_STATE_CHANGE.WINDOW_ENABLED);
|
||||
});
|
||||
|
||||
it("should record enabled sharing states for browser", function() {
|
||||
driver._noteSharingState("browser", true);
|
||||
|
||||
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
|
||||
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
|
||||
"LOOP_SHARING_STATE_CHANGE",
|
||||
mozLoop.SHARING_STATE_CHANGE.BROWSER_ENABLED);
|
||||
});
|
||||
|
||||
it("should record disabled sharing states for window", function() {
|
||||
driver._noteSharingState("window", false);
|
||||
|
||||
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
|
||||
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
|
||||
"LOOP_SHARING_STATE_CHANGE",
|
||||
mozLoop.SHARING_STATE_CHANGE.WINDOW_DISABLED);
|
||||
});
|
||||
|
||||
it("should record disabled sharing states for browser", function() {
|
||||
driver._noteSharingState("browser", false);
|
||||
|
||||
sinon.assert.calledOnce(mozLoop.telemetryAddKeyedValue);
|
||||
sinon.assert.calledWithExactly(mozLoop.telemetryAddKeyedValue,
|
||||
"LOOP_SHARING_STATE_CHANGE",
|
||||
mozLoop.SHARING_STATE_CHANGE.BROWSER_DISABLED);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#forceDisconnectAll", function() {
|
||||
it("should not disconnect anything when not connected", function() {
|
||||
driver.session = session;
|
||||
|
@ -58,6 +58,17 @@ const ITEM_RECORD_PROPERTIES = `
|
||||
readPosition
|
||||
`.trim().split(/\s+/);
|
||||
|
||||
// Article objects that are passed to ReadingList.addItem may contain
|
||||
// some properties that are known but are not currently stored in the
|
||||
// ReadingList records. This is the list of properties that are knowingly
|
||||
// disregarded before the item is normalized.
|
||||
const ITEM_DISREGARDED_PROPERTIES = `
|
||||
byline
|
||||
dir
|
||||
content
|
||||
length
|
||||
`.trim().split(/\s+/);
|
||||
|
||||
/**
|
||||
* A reading list contains ReadingListItems.
|
||||
*
|
||||
@ -853,6 +864,9 @@ ReadingListItemIterator.prototype = {
|
||||
function normalizeRecord(nonNormalizedRecord) {
|
||||
let record = {};
|
||||
for (let prop in nonNormalizedRecord) {
|
||||
if (ITEM_DISREGARDED_PROPERTIES.includes(prop)) {
|
||||
continue;
|
||||
}
|
||||
if (!ITEM_RECORD_PROPERTIES.includes(prop)) {
|
||||
throw new Error("Unrecognized item property: " + prop);
|
||||
}
|
||||
|
166
browser/components/readinglist/ServerClient.jsm
Normal file
166
browser/components/readinglist/ServerClient.jsm
Normal file
@ -0,0 +1,166 @@
|
||||
/* 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/. */
|
||||
|
||||
// The client used to access the ReadingList server.
|
||||
|
||||
"use strict";
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RESTRequest", "resource://services-common/rest.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils", "resource://services-common/utils.js");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts", "resource://gre/modules/FxAccounts.jsm");
|
||||
|
||||
let log = Log.repository.getLogger("readinglist.serverclient");
|
||||
|
||||
const OAUTH_SCOPE = "readinglist"; // The "scope" on the oauth token we request.
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"ServerClient",
|
||||
];
|
||||
|
||||
// utf-8 joy. rest.js, which we use for the underlying requests, does *not*
|
||||
// encode the request as utf-8 even though it wants to know the encoding.
|
||||
// It does, however, explicitly decode the response. This seems insane, but is
|
||||
// what it is.
|
||||
// The end result being we need to utf-8 the request and let the response take
|
||||
// care of itself.
|
||||
function objectToUTF8Json(obj) {
|
||||
// FTR, unescape(encodeURIComponent(JSON.stringify(obj))) also works ;)
|
||||
return CommonUtils.encodeUTF8(JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function ServerClient(fxa = fxAccounts) {
|
||||
this.fxa = fxa;
|
||||
}
|
||||
|
||||
ServerClient.prototype = {
|
||||
|
||||
request(options) {
|
||||
return this._request(options.path, options.method, options.body, options.headers);
|
||||
},
|
||||
|
||||
get serverURL() {
|
||||
return Services.prefs.getCharPref("readinglist.server");
|
||||
},
|
||||
|
||||
_getURL(path) {
|
||||
let result = this.serverURL;
|
||||
// we expect the path to have a leading slash, so remove any trailing
|
||||
// slashes on the pref.
|
||||
if (result.endsWith("/")) {
|
||||
result = result.slice(0, -1);
|
||||
}
|
||||
return result + path;
|
||||
},
|
||||
|
||||
// Hook points for testing.
|
||||
_getToken() {
|
||||
// Assume token-caching is in place - if it's not we should avoid doing
|
||||
// this each request.
|
||||
return this.fxa.getOAuthToken({scope: OAUTH_SCOPE});
|
||||
},
|
||||
|
||||
_removeToken(token) {
|
||||
// XXX - remove this check once tokencaching landsin FxA.
|
||||
if (!this.fxa.removeCachedOAuthToken) {
|
||||
dump("XXX - token caching support is yet to land - can't remove token!");
|
||||
return;
|
||||
}
|
||||
return this.fxa.removeCachedOAuthToken({token});
|
||||
},
|
||||
|
||||
// Converts an error from the RESTRequest object to an error we export.
|
||||
_convertRestError(error) {
|
||||
return error; // XXX - errors?
|
||||
},
|
||||
|
||||
// Converts an error from a try/catch handler to an error we export.
|
||||
_convertJSError(error) {
|
||||
return error; // XXX - errors?
|
||||
},
|
||||
|
||||
/*
|
||||
* Perform a request - handles authentication
|
||||
*/
|
||||
_request: Task.async(function* (path, method, body, headers) {
|
||||
let token = yield this._getToken();
|
||||
let response = yield this._rawRequest(path, method, body, headers, token);
|
||||
log.debug("initial request got status ${status}", response);
|
||||
if (response.status == 401) {
|
||||
// an auth error - assume our token has expired or similar.
|
||||
this._removeToken(token);
|
||||
token = yield this._getToken();
|
||||
response = yield this._rawRequest(path, method, body, headers, token);
|
||||
log.debug("retry of request got status ${status}", response);
|
||||
}
|
||||
return response;
|
||||
}),
|
||||
|
||||
/*
|
||||
* Perform a request *without* abstractions such as auth etc
|
||||
*
|
||||
* On success (which *includes* non-200 responses) returns an object like:
|
||||
* {
|
||||
* status: 200, # http status code
|
||||
* headers: {}, # header values keyed by header name.
|
||||
* body: {}, # parsed json
|
||||
}
|
||||
*/
|
||||
|
||||
_rawRequest(path, method, body, headers, oauthToken) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let url = this._getURL(path);
|
||||
log.debug("dispatching request to", url);
|
||||
let request = new RESTRequest(url);
|
||||
method = method.toUpperCase();
|
||||
|
||||
request.setHeader("Accept", "application/json");
|
||||
request.setHeader("Content-Type", "application/json; charset=utf-8");
|
||||
request.setHeader("Authorization", "Bearer " + oauthToken);
|
||||
// and additional header specified for this request.
|
||||
if (headers) {
|
||||
for (let [headerName, headerValue] in Iterator(headers)) {
|
||||
log.trace("Caller specified header: ${headerName}=${headerValue}", {headerName, headerValue});
|
||||
request.setHeader(headerName, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
request.onComplete = error => {
|
||||
if (error) {
|
||||
return reject(this._convertRestError(error));
|
||||
}
|
||||
|
||||
let response = request.response;
|
||||
log.debug("received response status: ${status} ${statusText}", response);
|
||||
// Handle response status codes we know about
|
||||
let result = {
|
||||
status: response.status,
|
||||
headers: response.headers
|
||||
};
|
||||
try {
|
||||
if (response.body) {
|
||||
result.body = JSON.parse(response.body);
|
||||
}
|
||||
} catch (e) {
|
||||
log.info("Failed to parse JSON body |${body}|: ${e}",
|
||||
{body: response.body, e});
|
||||
// We don't reject due to this (and don't even make a huge amount of
|
||||
// log noise - eg, a 50X error from a load balancer etc may not write
|
||||
// JSON.
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
}
|
||||
// We are assuming the body has already been decoded and thus contains
|
||||
// unicode, but the server expects utf-8. encodeURIComponent does that.
|
||||
request.dispatch(method, objectToUTF8Json(body));
|
||||
});
|
||||
},
|
||||
};
|
@ -6,6 +6,8 @@ JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
EXTRA_JS_MODULES.readinglist += [
|
||||
'ReadingList.jsm',
|
||||
'Scheduler.jsm',
|
||||
'ServerClient.jsm',
|
||||
'SQLiteStore.jsm',
|
||||
]
|
||||
|
||||
@ -17,9 +19,5 @@ BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['test/xpcshell/xpcshell.ini']
|
||||
|
||||
EXTRA_JS_MODULES.readinglist += [
|
||||
'Scheduler.jsm',
|
||||
]
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Reading List')
|
||||
|
@ -161,6 +161,7 @@ let RLSidebar = {
|
||||
} else {
|
||||
thumb.style.removeProperty("background-image");
|
||||
}
|
||||
thumb.classList.toggle("preview-available", !!item.preview);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -200,7 +201,7 @@ let RLSidebar = {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Setting activeItem: ${node ? node.id : null}`);
|
||||
log.trace(`Setting activeItem: ${node ? node.id : null}`);
|
||||
|
||||
if (node && node.classList.contains("active")) {
|
||||
return;
|
||||
@ -233,7 +234,7 @@ let RLSidebar = {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug(`Setting activeItem: ${node ? node.id : null}`);
|
||||
log.trace(`Setting selectedItem: ${node ? node.id : null}`);
|
||||
|
||||
let prevItem = document.querySelector("#list > .item.selected");
|
||||
if (prevItem) {
|
||||
@ -270,7 +271,7 @@ let RLSidebar = {
|
||||
},
|
||||
|
||||
set selectedIndex(index) {
|
||||
log.debug(`Setting selectedIndex: ${index}`);
|
||||
log.trace(`Setting selectedIndex: ${index}`);
|
||||
|
||||
if (index == -1) {
|
||||
this.selectedItem = null;
|
||||
|
@ -5,3 +5,52 @@ const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
do_get_profile(); // fxa needs a profile directory for storage.
|
||||
|
||||
Cu.import("resource://gre/modules/FxAccounts.jsm");
|
||||
Cu.import("resource://gre/modules/FxAccountsClient.jsm");
|
||||
|
||||
// Create a mocked FxAccounts object with a signed-in, verified user.
|
||||
function* createMockFxA() {
|
||||
|
||||
function MockFxAccountsClient() {
|
||||
this._email = "nobody@example.com";
|
||||
this._verified = false;
|
||||
|
||||
this.accountStatus = function(uid) {
|
||||
let deferred = Promise.defer();
|
||||
deferred.resolve(!!uid && (!this._deletedOnServer));
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
this.signOut = function() { return Promise.resolve(); };
|
||||
|
||||
FxAccountsClient.apply(this);
|
||||
}
|
||||
|
||||
MockFxAccountsClient.prototype = {
|
||||
__proto__: FxAccountsClient.prototype
|
||||
}
|
||||
|
||||
function MockFxAccounts() {
|
||||
return new FxAccounts({
|
||||
fxAccountsClient: new MockFxAccountsClient(),
|
||||
getAssertion: () => Promise.resolve("assertion"),
|
||||
});
|
||||
}
|
||||
|
||||
let fxa = new MockFxAccounts();
|
||||
let credentials = {
|
||||
email: "foo@example.com",
|
||||
uid: "1234@lcip.org",
|
||||
assertion: "foobar",
|
||||
sessionToken: "dead",
|
||||
kA: "beef",
|
||||
kB: "cafe",
|
||||
verified: true
|
||||
};
|
||||
|
||||
yield fxa.setSignedInUser(credentials);
|
||||
return fxa;
|
||||
}
|
||||
|
@ -0,0 +1,209 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
Cu.import("resource:///modules/readinglist/ServerClient.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
let appender = new Log.DumpAppender();
|
||||
for (let logName of ["FirefoxAccounts", "readinglist.serverclient"]) {
|
||||
Log.repository.getLogger(logName).addAppender(appender);
|
||||
}
|
||||
|
||||
// Some test servers we use.
|
||||
let Server = function(handlers) {
|
||||
this._server = null;
|
||||
this._handlers = handlers;
|
||||
}
|
||||
|
||||
Server.prototype = {
|
||||
start() {
|
||||
this._server = new HttpServer();
|
||||
for (let [path, handler] in Iterator(this._handlers)) {
|
||||
// httpd.js seems to swallow exceptions
|
||||
let thisHandler = handler;
|
||||
let wrapper = (request, response) => {
|
||||
try {
|
||||
thisHandler(request, response);
|
||||
} catch (ex) {
|
||||
print("**** Handler for", path, "failed:", ex, ex.stack);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
this._server.registerPathHandler(path, wrapper);
|
||||
}
|
||||
this._server.start(-1);
|
||||
},
|
||||
|
||||
stop() {
|
||||
return new Promise(resolve => {
|
||||
this._server.stop(resolve);
|
||||
this._server = null;
|
||||
});
|
||||
},
|
||||
|
||||
get host() {
|
||||
return "http://localhost:" + this._server.identity.primaryPort;
|
||||
},
|
||||
};
|
||||
|
||||
// An OAuth server that hands out tokens.
|
||||
function OAuthTokenServer() {
|
||||
let server;
|
||||
let handlers = {
|
||||
"/v1/authorization": (request, response) => {
|
||||
response.setStatusLine("1.1", 200, "OK");
|
||||
let token = "token" + server.numTokenFetches;
|
||||
print("Test OAuth server handing out token", token);
|
||||
server.numTokenFetches += 1;
|
||||
server.activeTokens.add(token);
|
||||
response.write(JSON.stringify({access_token: token}));
|
||||
},
|
||||
"/v1/destroy": (request, response) => {
|
||||
// Getting the body seems harder than it should be!
|
||||
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
|
||||
.createInstance(Ci.nsIScriptableInputStream);
|
||||
sis.init(request.bodyInputStream);
|
||||
let body = JSON.parse(sis.read(sis.available()));
|
||||
sis.close();
|
||||
let token = body.token;
|
||||
ok(server.activeTokens.delete(token));
|
||||
print("after destroy have", server.activeTokens.size, "tokens left.")
|
||||
response.setStatusLine("1.1", 200, "OK");
|
||||
response.write('{}');
|
||||
},
|
||||
}
|
||||
server = new Server(handlers);
|
||||
server.numTokenFetches = 0;
|
||||
server.activeTokens = new Set();
|
||||
return server;
|
||||
}
|
||||
|
||||
// The tests.
|
||||
function run_test() {
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
// Arrange for the first token we hand out to be rejected - the client should
|
||||
// notice the 401 and silently get a new token and retry the request.
|
||||
add_task(function testAuthRetry() {
|
||||
let handlers = {
|
||||
"/v1/batch": (request, response) => {
|
||||
// We know the first token we will get is "token0", so we simulate that
|
||||
// "expiring" by only accepting "token1". Then we just echo the response
|
||||
// back.
|
||||
let authHeader;
|
||||
try {
|
||||
authHeader = request.getHeader("Authorization");
|
||||
} catch (ex) {}
|
||||
if (authHeader != "Bearer token1") {
|
||||
response.setStatusLine("1.1", 401, "Unauthorized");
|
||||
response.write("wrong token");
|
||||
return;
|
||||
}
|
||||
response.setStatusLine("1.1", 200, "OK");
|
||||
response.write(JSON.stringify({ok: true}));
|
||||
}
|
||||
};
|
||||
let rlserver = new Server(handlers);
|
||||
rlserver.start();
|
||||
let authServer = OAuthTokenServer();
|
||||
authServer.start();
|
||||
try {
|
||||
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
|
||||
Services.prefs.setCharPref("identity.fxaccounts.remote.oauth.uri", authServer.host + "/v1");
|
||||
|
||||
let fxa = yield createMockFxA();
|
||||
let sc = new ServerClient(fxa);
|
||||
|
||||
let response = yield sc.request({
|
||||
path: "/batch",
|
||||
method: "post",
|
||||
body: {foo: "bar"},
|
||||
});
|
||||
equal(response.status, 200, "got the 200 we expected");
|
||||
equal(authServer.numTokenFetches, 2, "took 2 tokens to get the 200")
|
||||
deepEqual(response.body, {ok: true});
|
||||
} finally {
|
||||
yield authServer.stop();
|
||||
yield rlserver.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// Check that specified headers are seen by the server, and that server headers
|
||||
// in the response are seen by the client.
|
||||
add_task(function testHeaders() {
|
||||
let handlers = {
|
||||
"/v1/batch": (request, response) => {
|
||||
ok(request.hasHeader("x-foo"), "got our foo header");
|
||||
equal(request.getHeader("x-foo"), "bar", "foo header has the correct value");
|
||||
response.setHeader("Server-Sent-Header", "hello");
|
||||
response.setStatusLine("1.1", 200, "OK");
|
||||
response.write("{}");
|
||||
}
|
||||
};
|
||||
let rlserver = new Server(handlers);
|
||||
rlserver.start();
|
||||
try {
|
||||
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
|
||||
|
||||
let fxa = yield createMockFxA();
|
||||
let sc = new ServerClient(fxa);
|
||||
sc._getToken = () => Promise.resolve();
|
||||
|
||||
let response = yield sc.request({
|
||||
path: "/batch",
|
||||
method: "post",
|
||||
headers: {"X-Foo": "bar"},
|
||||
body: {foo: "bar"}});
|
||||
equal(response.status, 200, "got the 200 we expected");
|
||||
equal(response.headers["server-sent-header"], "hello", "got the server header");
|
||||
} finally {
|
||||
yield rlserver.stop();
|
||||
}
|
||||
});
|
||||
|
||||
// Check that unicode ends up as utf-8 in requests, and vice-versa in responses.
|
||||
// (Note the ServerClient assumes all strings in and out are UCS, and thus have
|
||||
// already been encoded/decoded (ie, it never expects to receive stuff already
|
||||
// utf-8 encoded, and never returns utf-8 encoded responses.)
|
||||
add_task(function testUTF8() {
|
||||
let handlers = {
|
||||
"/v1/hello": (request, response) => {
|
||||
// Get the body as bytes.
|
||||
let sis = Cc["@mozilla.org/scriptableinputstream;1"]
|
||||
.createInstance(Ci.nsIScriptableInputStream);
|
||||
sis.init(request.bodyInputStream);
|
||||
let body = sis.read(sis.available());
|
||||
sis.close();
|
||||
// The client sent "{"copyright: "\xa9"} where \xa9 is the copyright symbol.
|
||||
// It should have been encoded as utf-8 which is \xc2\xa9
|
||||
equal(body, '{"copyright":"\xc2\xa9"}', "server saw utf-8 encoded data");
|
||||
// and just write it back unchanged.
|
||||
response.setStatusLine("1.1", 200, "OK");
|
||||
response.write(body);
|
||||
}
|
||||
};
|
||||
let rlserver = new Server(handlers);
|
||||
rlserver.start();
|
||||
try {
|
||||
Services.prefs.setCharPref("readinglist.server", rlserver.host + "/v1");
|
||||
|
||||
let fxa = yield createMockFxA();
|
||||
let sc = new ServerClient(fxa);
|
||||
sc._getToken = () => Promise.resolve();
|
||||
|
||||
let body = {copyright: "\xa9"}; // see above - \xa9 is the copyright symbol
|
||||
let response = yield sc.request({
|
||||
path: "/hello",
|
||||
method: "post",
|
||||
body: body
|
||||
});
|
||||
equal(response.status, 200, "got the 200 we expected");
|
||||
deepEqual(response.body, body);
|
||||
} finally {
|
||||
yield rlserver.stop();
|
||||
}
|
||||
});
|
@ -3,5 +3,6 @@ head = head.js
|
||||
firefox-appdir = browser
|
||||
|
||||
[test_ReadingList.js]
|
||||
[test_ServerClient.js]
|
||||
[test_scheduler.js]
|
||||
[test_SQLiteStore.js]
|
||||
|
@ -45,7 +45,7 @@ const WINDOW_HIDEABLE_FEATURES = [
|
||||
];
|
||||
|
||||
// Messages that will be received via the Frame Message Manager.
|
||||
const FMM_MESSAGES = [
|
||||
const MESSAGES = [
|
||||
// The content script gives us a reference to an object that performs
|
||||
// synchronous collection of session data.
|
||||
"SessionStore:setupSyncHandler",
|
||||
@ -70,11 +70,15 @@ const FMM_MESSAGES = [
|
||||
// A tab that is being restored was reloaded. We call restoreTabContent to
|
||||
// finish restoring it right away.
|
||||
"SessionStore:reloadPendingTab",
|
||||
|
||||
// A crashed tab was revived by navigating to a different page. Remove its
|
||||
// browser from the list of crashed browsers to stop ignoring its messages.
|
||||
"SessionStore:crashedTabRevived",
|
||||
];
|
||||
|
||||
// The list of messages we accept from <xul:browser>s that have no tab
|
||||
// assigned. Those are for example the ones that preload about:newtab pages.
|
||||
const FMM_NOTAB_MESSAGES = new Set([
|
||||
const NOTAB_MESSAGES = new Set([
|
||||
// For a description see above.
|
||||
"SessionStore:setupSyncHandler",
|
||||
|
||||
@ -82,15 +86,13 @@ const FMM_NOTAB_MESSAGES = new Set([
|
||||
"SessionStore:update",
|
||||
]);
|
||||
|
||||
// Messages that will be received via the Parent Process Message Manager.
|
||||
const PPMM_MESSAGES = [
|
||||
// A tab is being revived from the crashed state. The sender of this
|
||||
// message should actually be running in the parent process, since this
|
||||
// will be the crashed tab interface. We use the Child and Parent Process
|
||||
// Message Managers because the message is sent during framescript unload
|
||||
// when the Frame Message Manager is not available.
|
||||
"SessionStore:RemoteTabRevived",
|
||||
];
|
||||
// The list of messages we want to receive even during the short period after a
|
||||
// frame has been removed from the DOM and before its frame script has finished
|
||||
// unloading.
|
||||
const CLOSED_MESSAGES = new Set([
|
||||
// For a description see above.
|
||||
"SessionStore:crashedTabRevived",
|
||||
]);
|
||||
|
||||
// These are tab events that we listen to.
|
||||
const TAB_EVENTS = [
|
||||
@ -423,8 +425,6 @@ let SessionStoreInternal = {
|
||||
Services.obs.addObserver(this, aTopic, true);
|
||||
}, this);
|
||||
|
||||
PPMM_MESSAGES.forEach(msg => ppmm.addMessageListener(msg, this));
|
||||
|
||||
this._initPrefs();
|
||||
this._initialized = true;
|
||||
},
|
||||
@ -554,8 +554,6 @@ let SessionStoreInternal = {
|
||||
|
||||
// Make sure to cancel pending saves.
|
||||
SessionSaver.cancel();
|
||||
|
||||
PPMM_MESSAGES.forEach(msg => ppmm.removeMessageListener(msg, this));
|
||||
},
|
||||
|
||||
/**
|
||||
@ -602,12 +600,6 @@ let SessionStoreInternal = {
|
||||
* and thus enables communication with OOP tabs.
|
||||
*/
|
||||
receiveMessage(aMessage) {
|
||||
// We'll deal with any Parent Process Message Manager messages first...
|
||||
if (aMessage.name == "SessionStore:RemoteTabRevived") {
|
||||
this._crashedBrowsers.delete(aMessage.objects.browser.permanentKey);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we got here, that means we're dealing with a frame message
|
||||
// manager message, so the target will be a <xul:browser>.
|
||||
var browser = aMessage.target;
|
||||
@ -616,7 +608,7 @@ let SessionStoreInternal = {
|
||||
|
||||
// Ensure we receive only specific messages from <xul:browser>s that
|
||||
// have no tab assigned, e.g. the ones that preload about:newtab pages.
|
||||
if (!tab && !FMM_NOTAB_MESSAGES.has(aMessage.name)) {
|
||||
if (!tab && !NOTAB_MESSAGES.has(aMessage.name)) {
|
||||
throw new Error(`received unexpected message '${aMessage.name}' ` +
|
||||
`from a browser that has no tab`);
|
||||
}
|
||||
@ -709,6 +701,9 @@ let SessionStoreInternal = {
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "SessionStore:crashedTabRevived":
|
||||
this._crashedBrowsers.delete(browser.permanentKey);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`received unknown message '${aMessage.name}'`);
|
||||
break;
|
||||
@ -799,7 +794,10 @@ let SessionStoreInternal = {
|
||||
aWindow.__SSi = this._generateWindowID();
|
||||
|
||||
let mm = aWindow.getGroupMessageManager("browsers");
|
||||
FMM_MESSAGES.forEach(msg => mm.addMessageListener(msg, this));
|
||||
MESSAGES.forEach(msg => {
|
||||
let listenWhenClosed = CLOSED_MESSAGES.has(msg);
|
||||
mm.addMessageListener(msg, this, listenWhenClosed);
|
||||
});
|
||||
|
||||
// Load the frame script after registering listeners.
|
||||
mm.loadFrameScript("chrome://browser/content/content-sessionStore.js", true);
|
||||
@ -1128,7 +1126,7 @@ let SessionStoreInternal = {
|
||||
DyingWindowCache.set(aWindow, winData);
|
||||
|
||||
let mm = aWindow.getGroupMessageManager("browsers");
|
||||
FMM_MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
|
||||
MESSAGES.forEach(msg => mm.removeMessageListener(msg, this));
|
||||
|
||||
delete aWindow.__SSi;
|
||||
},
|
||||
|
@ -749,12 +749,8 @@ function handleRevivedTab() {
|
||||
|
||||
removeEventListener("pagehide", handleRevivedTab);
|
||||
|
||||
// We can't send a message using the frame message manager because by
|
||||
// the time we reach the unload event handler, it's "too late", and messages
|
||||
// won't be sent or received. The child-process message manager works though,
|
||||
// despite the fact that we're really running in the parent process.
|
||||
let browser = docShell.chromeEventHandler;
|
||||
cpmm.sendAsyncMessage("SessionStore:RemoteTabRevived", null, {browser: browser});
|
||||
// Notify the parent.
|
||||
sendAsyncMessage("SessionStore:crashedTabRevived");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,8 +31,8 @@ const Telemetry = devtools.require("devtools/shared/telemetry");
|
||||
|
||||
const TABS_OPEN_PEAK_HISTOGRAM = "DEVTOOLS_TABS_OPEN_PEAK_LINEAR";
|
||||
const TABS_OPEN_AVG_HISTOGRAM = "DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR";
|
||||
const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL";
|
||||
const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_EXPONENTIAL";
|
||||
const TABS_PINNED_PEAK_HISTOGRAM = "DEVTOOLS_TABS_PINNED_PEAK_LINEAR";
|
||||
const TABS_PINNED_AVG_HISTOGRAM = "DEVTOOLS_TABS_PINNED_AVERAGE_LINEAR";
|
||||
|
||||
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
|
||||
const MAX_ORDINAL = 99;
|
||||
@ -47,6 +47,7 @@ this.DevTools = function DevTools() {
|
||||
this._tools = new Map(); // Map<toolId, tool>
|
||||
this._themes = new Map(); // Map<themeId, theme>
|
||||
this._toolboxes = new Map(); // Map<target, toolbox>
|
||||
this._telemetry = new Telemetry();
|
||||
|
||||
// destroy() is an observer's handler so we need to preserve context.
|
||||
this.destroy = this.destroy.bind(this);
|
||||
|
@ -5,599 +5,70 @@
|
||||
"use strict";
|
||||
|
||||
const { Ci, Cc } = require("chrome");
|
||||
const { getJSON } = require("devtools/shared/getjson");
|
||||
const { Services } = require("resource://gre/modules/Services.jsm");
|
||||
const promise = require("promise");
|
||||
|
||||
const DEVICES_URL = "devtools.devices.url";
|
||||
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/device.properties");
|
||||
|
||||
/* `Devices` is a catalog of existing devices and their properties, intended
|
||||
* for (mobile) device emulation tools and features.
|
||||
/* This is a catalog of common web-enabled devices and their properties,
|
||||
* intended for (mobile) device emulation.
|
||||
*
|
||||
* The properties of a device are:
|
||||
* - name: Device brand and model(s).
|
||||
* - width: Viewport width.
|
||||
* - height: Viewport height.
|
||||
* - pixelRatio: Screen pixel ratio to viewport.
|
||||
* - userAgent: Device UserAgent string.
|
||||
* - touch: Whether the screen is touch-enabled.
|
||||
* - name: brand and model(s).
|
||||
* - width: viewport width.
|
||||
* - height: viewport height.
|
||||
* - pixelRatio: ratio from viewport to physical screen pixels.
|
||||
* - userAgent: UA string of the device's browser.
|
||||
* - touch: whether it has a touch screen.
|
||||
* - firefoxOS: whether Firefox OS is supported.
|
||||
*
|
||||
* To add more devices to this catalog, either patch this file, or push new
|
||||
* device descriptions from your own code (e.g. an addon) like so:
|
||||
* The device types are:
|
||||
* ["phones", "tablets", "laptops", "televisions", "consoles", "watches"].
|
||||
*
|
||||
* You can easily add more devices to this catalog from your own code (e.g. an
|
||||
* addon) like so:
|
||||
*
|
||||
* var myPhone = { name: "My Phone", ... };
|
||||
* require("devtools/shared/devices").Devices.Others.phones.push(myPhone);
|
||||
* require("devtools/shared/devices").AddDevice(myPhone, "phones");
|
||||
*/
|
||||
|
||||
let Devices = {
|
||||
Types: ["phones", "tablets", "notebooks", "televisions", "watches"],
|
||||
// Local devices catalog that addons can add to.
|
||||
let localDevices = {};
|
||||
|
||||
// Get the localized string of a device type.
|
||||
GetString(deviceType) {
|
||||
// Add a device to the local catalog.
|
||||
function AddDevice(device, type = "phones") {
|
||||
let list = localDevices[type];
|
||||
if (!list) {
|
||||
list = localDevices[type] = [];
|
||||
}
|
||||
list.push(device);
|
||||
}
|
||||
exports.AddDevice = AddDevice;
|
||||
|
||||
// Get the complete devices catalog.
|
||||
function GetDevices(bypassCache = false) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
// Fetch common devices from Mozilla's CDN.
|
||||
getJSON(DEVICES_URL, bypassCache).then(devices => {
|
||||
for (let type in localDevices) {
|
||||
if (!devices[type]) {
|
||||
devices.TYPES.push(type);
|
||||
devices[type] = [];
|
||||
}
|
||||
devices[type] = localDevices[type].concat(devices[type]);
|
||||
}
|
||||
deferred.resolve(devices);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
exports.GetDevices = GetDevices;
|
||||
|
||||
// Get the localized string for a device type.
|
||||
function GetDeviceString(deviceType) {
|
||||
return Strings.GetStringFromName("device." + deviceType);
|
||||
},
|
||||
};
|
||||
exports.Devices = Devices;
|
||||
|
||||
|
||||
// The `Devices.FirefoxOS` list was put together from various sources online.
|
||||
Devices.FirefoxOS = {
|
||||
phones: [
|
||||
{
|
||||
name: "Firefox OS Flame",
|
||||
width: 320,
|
||||
height: 570,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Alcatel One Touch Fire, Fire C",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Alcatel Fire E",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Mobile; ALCATELOneTouch4012X; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Geeksphone Keon",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Geeksphone Peak, Revolution",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Intex Cloud Fx",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "LG Fireweb",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; LG-D300; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Spice Fire One Mi-FX1",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Symphony GoFox F15",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Zen Fire 105",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "ZTE Open",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; ZTEOPEN; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "ZTE Open C",
|
||||
width: 320,
|
||||
height: 450,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Mobile; OPENC; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
],
|
||||
tablets: [
|
||||
{
|
||||
name: "Foxconn InFocus",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "VIA Vixen",
|
||||
width: 1024,
|
||||
height: 600,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:28.0) Gecko/28.0 Firefox/28.0",
|
||||
touch: true,
|
||||
},
|
||||
],
|
||||
notebooks: [
|
||||
],
|
||||
televisions: [
|
||||
{
|
||||
name: "720p HD Television",
|
||||
width: 1280,
|
||||
height: 720,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: false,
|
||||
},
|
||||
{
|
||||
name: "1080p Full HD Television",
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: false,
|
||||
},
|
||||
{
|
||||
name: "4K Ultra HD Television",
|
||||
width: 3840,
|
||||
height: 2160,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: false,
|
||||
},
|
||||
],
|
||||
watches: [
|
||||
{
|
||||
name: "LG G Watch",
|
||||
width: 280,
|
||||
height: 280,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "LG G Watch R",
|
||||
width: 320,
|
||||
height: 320,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Moto 360",
|
||||
width: 320,
|
||||
height: 290,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Gear Live",
|
||||
width: 320,
|
||||
height: 320,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// `Devices.Others` was derived from the Chromium source code:
|
||||
// - chromium/src/third_party/WebKit/Source/devtools/front_end/toolbox/OverridesUI.js
|
||||
Devices.Others = {
|
||||
phones: [
|
||||
{
|
||||
name: "Apple iPhone 3GS",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Apple iPhone 4",
|
||||
width: 320,
|
||||
height: 480,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Apple iPhone 5",
|
||||
width: 320,
|
||||
height: 568,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 7_0 like Mac OS X; en-us) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Apple iPhone 6",
|
||||
width: 375,
|
||||
height: 667,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Apple iPhone 6 Plus",
|
||||
width: 414,
|
||||
height: 736,
|
||||
pixelRatio: 3,
|
||||
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 8_0 like Mac OS X) AppleWebKit/600.1.3 (KHTML, like Gecko) Version/8.0 Mobile/12A4345d Safari/600.1.4",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "BlackBerry Z10",
|
||||
width: 384,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "BlackBerry Z30",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (BB10; Touch) AppleWebKit/537.10+ (KHTML, like Gecko) Version/10.0.9.2372 Mobile Safari/537.10+",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Google Nexus 4",
|
||||
width: 384,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 4 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Google Nexus 5",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 3,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.2.1; en-us; Nexus 5 Build/JOP40D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Google Nexus S",
|
||||
width: 320,
|
||||
height: 533,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3.4; en-us; Nexus S Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "HTC Evo, Touch HD, Desire HD, Desire",
|
||||
width: 320,
|
||||
height: 533,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Sprint APA9292KT Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "HTC One X, EVO LTE",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.0.3; HTC One X Build/IML74K) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "HTC Sensation, Evo 3D",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; HTC Sensation Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "LG Optimus 2X, Optimus 3D, Optimus Black",
|
||||
width: 320,
|
||||
height: 533,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; LG-P990/V08c Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1 MMS/LG-Android-MMS-V1.0/1.2",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "LG Optimus G",
|
||||
width: 384,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.0; LG-E975 Build/IMM76L) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Mobile Safari/535.19",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "LG Optimus LTE, Optimus 4X HD",
|
||||
width: 424,
|
||||
height: 753,
|
||||
pixelRatio: 1.7,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; LG-P930 Build/GRJ90) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "LG Optimus One",
|
||||
width: 213,
|
||||
height: 320,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2.1; en-us; LG-MS690 Build/FRG83) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Motorola Defy, Droid, Droid X, Milestone",
|
||||
width: 320,
|
||||
height: 569,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.0; en-us; Milestone Build/ SHOLS_U2_01.03.1) AppleWebKit/530.17 (KHTML, like Gecko) Version/4.0 Mobile Safari/530.17",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Motorola Droid 3, Droid 4, Droid Razr, Atrix 4G, Atrix 2",
|
||||
width: 540,
|
||||
height: 960,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; Droid Build/FRG22D) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Motorola Droid Razr HD",
|
||||
width: 720,
|
||||
height: 1280,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; DROID RAZR 4G Build/6.5.1-73_DHD-11_M1-29) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Nokia C5, C6, C7, N97, N8, X7",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 1,
|
||||
userAgent: "NokiaN97/21.1.107 (SymbianOS/9.4; Series60/5.0 Mozilla/5.0; Profile/MIDP-2.1 Configuration/CLDC-1.1) AppleWebkit/525 (KHTML, like Gecko) BrowserNG/7.1.4",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Nokia Lumia 7X0, Lumia 8XX, Lumia 900, N800, N810, N900",
|
||||
width: 320,
|
||||
height: 533,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (compatible; MSIE 10.0; Windows Phone 8.0; Trident/6.0; IEMobile/10.0; ARM; Touch; NOKIA; Lumia 820)",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy Note 3",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 3,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 4.3; en-us; SM-N900T Build/JSS15J) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy Note II",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 4.1; en-us; GT-N7100 Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy Note",
|
||||
width: 400,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SAMSUNG-SGH-I717 Build/GINGERBREAD) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy S III, Galaxy Nexus",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; GT-I9300 Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy S, S II, W",
|
||||
width: 320,
|
||||
height: 533,
|
||||
pixelRatio: 1.5,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.1; en-us; GT-I9000 Build/ECLAIR) AppleWebKit/525.10+ (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy S4",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 3,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.2.2; GT-I9505 Build/JDQ39) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.59 Mobile Safari/537.36",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Sony Xperia S, Ion",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 4.0; en-us; LT28at Build/6.1.C.1.111) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Sony Xperia Sola, U",
|
||||
width: 480,
|
||||
height: 854,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.3; en-us; SonyEricssonST25i Build/6.0.B.1.564) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Sony Xperia Z, Z1",
|
||||
width: 360,
|
||||
height: 640,
|
||||
pixelRatio: 3,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 4.2; en-us; SonyC6903 Build/14.1.G.1.518) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30",
|
||||
touch: true,
|
||||
},
|
||||
],
|
||||
tablets: [
|
||||
{
|
||||
name: "Amazon Kindle Fire HDX 7″",
|
||||
width: 1920,
|
||||
height: 1200,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; en-us; KFTHWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Amazon Kindle Fire HDX 8.9″",
|
||||
width: 2560,
|
||||
height: 1600,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; en-us; KFAPWI Build/JDQ39) AppleWebKit/535.19 (KHTML, like Gecko) Silk/3.13 Safari/535.19 Silk-Accelerated=true",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Amazon Kindle Fire (First Generation)",
|
||||
width: 1024,
|
||||
height: 600,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_3; en-us; Silk/1.0.141.16-Gen4_11004310) AppleWebkit/533.16 (KHTML, like Gecko) Version/5.0 Safari/533.16 Silk-Accelerated=true",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Apple iPad 1 / 2 / iPad Mini",
|
||||
width: 1024,
|
||||
height: 768,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (iPad; CPU OS 4_3_5 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8L1 Safari/6533.18.5",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Apple iPad 3 / 4",
|
||||
width: 1024,
|
||||
height: 768,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (iPad; CPU OS 7_0 like Mac OS X) AppleWebKit/537.51.1 (KHTML, like Gecko) Version/7.0 Mobile/11A465 Safari/9537.53",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "BlackBerry PlayBook",
|
||||
width: 1024,
|
||||
height: 600,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (PlayBook; U; RIM Tablet OS 2.1.0; en-US) AppleWebKit/536.2+ (KHTML like Gecko) Version/7.2.1.0 Safari/536.2+",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Google Nexus 10",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 10 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Google Nexus 7 2",
|
||||
width: 960,
|
||||
height: 600,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Google Nexus 7",
|
||||
width: 966,
|
||||
height: 604,
|
||||
pixelRatio: 1.325,
|
||||
userAgent: "Mozilla/5.0 (Linux; Android 4.3; Nexus 7 Build/JSS15Q) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/29.0.1547.72 Safari/537.36",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Motorola Xoom, Xyboard",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 3.0; en-us; Xoom Build/HRI39) AppleWebKit/525.10 (KHTML, like Gecko) Version/3.0.4 Mobile Safari/523.12.2",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy Tab 7.7, 8.9, 10.1",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Samsung Galaxy Tab",
|
||||
width: 1024,
|
||||
height: 600,
|
||||
pixelRatio: 1,
|
||||
userAgent: "Mozilla/5.0 (Linux; U; Android 2.2; en-us; SCH-I800 Build/FROYO) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1",
|
||||
touch: true,
|
||||
},
|
||||
],
|
||||
notebooks: [
|
||||
{
|
||||
name: "Notebook with touch",
|
||||
width: 1280,
|
||||
height: 950,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: true,
|
||||
},
|
||||
{
|
||||
name: "Notebook with HiDPI screen",
|
||||
width: 1440,
|
||||
height: 900,
|
||||
pixelRatio: 2,
|
||||
userAgent: "",
|
||||
touch: false,
|
||||
},
|
||||
{
|
||||
name: "Generic notebook",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
pixelRatio: 1,
|
||||
userAgent: "",
|
||||
touch: false,
|
||||
},
|
||||
],
|
||||
televisions: [
|
||||
],
|
||||
watches: [
|
||||
],
|
||||
};
|
||||
}
|
||||
exports.GetDeviceString = GetDeviceString;
|
||||
|
@ -3,23 +3,22 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const {Cu, CC} = require("chrome");
|
||||
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
const promise = require("promise");
|
||||
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const XMLHttpRequest = CC("@mozilla.org/xmlextras/xmlhttprequest;1");
|
||||
|
||||
function getJSON(bypassCache, pref) {
|
||||
// Downloads and caches a JSON file from a URL given by the pref.
|
||||
exports.getJSON = function (prefName, bypassCache) {
|
||||
if (!bypassCache) {
|
||||
try {
|
||||
let str = Services.prefs.getCharPref(pref + "_cache");
|
||||
let str = Services.prefs.getCharPref(prefName + "_cache");
|
||||
let json = JSON.parse(str);
|
||||
return promise.resolve(json);
|
||||
} catch(e) {/* no pref or invalid json. Let's continue */}
|
||||
}
|
||||
|
||||
|
||||
let deferred = promise.defer();
|
||||
|
||||
let xhr = new XMLHttpRequest();
|
||||
|
||||
xhr.onload = () => {
|
||||
@ -27,9 +26,9 @@ function getJSON(bypassCache, pref) {
|
||||
try {
|
||||
json = JSON.parse(xhr.responseText);
|
||||
} catch(e) {
|
||||
return deferred.reject("Not valid JSON");
|
||||
return deferred.reject("Invalid JSON");
|
||||
}
|
||||
Services.prefs.setCharPref(pref + "_cache", xhr.responseText);
|
||||
Services.prefs.setCharPref(prefName + "_cache", xhr.responseText);
|
||||
deferred.resolve(json);
|
||||
}
|
||||
|
||||
@ -37,18 +36,8 @@ function getJSON(bypassCache, pref) {
|
||||
deferred.reject("Network error");
|
||||
}
|
||||
|
||||
xhr.open("get", Services.prefs.getCharPref(pref));
|
||||
xhr.open("get", Services.prefs.getCharPref(prefName));
|
||||
xhr.send();
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
|
||||
|
||||
exports.GetTemplatesJSON = function(bypassCache) {
|
||||
return getJSON(bypassCache, "devtools.webide.templatesURL");
|
||||
}
|
||||
|
||||
exports.GetAddonsJSON = function(bypassCache) {
|
||||
return getJSON(bypassCache, "devtools.webide.addonsURL");
|
||||
}
|
@ -51,6 +51,7 @@ EXTRA_JS_MODULES.devtools.shared += [
|
||||
'devices.js',
|
||||
'doorhanger.js',
|
||||
'frame-script-utils.js',
|
||||
'getjson.js',
|
||||
'inplace-editor.js',
|
||||
'observable-object.js',
|
||||
'options-view.js',
|
||||
@ -61,6 +62,7 @@ EXTRA_JS_MODULES.devtools.shared += [
|
||||
]
|
||||
|
||||
EXTRA_JS_MODULES.devtools.shared.widgets += [
|
||||
'widgets/CubicBezierPresets.js',
|
||||
'widgets/CubicBezierWidget.js',
|
||||
'widgets/FastListWidget.js',
|
||||
'widgets/Spectrum.js',
|
||||
|
@ -7,6 +7,7 @@ support-files =
|
||||
browser_templater_basic.html
|
||||
browser_toolbar_basic.html
|
||||
browser_toolbar_webconsole_errors_count.html
|
||||
browser_devices.json
|
||||
doc_options-view.xul
|
||||
head.js
|
||||
leakhunt.js
|
||||
@ -15,6 +16,9 @@ support-files =
|
||||
[browser_cubic-bezier-01.js]
|
||||
[browser_cubic-bezier-02.js]
|
||||
[browser_cubic-bezier-03.js]
|
||||
[browser_cubic-bezier-04.js]
|
||||
[browser_cubic-bezier-05.js]
|
||||
[browser_cubic-bezier-06.js]
|
||||
[browser_flame-graph-01.js]
|
||||
[browser_flame-graph-02.js]
|
||||
[browser_flame-graph-03a.js]
|
||||
@ -98,3 +102,4 @@ skip-if = buildapp == 'mulet' || e10s # The developertoolbar error count isn't c
|
||||
[browser_treeWidget_basic.js]
|
||||
[browser_treeWidget_keyboard_interaction.js]
|
||||
[browser_treeWidget_mouse_interaction.js]
|
||||
[browser_devices.js]
|
||||
|
@ -7,16 +7,20 @@
|
||||
// Tests that the CubicBezierWidget generates content in a given parent node
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierWidget} = devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
const {CubicBezierWidget} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
info("Checking that the markup is created in the parent");
|
||||
info("Checking that the graph markup is created in the parent");
|
||||
let container = doc.querySelector("#container");
|
||||
let w = new CubicBezierWidget(container);
|
||||
|
||||
ok(container.querySelector(".display-wrap"),
|
||||
"The display has been added");
|
||||
|
||||
ok(container.querySelector(".coordinate-plane"),
|
||||
"The coordinate plane has been added");
|
||||
let buttons = container.querySelectorAll("button");
|
||||
|
@ -7,26 +7,37 @@
|
||||
// Tests the CubicBezierWidget events
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierWidget, PREDEFINED} =
|
||||
const {CubicBezierWidget} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
// Required or widget will be clipped inside of 'bottom'
|
||||
// host by -14. Setting `fixed` zeroes this which is needed for
|
||||
// calculating offsets. Occurs in test env only.
|
||||
doc.body.setAttribute("style", "position: fixed");
|
||||
|
||||
let container = doc.querySelector("#container");
|
||||
let w = new CubicBezierWidget(container, PREDEFINED.linear);
|
||||
|
||||
yield pointsCanBeDragged(w, win, doc);
|
||||
yield curveCanBeClicked(w, win, doc);
|
||||
yield pointsCanBeMovedWithKeyboard(w, win, doc);
|
||||
let rect = w.curve.getBoundingClientRect();
|
||||
rect.graphTop = rect.height * w.bezierCanvas.padding[0];
|
||||
rect.graphBottom = rect.height - rect.graphTop;
|
||||
rect.graphHeight = rect.graphBottom - rect.graphTop;
|
||||
|
||||
yield pointsCanBeDragged(w, win, doc, rect);
|
||||
yield curveCanBeClicked(w, win, doc, rect);
|
||||
yield pointsCanBeMovedWithKeyboard(w, win, doc, rect);
|
||||
|
||||
w.destroy();
|
||||
host.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
function* pointsCanBeDragged(widget, win, doc) {
|
||||
function* pointsCanBeDragged(widget, win, doc, offsets) {
|
||||
info("Checking that the control points can be dragged with the mouse");
|
||||
|
||||
info("Listening for the update event");
|
||||
@ -34,7 +45,7 @@ function* pointsCanBeDragged(widget, win, doc) {
|
||||
|
||||
info("Generating a mousedown/move/up on P1");
|
||||
widget._onPointMouseDown({target: widget.p1});
|
||||
doc.onmousemove({pageX: 0, pageY: 100});
|
||||
doc.onmousemove({pageX: offsets.left, pageY: offsets.graphTop});
|
||||
doc.onmouseup();
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
@ -48,7 +59,7 @@ function* pointsCanBeDragged(widget, win, doc) {
|
||||
|
||||
info("Generating a mousedown/move/up on P2");
|
||||
widget._onPointMouseDown({target: widget.p2});
|
||||
doc.onmousemove({pageX: 200, pageY: 300});
|
||||
doc.onmousemove({pageX: offsets.right, pageY: offsets.graphBottom});
|
||||
doc.onmouseup();
|
||||
|
||||
bezier = yield onUpdated;
|
||||
@ -56,14 +67,16 @@ function* pointsCanBeDragged(widget, win, doc) {
|
||||
is(bezier.P2[1], 0, "The new P2 progress coordinate is correct");
|
||||
}
|
||||
|
||||
function* curveCanBeClicked(widget, win, doc) {
|
||||
function* curveCanBeClicked(widget, win, doc, offsets) {
|
||||
info("Checking that clicking on the curve moves the closest control point");
|
||||
|
||||
info("Listening for the update event");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Click close to P1");
|
||||
widget._onCurveClick({pageX: 50, pageY: 150});
|
||||
let x = offsets.left + (offsets.width / 4.0);
|
||||
let y = offsets.graphTop + (offsets.graphHeight / 4.0);
|
||||
widget._onCurveClick({pageX: x, pageY: y});
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
ok(true, "The widget fired the updated event");
|
||||
@ -76,7 +89,9 @@ function* curveCanBeClicked(widget, win, doc) {
|
||||
onUpdated = widget.once("updated");
|
||||
|
||||
info("Click close to P2");
|
||||
widget._onCurveClick({pageX: 150, pageY: 250});
|
||||
x = offsets.right - (offsets.width / 4);
|
||||
y = offsets.graphBottom - (offsets.graphHeight / 4);
|
||||
widget._onCurveClick({pageX: x, pageY: y});
|
||||
|
||||
bezier = yield onUpdated;
|
||||
is(bezier.P2[0], 0.75, "The new P2 time coordinate is correct");
|
||||
@ -85,57 +100,89 @@ function* curveCanBeClicked(widget, win, doc) {
|
||||
is(bezier.P1[1], 0.75, "P1 progress coordinate remained unchanged");
|
||||
}
|
||||
|
||||
function* pointsCanBeMovedWithKeyboard(widget, win, doc) {
|
||||
function* pointsCanBeMovedWithKeyboard(widget, win, doc, offsets) {
|
||||
info("Checking that points respond to keyboard events");
|
||||
|
||||
let singleStep = 3;
|
||||
let shiftStep = 30;
|
||||
|
||||
info("Moving P1 to the left");
|
||||
let newOffset = parseInt(widget.p1.style.left) - singleStep;
|
||||
let x = widget.bezierCanvas.
|
||||
offsetsToCoordinates({style: {left: newOffset}})[0];
|
||||
|
||||
let onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 37));
|
||||
let bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
|
||||
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the left, fast");
|
||||
newOffset = parseInt(widget.p1.style.left) - shiftStep;
|
||||
x = widget.bezierCanvas.
|
||||
offsetsToCoordinates({style: {left: newOffset}})[0];
|
||||
|
||||
onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 37, true));
|
||||
bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.085, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the right, fast");
|
||||
newOffset = parseInt(widget.p1.style.left) + shiftStep;
|
||||
x = widget.bezierCanvas.
|
||||
offsetsToCoordinates({style: {left: newOffset}})[0];
|
||||
|
||||
onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 39, true));
|
||||
bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.75, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the bottom");
|
||||
newOffset = parseInt(widget.p1.style.top) + singleStep;
|
||||
let y = widget.bezierCanvas.
|
||||
offsetsToCoordinates({style: {top: newOffset}})[1];
|
||||
|
||||
onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 40));
|
||||
bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
|
||||
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the bottom, fast");
|
||||
newOffset = parseInt(widget.p1.style.top) + shiftStep;
|
||||
y = widget.bezierCanvas.
|
||||
offsetsToCoordinates({style: {top: newOffset}})[1];
|
||||
|
||||
onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 40, true));
|
||||
bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.585, "The new P1 progress coordinate is correct");
|
||||
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Moving P1 to the top, fast");
|
||||
newOffset = parseInt(widget.p1.style.top) - shiftStep;
|
||||
y = widget.bezierCanvas.
|
||||
offsetsToCoordinates({style: {top: newOffset}})[1];
|
||||
|
||||
onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p1, 38, true));
|
||||
bezier = yield onUpdated;
|
||||
is(bezier.P1[0], 0.235, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], 0.735, "The new P1 progress coordinate is correct");
|
||||
is(bezier.P1[0], x, "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], y, "The new P1 progress coordinate is correct");
|
||||
|
||||
info("Checking that keyboard events also work with P2");
|
||||
info("Moving P2 to the left");
|
||||
newOffset = parseInt(widget.p2.style.left) - singleStep;
|
||||
x = widget.bezierCanvas.
|
||||
offsetsToCoordinates({style: {left: newOffset}})[0];
|
||||
|
||||
onUpdated = widget.once("updated");
|
||||
widget._onPointKeyDown(getKeyEvent(widget.p2, 37));
|
||||
bezier = yield onUpdated;
|
||||
is(bezier.P2[0], 0.735, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[0], x, "The new P2 time coordinate is correct");
|
||||
is(bezier.P2[1], 0.25, "The new P2 progress coordinate is correct");
|
||||
}
|
||||
|
||||
|
@ -7,8 +7,9 @@
|
||||
// Tests that coordinates can be changed programatically in the CubicBezierWidget
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierWidget, PREDEFINED} =
|
||||
const {CubicBezierWidget} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
const {PREDEFINED} = require("devtools/shared/widgets/CubicBezierPresets");
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
|
51
browser/devtools/shared/test/browser_cubic-bezier-04.js
Normal file
51
browser/devtools/shared/test/browser_cubic-bezier-04.js
Normal file
@ -0,0 +1,51 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that the CubicBezierPresetWidget generates markup.
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierPresetWidget} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
let container = doc.querySelector("#container");
|
||||
let w = new CubicBezierPresetWidget(container);
|
||||
|
||||
info("Checking that the presets are created in the parent");
|
||||
ok(container.querySelector(".preset-pane"),
|
||||
"The preset pane has been added");
|
||||
|
||||
ok(container.querySelector("#preset-categories"),
|
||||
"The preset categories have been added");
|
||||
let categories = container.querySelectorAll(".category");
|
||||
is(categories.length, Object.keys(PRESETS).length,
|
||||
"The preset categories have been added");
|
||||
Object.keys(PRESETS).forEach(category => {
|
||||
ok(container.querySelector("#" + category), `${category} has been added`);
|
||||
ok(container.querySelector("#preset-category-" + category),
|
||||
`The preset list for ${category} has been added.`);
|
||||
});
|
||||
|
||||
info("Checking that each of the presets and its preview have been added");
|
||||
Object.keys(PRESETS).forEach(category => {
|
||||
Object.keys(PRESETS[category]).forEach(presetLabel => {
|
||||
let preset = container.querySelector("#" + presetLabel);
|
||||
ok(preset, `${presetLabel} has been added`);
|
||||
ok(preset.querySelector("canvas"),
|
||||
`${presetLabel}'s canvas preview has been added`);
|
||||
ok(preset.querySelector("p"),
|
||||
`${presetLabel}'s label has been added`);
|
||||
});
|
||||
});
|
||||
|
||||
w.destroy();
|
||||
host.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
49
browser/devtools/shared/test/browser_cubic-bezier-05.js
Normal file
49
browser/devtools/shared/test/browser_cubic-bezier-05.js
Normal file
@ -0,0 +1,49 @@
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests that the CubicBezierPresetWidget cycles menus
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierPresetWidget} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} =
|
||||
require("devtools/shared/widgets/CubicBezierPresets");
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
let container = doc.querySelector("#container");
|
||||
let w = new CubicBezierPresetWidget(container);
|
||||
|
||||
info("Checking that preset is selected if coordinates are known");
|
||||
|
||||
w.refreshMenu([0, 0, 0, 0]);
|
||||
is(w.activeCategory, container.querySelector(`#${DEFAULT_PRESET_CATEGORY}`),
|
||||
"The default category is selected");
|
||||
is(w._activePreset, null, "There is no selected category");
|
||||
|
||||
w.refreshMenu(PREDEFINED["linear"]);
|
||||
is(w.activeCategory, container.querySelector("#ease-in-out"),
|
||||
"The ease-in-out category is active");
|
||||
is(w._activePreset, container.querySelector("#ease-in-out-linear"),
|
||||
"The ease-in-out-linear preset is active");
|
||||
|
||||
w.refreshMenu(PRESETS["ease-out"]["ease-out-sine"]);
|
||||
is(w.activeCategory, container.querySelector("#ease-out"),
|
||||
"The ease-out category is active");
|
||||
is(w._activePreset, container.querySelector("#ease-out-sine"),
|
||||
"The ease-out-sine preset is active");
|
||||
|
||||
w.refreshMenu([0, 0, 0, 0]);
|
||||
is(w.activeCategory, container.querySelector("#ease-out"),
|
||||
"The ease-out category is still active");
|
||||
is(w._activePreset, null, "No preset is active");
|
||||
|
||||
w.destroy();
|
||||
host.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
80
browser/devtools/shared/test/browser_cubic-bezier-06.js
Normal file
80
browser/devtools/shared/test/browser_cubic-bezier-06.js
Normal file
@ -0,0 +1,80 @@
|
||||
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the integration between CubicBezierWidget and CubicBezierPresets
|
||||
|
||||
const TEST_URI = "chrome://browser/content/devtools/cubic-bezier-frame.xhtml";
|
||||
const {CubicBezierWidget} =
|
||||
devtools.require("devtools/shared/widgets/CubicBezierWidget");
|
||||
const {PRESETS} = require("devtools/shared/widgets/CubicBezierPresets");
|
||||
|
||||
add_task(function*() {
|
||||
yield promiseTab("about:blank");
|
||||
let [host, win, doc] = yield createHost("bottom", TEST_URI);
|
||||
|
||||
let container = doc.querySelector("#container");
|
||||
let w = new CubicBezierWidget(container,
|
||||
PRESETS["ease-in"]["ease-in-sine"]);
|
||||
w.presets.refreshMenu(PRESETS["ease-in"]["ease-in-sine"]);
|
||||
|
||||
let rect = w.curve.getBoundingClientRect();
|
||||
rect.graphTop = rect.height * w.bezierCanvas.padding[0];
|
||||
|
||||
yield adjustingBezierUpdatesPreset(w, win, doc, rect);
|
||||
yield selectingPresetUpdatesBezier(w, win, doc, rect);
|
||||
|
||||
w.destroy();
|
||||
host.destroy();
|
||||
gBrowser.removeCurrentTab();
|
||||
});
|
||||
|
||||
function* adjustingBezierUpdatesPreset(widget, win, doc, rect) {
|
||||
info("Checking that changing the bezier refreshes the preset menu");
|
||||
|
||||
is(widget.presets.activeCategory,
|
||||
doc.querySelector("#ease-in"),
|
||||
"The selected category is ease-in");
|
||||
|
||||
is(widget.presets._activePreset,
|
||||
doc.querySelector("#ease-in-sine"),
|
||||
"The selected preset is ease-in-sine");
|
||||
|
||||
info("Generating custom bezier curve by dragging");
|
||||
widget._onPointMouseDown({target: widget.p1});
|
||||
doc.onmousemove({pageX: rect.left, pageY: rect.graphTop});
|
||||
doc.onmouseup();
|
||||
|
||||
is(widget.presets.activeCategory,
|
||||
doc.querySelector("#ease-in"),
|
||||
"The selected category is still ease-in");
|
||||
|
||||
is(widget.presets._activePreset, null,
|
||||
"There is no active preset");
|
||||
}
|
||||
|
||||
function* selectingPresetUpdatesBezier(widget, win, doc, rect) {
|
||||
info("Checking that selecting a preset updates bezier curve");
|
||||
|
||||
info("Listening for the new coordinates event");
|
||||
let onNewCoordinates = widget.presets.once("new-coordinates");
|
||||
let onUpdated = widget.once("updated");
|
||||
|
||||
info("Click a preset");
|
||||
let preset = doc.querySelector("#ease-in-sine");
|
||||
widget.presets._onPresetClick({currentTarget: preset});
|
||||
|
||||
yield onNewCoordinates;
|
||||
ok(true, "The preset widget fired the new-coordinates event");
|
||||
|
||||
let bezier = yield onUpdated;
|
||||
ok(true, "The bezier canvas fired the updated event");
|
||||
|
||||
is(bezier.P1[0], preset.coordinates[0], "The new P1 time coordinate is correct");
|
||||
is(bezier.P1[1], preset.coordinates[1], "The new P1 progress coordinate is correct");
|
||||
is(bezier.P2[0], preset.coordinates[2], "P2 time coordinate is correct ");
|
||||
is(bezier.P2[1], preset.coordinates[3], "P2 progress coordinate is correct");
|
||||
}
|
50
browser/devtools/shared/test/browser_devices.js
Normal file
50
browser/devtools/shared/test/browser_devices.js
Normal file
@ -0,0 +1,50 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
let { GetDevices, GetDeviceString, AddDevice } = devtools.require("devtools/shared/devices");
|
||||
|
||||
add_task(function*() {
|
||||
Services.prefs.setCharPref("devtools.devices.url", TEST_URI_ROOT + "browser_devices.json");
|
||||
|
||||
let devices = yield GetDevices();
|
||||
|
||||
is(devices.TYPES.length, 1, "Found 1 device type.");
|
||||
|
||||
let type1 = devices.TYPES[0];
|
||||
|
||||
is(devices[type1].length, 2, "Found 2 devices of type #1.");
|
||||
|
||||
let string = GetDeviceString(type1);
|
||||
ok(typeof string === "string" && string.length > 0, "Able to localize type #1.");
|
||||
|
||||
let device1 = {
|
||||
name: "SquarePhone",
|
||||
width: 320,
|
||||
height: 320,
|
||||
pixelRatio: 2,
|
||||
userAgent: "Mozilla/5.0 (Mobile; rv:42.0)",
|
||||
touch: true,
|
||||
firefoxOS: true
|
||||
};
|
||||
AddDevice(device1, type1);
|
||||
devices = yield GetDevices();
|
||||
|
||||
is(devices[type1].length, 3, "Added new device of type #1.");
|
||||
ok(devices[type1].filter(d => d.name === device1.name), "Found the new device.");
|
||||
|
||||
let type2 = "appliances";
|
||||
let device2 = {
|
||||
name: "Mr Freezer",
|
||||
width: 800,
|
||||
height: 600,
|
||||
pixelRatio: 5,
|
||||
userAgent: "Mozilla/5.0 (Appliance; rv:42.0)",
|
||||
touch: true,
|
||||
firefoxOS: true
|
||||
};
|
||||
AddDevice(device2, type2);
|
||||
devices = yield GetDevices();
|
||||
|
||||
is(devices.TYPES.length, 2, "Added device type #2.");
|
||||
is(devices[type2].length, 1, "Added new device of type #2.");
|
||||
});
|
23
browser/devtools/shared/test/browser_devices.json
Normal file
23
browser/devtools/shared/test/browser_devices.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"TYPES": [ "phones" ],
|
||||
"phones": [
|
||||
{
|
||||
"name": "Small Phone",
|
||||
"width": 320,
|
||||
"height": 480,
|
||||
"pixelRatio": 1,
|
||||
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
|
||||
"touch": true,
|
||||
"firefoxOS": true
|
||||
},
|
||||
{
|
||||
"name": "Big Phone",
|
||||
"width": 360,
|
||||
"height": 640,
|
||||
"pixelRatio": 3,
|
||||
"userAgent": "Mozilla/5.0 (Mobile; rv:39.0) Gecko/39.0 Firefox/39.0",
|
||||
"touch": true,
|
||||
"firefoxOS": true
|
||||
}
|
||||
]
|
||||
}
|
@ -17,14 +17,18 @@ function* performTest() {
|
||||
let [host, win, doc] = yield createHost();
|
||||
let graph = new LineGraphWidget(doc.body, "fps");
|
||||
yield graph.once("ready");
|
||||
|
||||
testGraph(graph);
|
||||
|
||||
testGraph(graph, normalDragStop);
|
||||
yield graph.destroy();
|
||||
|
||||
let graph2 = new LineGraphWidget(doc.body, "fps");
|
||||
yield graph2.once("ready");
|
||||
testGraph(graph2, buggyDragStop);
|
||||
yield graph2.destroy();
|
||||
|
||||
host.destroy();
|
||||
}
|
||||
|
||||
function testGraph(graph) {
|
||||
function testGraph(graph, dragStop) {
|
||||
graph.setData(TEST_DATA);
|
||||
|
||||
info("Making a selection.");
|
||||
@ -186,13 +190,24 @@ function dragStart(graph, x, y = 1) {
|
||||
graph._onMouseDown({ clientX: x, clientY: y });
|
||||
}
|
||||
|
||||
function dragStop(graph, x, y = 1) {
|
||||
function normalDragStop(graph, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
graph._onMouseMove({ clientX: x, clientY: y });
|
||||
graph._onMouseUp({ clientX: x, clientY: y });
|
||||
}
|
||||
|
||||
function buggyDragStop(graph, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
|
||||
// Only fire a mousemove instead of a mouseup.
|
||||
// This happens when the mouseup happens outside of the toolbox,
|
||||
// see Bug 1066504.
|
||||
graph._onMouseMove({ clientX: x, clientY: y });
|
||||
graph._onMouseMove({ clientX: x, clientY: y, buttons: 0 });
|
||||
}
|
||||
|
||||
function scroll(graph, wheel, x, y = 1) {
|
||||
x /= window.devicePixelRatio;
|
||||
y /= window.devicePixelRatio;
|
||||
|
@ -104,7 +104,10 @@ function getCanvasMock(w=200, h=400) {
|
||||
stroke: () => {},
|
||||
arc: () => {},
|
||||
fill: () => {},
|
||||
bezierCurveTo: () => {}
|
||||
bezierCurveTo: () => {},
|
||||
save: () => {},
|
||||
restore: () => {},
|
||||
setTransform: () => {}
|
||||
};
|
||||
},
|
||||
width: w,
|
||||
|
@ -19,6 +19,7 @@ function run_test() {
|
||||
coordinatesToStringOutputsAString();
|
||||
pointGettersReturnPointCoordinatesArrays();
|
||||
toStringOutputsCubicBezierValue();
|
||||
toStringOutputsCssPresetValues();
|
||||
}
|
||||
|
||||
function throwsWhenMissingCoordinates() {
|
||||
@ -84,8 +85,27 @@ function pointGettersReturnPointCoordinatesArrays() {
|
||||
function toStringOutputsCubicBezierValue() {
|
||||
do_print("toString() outputs the cubic-bezier() value");
|
||||
|
||||
let c = new CubicBezier([0, 1, 1, 0]);
|
||||
do_check_eq(c.toString(), "cubic-bezier(0,1,1,0)");
|
||||
}
|
||||
|
||||
function toStringOutputsCssPresetValues() {
|
||||
do_print("toString() outputs the css predefined values");
|
||||
|
||||
let c = new CubicBezier([0, 0, 1, 1]);
|
||||
do_check_eq(c.toString(), "cubic-bezier(0,0,1,1)");
|
||||
do_check_eq(c.toString(), "linear");
|
||||
|
||||
c = new CubicBezier([0.25, 0.1, 0.25, 1]);
|
||||
do_check_eq(c.toString(), "ease");
|
||||
|
||||
c = new CubicBezier([0.42, 0, 1, 1]);
|
||||
do_check_eq(c.toString(), "ease-in");
|
||||
|
||||
c = new CubicBezier([0, 0, 0.58, 1]);
|
||||
do_check_eq(c.toString(), "ease-out");
|
||||
|
||||
c = new CubicBezier([0.42, 0, 0.58, 1]);
|
||||
do_check_eq(c.toString(), "ease-in-out");
|
||||
}
|
||||
|
||||
function do_check_throws(cb, info) {
|
||||
|
64
browser/devtools/shared/widgets/CubicBezierPresets.js
Normal file
64
browser/devtools/shared/widgets/CubicBezierPresets.js
Normal file
@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
// Set of preset definitions for use with CubicBezierWidget
|
||||
// Credit: http://easings.net
|
||||
|
||||
"use strict";
|
||||
|
||||
const PREDEFINED = {
|
||||
"ease": [0.25, 0.1, 0.25, 1],
|
||||
"linear": [0, 0, 1, 1],
|
||||
"ease-in": [0.42, 0, 1, 1],
|
||||
"ease-out": [0, 0, 0.58, 1],
|
||||
"ease-in-out": [0.42, 0, 0.58, 1]
|
||||
};
|
||||
|
||||
const PRESETS = {
|
||||
"ease-in": {
|
||||
"ease-in-linear": [0, 0, 1, 1],
|
||||
"ease-in-ease-in": [0.42, 0, 1, 1],
|
||||
"ease-in-sine": [0.47, 0, 0.74, 0.71],
|
||||
"ease-in-quadratic": [0.55, 0.09, 0.68, 0.53],
|
||||
"ease-in-cubic": [0.55, 0.06, 0.68, 0.19],
|
||||
"ease-in-quartic": [0.9, 0.03, 0.69, 0.22],
|
||||
"ease-in-quintic": [0.76, 0.05, 0.86, 0.06],
|
||||
"ease-in-exponential": [0.95, 0.05, 0.8, 0.04],
|
||||
"ease-in-circular": [0.6, 0.04, 0.98, 0.34],
|
||||
"ease-in-backward": [0.6, -0.28, 0.74, 0.05]
|
||||
},
|
||||
"ease-out": {
|
||||
"ease-out-linear": [0, 0, 1, 1],
|
||||
"ease-out-ease-out": [0, 0, 0.58, 1],
|
||||
"ease-out-sine": [0.39, 0.58, 0.57, 1],
|
||||
"ease-out-quadratic": [0.25, 0.46, 0.45, 0.94],
|
||||
"ease-out-cubic": [0.22, 0.61, 0.36, 1],
|
||||
"ease-out-quartic": [0.17, 0.84, 0.44, 1],
|
||||
"ease-out-quintic": [0.23, 1, 0.32, 1],
|
||||
"ease-out-exponential": [0.19, 1, 0.22, 1],
|
||||
"ease-out-circular": [0.08, 0.82, 0.17, 1],
|
||||
"ease-out-backward": [0.18, 0.89, 0.32, 1.28]
|
||||
},
|
||||
"ease-in-out": {
|
||||
"ease-in-out-linear": [0, 0, 1, 1],
|
||||
"ease-in-out-ease": [0.25, 0.1, 0.25, 1],
|
||||
"ease-in-out-ease-in-out": [0.42, 0, 0.58, 1],
|
||||
"ease-in-out-sine": [0.45, 0.05, 0.55, 0.95],
|
||||
"ease-in-out-quadratic": [0.46, 0.03, 0.52, 0.96],
|
||||
"ease-in-out-cubic": [0.65, 0.05, 0.36, 1],
|
||||
"ease-in-out-quartic": [0.77, 0, 0.18, 1],
|
||||
"ease-in-out-quintic": [0.86, 0, 0.07, 1],
|
||||
"ease-in-out-exponential": [1, 0, 0, 1],
|
||||
"ease-in-out-circular": [0.79, 0.14, 0.15, 0.86],
|
||||
"ease-in-out-backward": [0.68, -0.55, 0.27, 1.55]
|
||||
}
|
||||
};
|
||||
|
||||
const DEFAULT_PRESET_CATEGORY = Object.keys(PRESETS)[0];
|
||||
|
||||
exports.PRESETS = PRESETS;
|
||||
exports.PREDEFINED = PREDEFINED;
|
||||
exports.DEFAULT_PRESET_CATEGORY = DEFAULT_PRESET_CATEGORY;
|
@ -27,14 +27,7 @@
|
||||
|
||||
const EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
const {setTimeout, clearTimeout} = require("sdk/timers");
|
||||
|
||||
const PREDEFINED = exports.PREDEFINED = {
|
||||
"ease": [.25, .1, .25, 1],
|
||||
"linear": [0, 0, 1, 1],
|
||||
"ease-in": [.42, 0, 1, 1],
|
||||
"ease-out": [0, 0, .58, 1],
|
||||
"ease-in-out": [.42, 0, .58, 1]
|
||||
};
|
||||
const {PREDEFINED, PRESETS, DEFAULT_PRESET_CATEGORY} = require("devtools/shared/widgets/CubicBezierPresets");
|
||||
|
||||
/**
|
||||
* CubicBezier data structure helper
|
||||
@ -59,7 +52,7 @@ function CubicBezier(coordinates) {
|
||||
return this.map(n => {
|
||||
return (Math.round(n * 100)/100 + '').replace(/^0\./, '.');
|
||||
}) + "";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
exports.CubicBezier = CubicBezier;
|
||||
@ -74,7 +67,11 @@ CubicBezier.prototype = {
|
||||
},
|
||||
|
||||
toString: function() {
|
||||
return 'cubic-bezier(' + this.coordinates + ')';
|
||||
// Check first if current coords are one of css predefined functions
|
||||
let predefName = Object.keys(PREDEFINED)
|
||||
.find(key => coordsAreEqual(PREDEFINED[key], this.coordinates));
|
||||
|
||||
return predefName || 'cubic-bezier(' + this.coordinates + ')';
|
||||
}
|
||||
};
|
||||
|
||||
@ -97,7 +94,7 @@ function BezierCanvas(canvas, bezier, padding) {
|
||||
-canvas.height * (1 - p[0] - p[2]));
|
||||
this.ctx.translate(p[3] / (1 - p[1] - p[3]),
|
||||
-1 - p[0] / (1 - p[0] - p[2]));
|
||||
};
|
||||
}
|
||||
|
||||
exports.BezierCanvas = BezierCanvas;
|
||||
|
||||
@ -115,7 +112,7 @@ BezierCanvas.prototype = {
|
||||
}, {
|
||||
left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px',
|
||||
top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px'
|
||||
}]
|
||||
}];
|
||||
},
|
||||
|
||||
/**
|
||||
@ -128,8 +125,8 @@ BezierCanvas.prototype = {
|
||||
p = p.map(function(a, i) { return a * (i % 2? w : h)});
|
||||
|
||||
return [
|
||||
(parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]),
|
||||
(h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2])
|
||||
(parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]),
|
||||
(h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2])
|
||||
];
|
||||
},
|
||||
|
||||
@ -143,15 +140,22 @@ BezierCanvas.prototype = {
|
||||
handleColor: '#666',
|
||||
handleThickness: .008,
|
||||
bezierColor: '#4C9ED9',
|
||||
bezierThickness: .015
|
||||
bezierThickness: .015,
|
||||
drawHandles: true
|
||||
};
|
||||
|
||||
for (let setting in settings) {
|
||||
defaultSettings[setting] = settings[setting];
|
||||
}
|
||||
|
||||
this.ctx.clearRect(-.5,-.5, 2, 2);
|
||||
// Clear the canvas –making sure to clear the
|
||||
// whole area by resetting the transform first.
|
||||
this.ctx.save();
|
||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.ctx.restore();
|
||||
|
||||
if (defaultSettings.drawHandles) {
|
||||
// Draw control handles
|
||||
this.ctx.beginPath();
|
||||
this.ctx.fillStyle = defaultSettings.handleColor;
|
||||
@ -166,16 +170,17 @@ BezierCanvas.prototype = {
|
||||
this.ctx.stroke();
|
||||
this.ctx.closePath();
|
||||
|
||||
function circle(ctx, cx, cy, r) {
|
||||
var circle = function(ctx, cx, cy, r) {
|
||||
return ctx.beginPath();
|
||||
ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
|
||||
ctx.closePath();
|
||||
}
|
||||
};
|
||||
|
||||
circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
|
||||
this.ctx.fill();
|
||||
circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
|
||||
this.ctx.fill();
|
||||
}
|
||||
|
||||
// Draw bezier curve
|
||||
this.ctx.beginPath();
|
||||
@ -197,18 +202,20 @@ BezierCanvas.prototype = {
|
||||
* Emits "updated" events whenever the curve is changed. Along with the event is
|
||||
* sent a CubicBezier object
|
||||
*/
|
||||
function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
|
||||
function CubicBezierWidget(parent, coordinates=PRESETS["ease-in"]["ease-in-sine"]) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.parent = parent;
|
||||
let {curve, p1, p2} = this._initMarkup();
|
||||
|
||||
this.curve = curve;
|
||||
this.curveBoundingBox = curve.getBoundingClientRect();
|
||||
this.curve = curve;
|
||||
this.p1 = p1;
|
||||
this.p2 = p2;
|
||||
|
||||
// Create and plot the bezier curve
|
||||
this.bezierCanvas = new BezierCanvas(this.curve,
|
||||
new CubicBezier(coordinates), [.25, 0]);
|
||||
new CubicBezier(coordinates), [0.30, 0]);
|
||||
this.bezierCanvas.plot();
|
||||
|
||||
// Place the control points
|
||||
@ -221,12 +228,15 @@ function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
|
||||
this._onPointMouseDown = this._onPointMouseDown.bind(this);
|
||||
this._onPointKeyDown = this._onPointKeyDown.bind(this);
|
||||
this._onCurveClick = this._onCurveClick.bind(this);
|
||||
this._initEvents();
|
||||
this._onNewCoordinates = this._onNewCoordinates.bind(this);
|
||||
|
||||
// Add preset preview menu
|
||||
this.presets = new CubicBezierPresetWidget(parent);
|
||||
|
||||
// Add the timing function previewer
|
||||
this.timingPreview = new TimingFunctionPreviewWidget(parent);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
this._initEvents();
|
||||
}
|
||||
|
||||
exports.CubicBezierWidget = CubicBezierWidget;
|
||||
@ -235,6 +245,9 @@ CubicBezierWidget.prototype = {
|
||||
_initMarkup: function() {
|
||||
let doc = this.parent.ownerDocument;
|
||||
|
||||
let wrap = doc.createElement("div");
|
||||
wrap.className = "display-wrap";
|
||||
|
||||
let plane = doc.createElement("div");
|
||||
plane.className = "coordinate-plane";
|
||||
|
||||
@ -249,22 +262,24 @@ CubicBezierWidget.prototype = {
|
||||
plane.appendChild(p2);
|
||||
|
||||
let curve = doc.createElement("canvas");
|
||||
curve.setAttribute("height", "400");
|
||||
curve.setAttribute("width", "200");
|
||||
curve.setAttribute("width", 150);
|
||||
curve.setAttribute("height", 370);
|
||||
curve.id = "curve";
|
||||
plane.appendChild(curve);
|
||||
|
||||
this.parent.appendChild(plane);
|
||||
plane.appendChild(curve);
|
||||
wrap.appendChild(plane);
|
||||
|
||||
this.parent.appendChild(wrap);
|
||||
|
||||
return {
|
||||
p1: p1,
|
||||
p2: p2,
|
||||
curve: curve
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
_removeMarkup: function() {
|
||||
this.parent.ownerDocument.querySelector(".coordinate-plane").remove();
|
||||
this.parent.ownerDocument.querySelector(".display-wrap").remove();
|
||||
},
|
||||
|
||||
_initEvents: function() {
|
||||
@ -275,6 +290,8 @@ CubicBezierWidget.prototype = {
|
||||
this.p2.addEventListener("keydown", this._onPointKeyDown);
|
||||
|
||||
this.curve.addEventListener("click", this._onCurveClick);
|
||||
|
||||
this.presets.on("new-coordinates", this._onNewCoordinates);
|
||||
},
|
||||
|
||||
_removeEvents: function() {
|
||||
@ -285,6 +302,8 @@ CubicBezierWidget.prototype = {
|
||||
this.p2.removeEventListener("keydown", this._onPointKeyDown);
|
||||
|
||||
this.curve.removeEventListener("click", this._onCurveClick);
|
||||
|
||||
this.presets.off("new-coordinates", this._onNewCoordinates);
|
||||
},
|
||||
|
||||
_onPointMouseDown: function(event) {
|
||||
@ -317,7 +336,7 @@ CubicBezierWidget.prototype = {
|
||||
doc.onmouseup = function () {
|
||||
point.focus();
|
||||
doc.onmousemove = doc.onmouseup = null;
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
_onPointKeyDown: function(event) {
|
||||
@ -344,6 +363,8 @@ CubicBezierWidget.prototype = {
|
||||
},
|
||||
|
||||
_onCurveClick: function(event) {
|
||||
this.curveBoundingBox = this.curve.getBoundingClientRect();
|
||||
|
||||
let left = this.curveBoundingBox.left;
|
||||
let top = this.curveBoundingBox.top;
|
||||
let x = event.pageX - left;
|
||||
@ -362,14 +383,19 @@ CubicBezierWidget.prototype = {
|
||||
this._updateFromPoints();
|
||||
},
|
||||
|
||||
_onNewCoordinates: function(event, coordinates) {
|
||||
this.coordinates = coordinates;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current point coordinates and redraw the curve to match
|
||||
*/
|
||||
_updateFromPoints: function() {
|
||||
// Get the new coordinates from the point's offsets
|
||||
let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1)
|
||||
let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1);
|
||||
coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2));
|
||||
|
||||
this.presets.refreshMenu(coordinates);
|
||||
this._redraw(coordinates);
|
||||
},
|
||||
|
||||
@ -391,7 +417,7 @@ CubicBezierWidget.prototype = {
|
||||
* @param {Array} coordinates
|
||||
*/
|
||||
set coordinates(coordinates) {
|
||||
this._redraw(coordinates)
|
||||
this._redraw(coordinates);
|
||||
|
||||
// Move the points
|
||||
let offsets = this.bezierCanvas.offsets;
|
||||
@ -420,6 +446,7 @@ CubicBezierWidget.prototype = {
|
||||
coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat);
|
||||
}
|
||||
|
||||
this.presets.refreshMenu(coordinates);
|
||||
this.coordinates = coordinates;
|
||||
},
|
||||
|
||||
@ -428,11 +455,262 @@ CubicBezierWidget.prototype = {
|
||||
this._removeMarkup();
|
||||
|
||||
this.timingPreview.destroy();
|
||||
this.presets.destroy();
|
||||
|
||||
this.curve = this.p1 = this.p2 = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CubicBezierPreset widget.
|
||||
* Builds a menu of presets from CubicBezierPresets
|
||||
* @param {DOMNode} parent The container where the preset panel should be created
|
||||
*
|
||||
* Emits "new-coordinate" event along with the coordinates
|
||||
* whenever a preset is selected.
|
||||
*/
|
||||
function CubicBezierPresetWidget(parent) {
|
||||
this.parent = parent;
|
||||
|
||||
let {presetPane, presets, categories} = this._initMarkup();
|
||||
this.presetPane = presetPane;
|
||||
this.presets = presets;
|
||||
this.categories = categories;
|
||||
|
||||
this._activeCategory = null;
|
||||
this._activePresetList = null;
|
||||
this._activePreset = null;
|
||||
|
||||
this._onCategoryClick = this._onCategoryClick.bind(this);
|
||||
this._onPresetClick = this._onPresetClick.bind(this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
this._initEvents();
|
||||
}
|
||||
|
||||
exports.CubicBezierPresetWidget = CubicBezierPresetWidget;
|
||||
|
||||
CubicBezierPresetWidget.prototype = {
|
||||
/*
|
||||
* Constructs a list of all preset categories and a list
|
||||
* of presets for each category.
|
||||
*
|
||||
* High level markup:
|
||||
* div .preset-pane
|
||||
* div .preset-categories
|
||||
* div .category
|
||||
* div .category
|
||||
* ...
|
||||
* div .preset-container
|
||||
* div .presetList
|
||||
* div .preset
|
||||
* ...
|
||||
* div .presetList
|
||||
* div .preset
|
||||
* ...
|
||||
*/
|
||||
_initMarkup: function() {
|
||||
let doc = this.parent.ownerDocument;
|
||||
|
||||
let presetPane = doc.createElement("div");
|
||||
presetPane.className = "preset-pane";
|
||||
|
||||
let categoryList = doc.createElement("div");
|
||||
categoryList.id = "preset-categories";
|
||||
|
||||
let presetContainer = doc.createElement("div");
|
||||
presetContainer.id = "preset-container";
|
||||
|
||||
Object.keys(PRESETS).forEach(categoryLabel => {
|
||||
let category = this._createCategory(categoryLabel);
|
||||
categoryList.appendChild(category);
|
||||
|
||||
let presetList = this._createPresetList(categoryLabel);
|
||||
presetContainer.appendChild(presetList);
|
||||
});
|
||||
|
||||
presetPane.appendChild(categoryList);
|
||||
presetPane.appendChild(presetContainer);
|
||||
|
||||
this.parent.appendChild(presetPane);
|
||||
|
||||
let allCategories = presetPane.querySelectorAll(".category");
|
||||
let allPresets = presetPane.querySelectorAll(".preset");
|
||||
|
||||
return {
|
||||
presetPane: presetPane,
|
||||
presets: allPresets,
|
||||
categories: allCategories
|
||||
};
|
||||
},
|
||||
|
||||
_createCategory: function(categoryLabel) {
|
||||
let doc = this.parent.ownerDocument;
|
||||
|
||||
let category = doc.createElement("div");
|
||||
category.id = categoryLabel;
|
||||
category.classList.add("category");
|
||||
|
||||
let categoryDisplayLabel = this._normalizeCategoryLabel(categoryLabel);
|
||||
category.textContent = categoryDisplayLabel;
|
||||
|
||||
return category;
|
||||
},
|
||||
|
||||
_normalizeCategoryLabel: function(categoryLabel) {
|
||||
return categoryLabel.replace("/-/g", " ");
|
||||
},
|
||||
|
||||
_createPresetList: function(categoryLabel) {
|
||||
let doc = this.parent.ownerDocument;
|
||||
|
||||
let presetList = doc.createElement("div");
|
||||
presetList.id = "preset-category-" + categoryLabel;
|
||||
presetList.classList.add("preset-list");
|
||||
|
||||
Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
|
||||
let preset = this._createPreset(categoryLabel, presetLabel);
|
||||
presetList.appendChild(preset);
|
||||
});
|
||||
|
||||
return presetList;
|
||||
},
|
||||
|
||||
_createPreset: function(categoryLabel, presetLabel) {
|
||||
let doc = this.parent.ownerDocument;
|
||||
|
||||
let preset = doc.createElement("div");
|
||||
preset.classList.add("preset");
|
||||
preset.id = presetLabel;
|
||||
preset.coordinates = PRESETS[categoryLabel][presetLabel];
|
||||
|
||||
// Create preset preview
|
||||
let curve = doc.createElement("canvas");
|
||||
let bezier = new CubicBezier(preset.coordinates);
|
||||
|
||||
curve.setAttribute("height", 55);
|
||||
curve.setAttribute("width", 55);
|
||||
|
||||
preset.bezierCanvas = new BezierCanvas(curve, bezier, [0.15, 0]);
|
||||
preset.bezierCanvas.plot({
|
||||
drawHandles: false,
|
||||
bezierThickness: 0.025
|
||||
});
|
||||
|
||||
preset.appendChild(curve);
|
||||
|
||||
// Create preset label
|
||||
let presetLabelElem = doc.createElement("p");
|
||||
let presetDisplayLabel = this._normalizePresetLabel(categoryLabel, presetLabel);
|
||||
presetLabelElem.textContent = presetDisplayLabel;
|
||||
preset.appendChild(presetLabelElem);
|
||||
|
||||
return preset;
|
||||
},
|
||||
|
||||
_normalizePresetLabel: function(categoryLabel, presetLabel) {
|
||||
return presetLabel.replace(categoryLabel + "-", "").replace("/-/g", " ");
|
||||
},
|
||||
|
||||
_initEvents: function() {
|
||||
for (let category of this.categories) {
|
||||
category.addEventListener("click", this._onCategoryClick);
|
||||
}
|
||||
|
||||
for (let preset of this.presets) {
|
||||
preset.addEventListener("click", this._onPresetClick);
|
||||
}
|
||||
},
|
||||
|
||||
_removeEvents: function() {
|
||||
for (let category of this.categories) {
|
||||
category.removeEventListener("click", this._onCategoryClick);
|
||||
}
|
||||
|
||||
for (let preset of this.presets) {
|
||||
preset.removeEventListener("click", this._onPresetClick);
|
||||
}
|
||||
},
|
||||
|
||||
_onPresetClick: function(event) {
|
||||
this.emit("new-coordinates", event.currentTarget.coordinates);
|
||||
this.activePreset = event.currentTarget;
|
||||
},
|
||||
|
||||
_onCategoryClick: function(event) {
|
||||
this.activeCategory = event.target;
|
||||
},
|
||||
|
||||
_setActivePresetList: function(presetListId) {
|
||||
let presetList = this.presetPane.querySelector("#" + presetListId);
|
||||
swapClassName("active-preset-list", this._activePresetList, presetList);
|
||||
this._activePresetList = presetList;
|
||||
},
|
||||
|
||||
set activeCategory(category) {
|
||||
swapClassName("active-category", this._activeCategory, category);
|
||||
this._activeCategory = category;
|
||||
this._setActivePresetList("preset-category-" + category.id);
|
||||
},
|
||||
|
||||
get activeCategory() {
|
||||
return this._activeCategory;
|
||||
},
|
||||
|
||||
set activePreset(preset) {
|
||||
swapClassName("active-preset", this._activePreset, preset);
|
||||
this._activePreset = preset;
|
||||
},
|
||||
|
||||
get activePreset() {
|
||||
return this._activePreset;
|
||||
},
|
||||
|
||||
/**
|
||||
* Called by CubicBezierWidget onload and when
|
||||
* the curve is modified via the canvas.
|
||||
* Attempts to match the new user setting with an
|
||||
* existing preset.
|
||||
* @param {Array} coordinates new coords [i, j, k, l]
|
||||
*/
|
||||
refreshMenu: function(coordinates) {
|
||||
// If we cannot find a matching preset, keep
|
||||
// menu on last known preset category.
|
||||
let category = this._activeCategory;
|
||||
|
||||
// If we cannot find a matching preset
|
||||
// deselect any selected preset.
|
||||
let preset = null;
|
||||
|
||||
// If a category has never been viewed before
|
||||
// show the default category.
|
||||
if (!category) {
|
||||
category = this.parent.querySelector("#" + DEFAULT_PRESET_CATEGORY);
|
||||
}
|
||||
|
||||
// If the new coordinates do match a preset,
|
||||
// set its category and preset button as active.
|
||||
Object.keys(PRESETS).forEach(categoryLabel => {
|
||||
|
||||
Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
|
||||
if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) {
|
||||
category = this.parent.querySelector("#" + categoryLabel);
|
||||
preset = this.parent.querySelector("#" + presetLabel);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
this.activeCategory = category;
|
||||
this.activePreset = preset;
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
this._removeEvents();
|
||||
this.parent.querySelector(".preset-pane").remove();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The TimingFunctionPreviewWidget animates a dot on a scale with a given
|
||||
* timing-function
|
||||
@ -554,3 +832,29 @@ function isValidTimingFunction(value) {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a class from a node and adds it to another.
|
||||
* @param {String} className the class to swap
|
||||
* @param {DOMNode} from the node to remove the class from
|
||||
* @param {DOMNode} to the node to add the class to
|
||||
*/
|
||||
function swapClassName(className, from, to) {
|
||||
if (from !== null) {
|
||||
from.classList.remove(className);
|
||||
}
|
||||
|
||||
if (to !== null) {
|
||||
to.classList.add(className);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two arrays of coordinates [i, j, k, l]
|
||||
* @param {Array} c1 first coordinate array to compare
|
||||
* @param {Array} c2 second coordinate array to compare
|
||||
* @return {Boolean}
|
||||
*/
|
||||
function coordsAreEqual(c1, c2) {
|
||||
return c1.reduce((prev, curr, index) => prev && (curr === c2[index]), true);
|
||||
}
|
||||
|
@ -181,6 +181,7 @@ this.AbstractCanvasGraph = function(parent, name, sharpness) {
|
||||
this._selection = new GraphArea();
|
||||
this._selectionDragger = new GraphAreaDragger();
|
||||
this._selectionResizer = new GraphAreaResizer();
|
||||
this._isMouseActive = false;
|
||||
|
||||
this._onAnimationFrame = this._onAnimationFrame.bind(this);
|
||||
this._onMouseMove = this._onMouseMove.bind(this);
|
||||
@ -952,13 +953,23 @@ AbstractCanvasGraph.prototype = {
|
||||
* Listener for the "mousemove" event on the graph's container.
|
||||
*/
|
||||
_onMouseMove: function(e) {
|
||||
let resizer = this._selectionResizer;
|
||||
let dragger = this._selectionDragger;
|
||||
|
||||
// If a mouseup happened outside the toolbox and the current operation
|
||||
// is causing the selection changed, then end it.
|
||||
if (e.buttons == 0 && (this.hasSelectionInProgress() ||
|
||||
resizer.margin != null ||
|
||||
dragger.origin != null)) {
|
||||
return this._onMouseUp(e);
|
||||
}
|
||||
|
||||
let offset = this._getContainerOffset();
|
||||
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
|
||||
let mouseY = (e.clientY - offset.top) * this._pixelRatio;
|
||||
this._cursor.x = mouseX;
|
||||
this._cursor.y = mouseY;
|
||||
|
||||
let resizer = this._selectionResizer;
|
||||
if (resizer.margin != null) {
|
||||
this._selection[resizer.margin] = mouseX;
|
||||
this._shouldRedraw = true;
|
||||
@ -966,7 +977,6 @@ AbstractCanvasGraph.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
let dragger = this._selectionDragger;
|
||||
if (dragger.origin != null) {
|
||||
this._selection.start = dragger.anchor.start - dragger.origin + mouseX;
|
||||
this._selection.end = dragger.anchor.end - dragger.origin + mouseX;
|
||||
@ -1013,6 +1023,7 @@ AbstractCanvasGraph.prototype = {
|
||||
* Listener for the "mousedown" event on the graph's container.
|
||||
*/
|
||||
_onMouseDown: function(e) {
|
||||
this._isMouseActive = true;
|
||||
let offset = this._getContainerOffset();
|
||||
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
|
||||
|
||||
@ -1051,6 +1062,7 @@ AbstractCanvasGraph.prototype = {
|
||||
* Listener for the "mouseup" event on the graph's container.
|
||||
*/
|
||||
_onMouseUp: function(e) {
|
||||
this._isMouseActive = false;
|
||||
let offset = this._getContainerOffset();
|
||||
let mouseX = (e.clientX - offset.left) * this._pixelRatio;
|
||||
|
||||
@ -1163,19 +1175,15 @@ AbstractCanvasGraph.prototype = {
|
||||
|
||||
/**
|
||||
* Listener for the "mouseout" event on the graph's container.
|
||||
* Clear any active cursors if a drag isn't happening.
|
||||
*/
|
||||
_onMouseOut: function() {
|
||||
if (this.hasSelectionInProgress()) {
|
||||
this.dropSelection();
|
||||
}
|
||||
|
||||
_onMouseOut: function(e) {
|
||||
if (!this._isMouseActive) {
|
||||
this._cursor.x = null;
|
||||
this._cursor.y = null;
|
||||
this._selectionResizer.margin = null;
|
||||
this._selectionDragger.origin = null;
|
||||
|
||||
this._canvas.removeAttribute("input");
|
||||
this._shouldRedraw = true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -795,8 +795,8 @@ Tooltip.prototype = {
|
||||
// Create an iframe to host the cubic-bezier widget
|
||||
let iframe = this.doc.createElementNS(XHTML_NS, "iframe");
|
||||
iframe.setAttribute("transparent", true);
|
||||
iframe.setAttribute("width", "200");
|
||||
iframe.setAttribute("height", "415");
|
||||
iframe.setAttribute("width", "410");
|
||||
iframe.setAttribute("height", "360");
|
||||
iframe.setAttribute("flex", "1");
|
||||
iframe.setAttribute("class", "devtools-tooltip-iframe");
|
||||
|
||||
|
@ -8,14 +8,15 @@
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link rel="stylesheet" href="chrome://browser/skin/devtools/common.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" ype="text/css"/>
|
||||
<link rel="stylesheet" href="chrome://browser/content/devtools/cubic-bezier.css" type="text/css"/>
|
||||
<script type="application/javascript;version=1.8" src="theme-switching.js"/>
|
||||
<style>
|
||||
body {
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 200px;
|
||||
height: 415px;
|
||||
overflow: hidden;
|
||||
width: 410px;
|
||||
height: 370px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
@ -5,32 +5,28 @@
|
||||
/* Based on Lea Verou www.cubic-bezier.com
|
||||
See https://github.com/LeaVerou/cubic-bezier */
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
width: 410px;
|
||||
height: 370px;
|
||||
flex-direction: row-reverse;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.display-wrap {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Coordinate Plane */
|
||||
|
||||
.coordinate-plane {
|
||||
position: absolute;
|
||||
line-height: 0;
|
||||
height: 400px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.coordinate-plane:before,
|
||||
.coordinate-plane:after {
|
||||
position: absolute;
|
||||
bottom: 25%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.coordinate-plane:before {
|
||||
content: "";
|
||||
border-bottom: 2px solid;
|
||||
transform: rotate(-90deg) translateY(2px);
|
||||
transform-origin: bottom left;
|
||||
}
|
||||
|
||||
.coordinate-plane:after {
|
||||
content: "";
|
||||
border-top: 2px solid;
|
||||
margin-bottom: -2px;
|
||||
width: 150px;
|
||||
height: 370px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.theme-dark .coordinate-plane:before,
|
||||
@ -50,45 +46,41 @@
|
||||
outline: none;
|
||||
border-radius: 5px;
|
||||
padding: 0;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#P1x, #P1y {
|
||||
color: #f08;
|
||||
}
|
||||
|
||||
#P2x, #P2y {
|
||||
color: #0ab;
|
||||
}
|
||||
|
||||
canvas#curve {
|
||||
background:
|
||||
linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat,
|
||||
repeating-linear-gradient(transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat,
|
||||
repeating-linear-gradient(-90deg, transparent, #eee 0, #eee .5%, transparent .5%, transparent 10%) no-repeat;
|
||||
|
||||
background-size: 100% 50%, 100% 50%, 100% 50%;
|
||||
background-position: 25%, 0, 0;
|
||||
.display-wrap {
|
||||
background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.05) 0, rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 15px) no-repeat;
|
||||
background-size: 100% 100%, 100% 100%;
|
||||
background-position: -2px 5px, -2px 5px;
|
||||
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.theme-dark canvas#curve {
|
||||
background:
|
||||
linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat,
|
||||
repeating-linear-gradient(transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat,
|
||||
repeating-linear-gradient(-90deg, transparent, rgba(0,0,0,.2) 0, rgba(0,0,0,.2) .5%, transparent .5%, transparent 10%) no-repeat;
|
||||
.theme-dark .display-wrap {
|
||||
background: repeating-linear-gradient(0deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat, repeating-linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.2) 0, rgba(0, 0, 0, 0.2) 1px, transparent 1px, transparent 15px) no-repeat;
|
||||
background-size: 100% 100%, 100% 100%;
|
||||
background-position: -2px 5px, -2px 5px;
|
||||
|
||||
background-size: 100% 50%, 100% 50%, 100% 50%;
|
||||
background-position: 25%, 0, 0;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
canvas#curve {
|
||||
background: linear-gradient(-45deg, transparent 49.7%, rgba(0,0,0,.2) 49.7%, rgba(0,0,0,.2) 50.3%, transparent 50.3%) center no-repeat;
|
||||
background-size: 100% 100%;
|
||||
background-position: 0 0;
|
||||
}
|
||||
|
||||
/* Timing function preview widget */
|
||||
.theme-dark canvas#curve {
|
||||
background: linear-gradient(-45deg, transparent 49.7%, #eee 49.7%, #eee 50.3%, transparent 50.3%) center no-repeat;
|
||||
}
|
||||
|
||||
/* Timing Function Preview Widget */
|
||||
|
||||
.timing-function-preview {
|
||||
position: absolute;
|
||||
top: 400px;
|
||||
bottom: 20px;
|
||||
right: 27px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.timing-function-preview .scale {
|
||||
@ -97,7 +89,7 @@ canvas#curve {
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
|
||||
width: 200px;
|
||||
width: 150px;
|
||||
height: 1px;
|
||||
|
||||
background: #ccc;
|
||||
@ -128,10 +120,10 @@ canvas#curve {
|
||||
left: -7px;
|
||||
}
|
||||
33% {
|
||||
left: 193px;
|
||||
left: 143px;
|
||||
}
|
||||
50% {
|
||||
left: 193px;
|
||||
left: 143px;
|
||||
}
|
||||
83% {
|
||||
left: -7px;
|
||||
@ -140,3 +132,109 @@ canvas#curve {
|
||||
left: -7px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Preset Widget */
|
||||
|
||||
.preset-pane {
|
||||
width:50%;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--theme-splitter-color);
|
||||
}
|
||||
|
||||
#preset-categories {
|
||||
display: flex;
|
||||
width: 94%;
|
||||
border: 1px solid var(--theme-splitter-color);
|
||||
border-radius: 2px;
|
||||
background-color: var(--theme-toolbar-background);
|
||||
margin-left: 4px;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
#preset-categories .category:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.category {
|
||||
flex: 1 1 auto;
|
||||
padding: 5px;
|
||||
width: 33.33%;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
border-right: 1px solid var(--theme-splitter-color);
|
||||
cursor: default;
|
||||
color: var(--theme-body-color);
|
||||
}
|
||||
|
||||
.category:hover {
|
||||
background-color: var(--theme-tab-toolbar-background);
|
||||
}
|
||||
|
||||
.active-category {
|
||||
background-color: var(--theme-selection-background);
|
||||
color: var(--theme-selection-color);
|
||||
}
|
||||
|
||||
.active-category:hover {
|
||||
background-color: var(--theme-selection-background);
|
||||
}
|
||||
|
||||
#preset-container {
|
||||
padding: 0px;
|
||||
width: 100%;
|
||||
height: 331px;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.preset-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.active-preset-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: left;
|
||||
padding-left: 4px;
|
||||
padding-top: 3px;
|
||||
}
|
||||
|
||||
.preset {
|
||||
cursor: pointer;
|
||||
width: 55px;
|
||||
margin: 5px 11px 0px 0px;
|
||||
text-transform: capitalize;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.preset canvas {
|
||||
display: block;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
background-color: var(--theme-body-background);
|
||||
}
|
||||
|
||||
.theme-dark .preset canvas {
|
||||
border-color: #444e58;
|
||||
}
|
||||
|
||||
.preset p {
|
||||
text-align: center;
|
||||
font-size: 0.9em;
|
||||
line-height: 0px;
|
||||
margin: 2px 0px 0px 0p;
|
||||
color: var(--theme-body-color-alt);
|
||||
}
|
||||
|
||||
.active-preset p, .active-preset:hover p {
|
||||
color: var(--theme-body-color);
|
||||
}
|
||||
|
||||
.preset:hover canvas {
|
||||
border-color: var(--theme-selection-background);
|
||||
}
|
||||
|
||||
.active-preset canvas, .active-preset:hover canvas,
|
||||
.theme-dark .active-preset canvas, .theme-dark .preset:hover canvas {
|
||||
background-color: var(--theme-selection-background-semitransparent);
|
||||
border-color: var(--theme-selection-background);
|
||||
}
|
||||
|
@ -15,9 +15,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "Downloads", "resource://gre/modules/Dow
|
||||
const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
|
||||
const {FileUtils} = Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
const {AppProjects} = require("devtools/app-manager/app-projects");
|
||||
const APP_CREATOR_LIST = "devtools.webide.templatesURL";
|
||||
const {AppManager} = require("devtools/webide/app-manager");
|
||||
const {GetTemplatesJSON} = require("devtools/webide/remote-resources");
|
||||
const {getJSON} = require("devtools/shared/getjson");
|
||||
|
||||
const TEMPLATES_URL = "devtools.webide.templatesURL";
|
||||
|
||||
let gTemplateList = null;
|
||||
|
||||
@ -30,11 +31,11 @@ window.addEventListener("load", function onLoad() {
|
||||
window.removeEventListener("load", onLoad);
|
||||
let projectNameNode = document.querySelector("#project-name");
|
||||
projectNameNode.addEventListener("input", canValidate, true);
|
||||
getJSON();
|
||||
getTemplatesJSON();
|
||||
}, true);
|
||||
|
||||
function getJSON() {
|
||||
GetTemplatesJSON().then(list => {
|
||||
function getTemplatesJSON() {
|
||||
getJSON(TEMPLATES_URL).then(list => {
|
||||
if (!Array.isArray(list)) {
|
||||
throw new Error("JSON response not an array");
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
|
||||
const ProjectEditor = require("projecteditor/projecteditor");
|
||||
const {Devices} = Cu.import("resource://gre/modules/devtools/Devices.jsm");
|
||||
const {GetAvailableAddons} = require("devtools/webide/addons");
|
||||
const {GetTemplatesJSON, GetAddonsJSON} = require("devtools/webide/remote-resources");
|
||||
const {getJSON} = require("devtools/shared/getjson");
|
||||
const utils = require("devtools/webide/utils");
|
||||
const Telemetry = require("devtools/shared/telemetry");
|
||||
const {RuntimeScanners, WiFiScanner} = require("devtools/webide/runtimes");
|
||||
@ -34,8 +34,9 @@ const HELP_URL = "https://developer.mozilla.org/docs/Tools/WebIDE/Troubleshootin
|
||||
const MAX_ZOOM = 1.4;
|
||||
const MIN_ZOOM = 0.6;
|
||||
|
||||
// download template index early
|
||||
GetTemplatesJSON(true);
|
||||
// Download remote resources early
|
||||
getJSON("devtools.webide.addonsURL", true);
|
||||
getJSON("devtools.webide.templatesURL", true);
|
||||
|
||||
// See bug 989619
|
||||
console.log = console.log.bind(console);
|
||||
|
@ -5,9 +5,11 @@
|
||||
const {Cu} = require("chrome");
|
||||
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm");
|
||||
const {AddonManager} = Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
|
||||
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
|
||||
const {GetAddonsJSON} = require("devtools/webide/remote-resources");
|
||||
const {getJSON} = require("devtools/shared/getjson");
|
||||
const EventEmitter = require("devtools/toolkit/event-emitter");
|
||||
|
||||
const ADDONS_URL = "devtools.webide.addonsURL";
|
||||
|
||||
let SIMULATOR_LINK = Services.prefs.getCharPref("devtools.webide.simulatorAddonsURL");
|
||||
let ADB_LINK = Services.prefs.getCharPref("devtools.webide.adbAddonURL");
|
||||
@ -54,7 +56,7 @@ let GetAvailableAddons = exports.GetAvailableAddons = function() {
|
||||
simulators: [],
|
||||
adb: null
|
||||
}
|
||||
GetAddonsJSON(true).then(json => {
|
||||
getJSON(ADDONS_URL, true).then(json => {
|
||||
for (let stability in json) {
|
||||
for (let version of json[stability]) {
|
||||
addons.simulators.push(new SimulatorAddon(stability, version));
|
||||
|
@ -25,7 +25,6 @@ EXTRA_JS_MODULES.devtools.webide += [
|
||||
'modules/build.js',
|
||||
'modules/config-view.js',
|
||||
'modules/project-list.js',
|
||||
'modules/remote-resources.js',
|
||||
'modules/runtimes.js',
|
||||
'modules/simulator-process.js',
|
||||
'modules/simulators.js',
|
||||
|
@ -14,6 +14,7 @@
|
||||
# to simulate (e.g. "ZTE Open C", "VIA Vixen", "720p HD Television", etc).
|
||||
device.phones=Phones
|
||||
device.tablets=Tablets
|
||||
device.notebooks=Notebooks
|
||||
device.laptops=Laptops
|
||||
device.televisions=TVs
|
||||
device.consoles=Gaming consoles
|
||||
device.watches=Watches
|
||||
|
@ -544,7 +544,7 @@ pref("services.sync.registerEngines", "Tab,Bookmarks,Form,History,Password,Prefs
|
||||
// prefs to sync by default
|
||||
pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true);
|
||||
pref("services.sync.prefs.sync.devtools.errorconsole.enabled", true);
|
||||
pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
|
||||
pref("services.sync.prefs.sync.lightweightThemes.selectedThemeID", true);
|
||||
pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
|
||||
pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true);
|
||||
pref("services.sync.prefs.sync.privacy.donottrackheader.value", true);
|
||||
|
@ -13,6 +13,7 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils","resource://gre/modules/PlacesUtils.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ReaderMode", "resource://gre/modules/ReaderMode.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "ReadingList", "resource:///modules/readinglist/ReadingList.jsm");
|
||||
|
||||
@ -56,7 +57,18 @@ let ReaderParent = {
|
||||
break;
|
||||
|
||||
case "Reader:FaviconRequest": {
|
||||
// XXX: To implement.
|
||||
if (message.target.messageManager) {
|
||||
let faviconUrl = PlacesUtils.promiseFaviconLinkUrl(message.data.url);
|
||||
faviconUrl.then(function onResolution(favicon) {
|
||||
message.target.messageManager.sendAsyncMessage("Reader:FaviconReturn", {
|
||||
url: message.data.url,
|
||||
faviconUrl: favicon.path.replace(/^favicon:/, "")
|
||||
})
|
||||
},
|
||||
function onRejection(reason) {
|
||||
Cu.reportError("Error requesting favicon URL for about:reader content: " + reason);
|
||||
}).catch(Cu.reportError);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "Reader:ListStatusRequest":
|
||||
|
@ -50,13 +50,18 @@ body {
|
||||
border: 1px solid white;
|
||||
box-shadow: 0px 1px 2px rgba(0,0,0,.35);
|
||||
margin: 5px;
|
||||
background-color: #fff;
|
||||
background-size: cover;
|
||||
background-color: #ebebeb;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
background-image: url("chrome://branding/content/silhouette-40.svg");
|
||||
}
|
||||
|
||||
.item-thumb-container.preview-available {
|
||||
background-color: #fff;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.item-summary-container {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
@ -89,7 +94,7 @@ body {
|
||||
}
|
||||
|
||||
.item:not(:hover):not(.selected) .remove-button {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
|
@ -43,7 +43,7 @@ class TextSelection extends Layer implements GeckoEventListener {
|
||||
private final DrawListener mDrawListener;
|
||||
private boolean mDraggingHandles;
|
||||
|
||||
private int selectionID; // Unique ID provided for each selection action.
|
||||
private String selectionID; // Unique ID provided for each selection action.
|
||||
private float mViewLeft;
|
||||
private float mViewTop;
|
||||
private float mViewZoom;
|
||||
@ -132,7 +132,7 @@ class TextSelection extends Layer implements GeckoEventListener {
|
||||
public void run() {
|
||||
try {
|
||||
if (event.equals("TextSelection:ShowHandles")) {
|
||||
selectionID = message.getInt("selectionID");
|
||||
selectionID = message.getString("selectionID");
|
||||
final JSONArray handles = message.getJSONArray("handles");
|
||||
for (int i=0; i < handles.length(); i++) {
|
||||
String handle = handles.getString(i);
|
||||
|
@ -13,7 +13,7 @@ var InputWidgetHelper = {
|
||||
handleClick: function(aTarget) {
|
||||
// if we're busy looking at a InputWidget we want to eat any clicks that
|
||||
// come to us, but not to process them
|
||||
if (this._uiBusy || !this.hasInputWidget(aTarget))
|
||||
if (this._uiBusy || !this.hasInputWidget(aTarget) || this._isDisabledElement(aTarget))
|
||||
return;
|
||||
|
||||
this._uiBusy = true;
|
||||
@ -81,5 +81,16 @@ var InputWidgetHelper = {
|
||||
setTimeout(function() {
|
||||
aElement.dispatchEvent(evt);
|
||||
}, 0);
|
||||
},
|
||||
|
||||
_isDisabledElement : function(aElement) {
|
||||
let currentElement = aElement;
|
||||
while (currentElement) {
|
||||
if (currentElement.disabled)
|
||||
return true;
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
@ -45,7 +45,7 @@ var SelectionHandler = {
|
||||
|
||||
_activeType: 0, // TYPE_NONE
|
||||
_selectionPrivate: null, // private selection reference
|
||||
_selectionID: 0, // Unique Selection ID
|
||||
_selectionID: null, // Unique Selection ID
|
||||
|
||||
_draggingHandles: false, // True while user drags text selection handles
|
||||
_dragStartAnchorOffset: null, // Editables need initial pos during HandleMove events
|
||||
@ -84,6 +84,13 @@ var SelectionHandler = {
|
||||
getInterface(Ci.nsIDOMWindowUtils);
|
||||
},
|
||||
|
||||
// Provides UUID service for selection ID's.
|
||||
get _idService() {
|
||||
delete this._idService;
|
||||
return this._idService = Cc["@mozilla.org/uuid-generator;1"].
|
||||
getService(Ci.nsIUUIDGenerator);
|
||||
},
|
||||
|
||||
_addObservers: function sh_addObservers() {
|
||||
Services.obs.addObserver(this, "Gesture:SingleTap", false);
|
||||
Services.obs.addObserver(this, "Tab:Selected", false);
|
||||
@ -828,7 +835,7 @@ var SelectionHandler = {
|
||||
aElement.focus();
|
||||
}
|
||||
|
||||
this._selectionID++;
|
||||
this._selectionID = this._idService.generateUUID().toString();
|
||||
this._stopDraggingHandles();
|
||||
this._contentWindow = aElement.ownerDocument.defaultView;
|
||||
this._targetIsRTL = (this._contentWindow.getComputedStyle(aElement, "").direction == "rtl");
|
||||
|
@ -7748,7 +7748,7 @@ var Distribution = {
|
||||
}
|
||||
|
||||
// Apply a lightweight theme if necessary
|
||||
if (prefs && prefs["lightweightThemes.isThemeSelected"]) {
|
||||
if (prefs && prefs["lightweightThemes.selectedThemeID"]) {
|
||||
Services.obs.notifyObservers(null, "lightweight-theme-apply", "");
|
||||
}
|
||||
|
||||
|
@ -837,6 +837,9 @@ pref("devtools.remote.wifi.visible", false);
|
||||
// Client must complete TLS handshake within this window (ms)
|
||||
pref("devtools.remote.tls-handshake-timeout", 10000);
|
||||
|
||||
// URL of the remote JSON catalog used for device simulation
|
||||
pref("devtools.devices.url", "https://code.cdn.mozilla.net/devices/devices.json");
|
||||
|
||||
// view source
|
||||
pref("view_source.syntax_highlight", true);
|
||||
pref("view_source.wrap_long_lines", false);
|
||||
@ -4525,7 +4528,7 @@ pref("browser.addon-watch.interval", 15000);
|
||||
#else
|
||||
pref("browser.addon-watch.interval", -1);
|
||||
#endif
|
||||
pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\"]");
|
||||
pref("browser.addon-watch.ignore", "[\"mochikit@mozilla.org\",\"special-powers@mozilla.org\",\"fxdevtools-adapters@mozilla.org\",\"fx-devtools\"]");
|
||||
// the percentage of time addons are allowed to use without being labeled slow
|
||||
pref("browser.addon-watch.percentage-limit", 5);
|
||||
|
||||
|
@ -110,9 +110,8 @@ PrefStore.prototype = {
|
||||
},
|
||||
|
||||
_setAllPrefs: function PrefStore__setAllPrefs(values) {
|
||||
let enabledPref = "lightweightThemes.isThemeSelected";
|
||||
let enabledBefore = this._prefs.get(enabledPref, false);
|
||||
let prevTheme = LightweightThemeManager.currentTheme;
|
||||
let selectedThemeIDPref = "lightweightThemes.selectedThemeID";
|
||||
let selectedThemeIDBefore = this._prefs.get(selectedThemeIDPref, null);
|
||||
|
||||
for (let [pref, value] in Iterator(values)) {
|
||||
if (!this._isSynced(pref))
|
||||
@ -131,13 +130,14 @@ PrefStore.prototype = {
|
||||
}
|
||||
}
|
||||
|
||||
// Notify the lightweight theme manager of all the new values
|
||||
let enabledNow = this._prefs.get(enabledPref, false);
|
||||
if (enabledBefore && !enabledNow) {
|
||||
// Notify the lightweight theme manager if the selected theme has changed.
|
||||
let selectedThemeIDAfter = this._prefs.get(selectedThemeIDPref, null);
|
||||
if (selectedThemeIDBefore != selectedThemeIDAfter) {
|
||||
// The currentTheme getter will reflect the theme with the new
|
||||
// selectedThemeID (if there is one). Just reset it to itself
|
||||
let currentTheme = LightweightThemeManager.currentTheme;
|
||||
LightweightThemeManager.currentTheme = null;
|
||||
} else if (enabledNow && LightweightThemeManager.usedThemes[0] != prevTheme) {
|
||||
LightweightThemeManager.currentTheme = null;
|
||||
LightweightThemeManager.currentTheme = LightweightThemeManager.usedThemes[0];
|
||||
LightweightThemeManager.currentTheme = currentTheme;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -95,28 +95,28 @@ function run_test() {
|
||||
// Ensure we don't go to the network to fetch personas and end up leaking
|
||||
// stuff.
|
||||
Services.io.offline = true;
|
||||
do_check_false(!!prefs.get("lightweightThemes.isThemeSelected"));
|
||||
do_check_false(!!prefs.get("lightweightThemes.selectedThemeID"));
|
||||
do_check_eq(LightweightThemeManager.currentTheme, null);
|
||||
|
||||
let persona1 = makePersona();
|
||||
let persona2 = makePersona();
|
||||
let usedThemes = JSON.stringify([persona1, persona2]);
|
||||
record.value = {
|
||||
"lightweightThemes.isThemeSelected": true,
|
||||
"lightweightThemes.selectedThemeID": persona1.id,
|
||||
"lightweightThemes.usedThemes": usedThemes
|
||||
};
|
||||
store.update(record);
|
||||
do_check_true(prefs.get("lightweightThemes.isThemeSelected"));
|
||||
do_check_eq(prefs.get("lightweightThemes.selectedThemeID"), persona1.id);
|
||||
do_check_true(Utils.deepEquals(LightweightThemeManager.currentTheme,
|
||||
persona1));
|
||||
|
||||
_("Disable persona");
|
||||
record.value = {
|
||||
"lightweightThemes.isThemeSelected": false,
|
||||
"lightweightThemes.selectedThemeID": null,
|
||||
"lightweightThemes.usedThemes": usedThemes
|
||||
};
|
||||
store.update(record);
|
||||
do_check_false(prefs.get("lightweightThemes.isThemeSelected"));
|
||||
do_check_false(!!prefs.get("lightweightThemes.selectedThemeID"));
|
||||
do_check_eq(LightweightThemeManager.currentTheme, null);
|
||||
|
||||
_("Only the current app's preferences are applied.");
|
||||
|
@ -16,6 +16,8 @@ class mozIStorageAsyncStatement;
|
||||
namespace mozilla {
|
||||
namespace storage {
|
||||
|
||||
class AsyncStatement;
|
||||
|
||||
/*
|
||||
* Since mozIStorageStatementParams is just a tagging interface we do not have
|
||||
* an async variant.
|
||||
|
@ -8,6 +8,7 @@
|
||||
#define MOZSTORAGESTATEMENTJSHELPER_H
|
||||
|
||||
#include "nsIXPCScriptable.h"
|
||||
#include "nsIXPConnect.h"
|
||||
|
||||
class Statement;
|
||||
|
||||
|
@ -14,6 +14,8 @@
|
||||
namespace mozilla {
|
||||
namespace storage {
|
||||
|
||||
class Statement;
|
||||
|
||||
class StatementRow final : public mozIStorageStatementRow
|
||||
, public nsIXPCScriptable
|
||||
{
|
||||
|
@ -732,6 +732,18 @@ nsAutoCompleteController::GetSearchString(nsAString &aSearchString)
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
void
|
||||
nsAutoCompleteController::HandleSearchResult(nsIAutoCompleteSearch *aSearch,
|
||||
nsIAutoCompleteResult *aResult)
|
||||
{
|
||||
// Look up the index of the search which is returning.
|
||||
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
|
||||
if (mSearches[i] == aSearch) {
|
||||
ProcessResult(i, aResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////
|
||||
//// nsIAutoCompleteObserver
|
||||
@ -739,18 +751,41 @@ nsAutoCompleteController::GetSearchString(nsAString &aSearchString)
|
||||
NS_IMETHODIMP
|
||||
nsAutoCompleteController::OnUpdateSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
|
||||
{
|
||||
MOZ_ASSERT(mSearches.Contains(aSearch));
|
||||
|
||||
ClearResults();
|
||||
return OnSearchResult(aSearch, aResult);
|
||||
HandleSearchResult(aSearch, aResult);
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsAutoCompleteController::OnSearchResult(nsIAutoCompleteSearch *aSearch, nsIAutoCompleteResult* aResult)
|
||||
{
|
||||
// look up the index of the search which is returning
|
||||
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
|
||||
if (mSearches[i] == aSearch) {
|
||||
ProcessResult(i, aResult);
|
||||
MOZ_ASSERT(mSearchesOngoing > 0 && mSearches.Contains(aSearch));
|
||||
|
||||
// If this is the first search result we are processing
|
||||
// we should clear out the previously cached results.
|
||||
if (mFirstSearchResult) {
|
||||
ClearResults();
|
||||
mFirstSearchResult = false;
|
||||
}
|
||||
|
||||
uint16_t result = 0;
|
||||
if (aResult) {
|
||||
aResult->GetSearchResult(&result);
|
||||
}
|
||||
|
||||
// If our results are incremental, the search is still ongoing.
|
||||
if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
|
||||
result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
|
||||
--mSearchesOngoing;
|
||||
}
|
||||
|
||||
HandleSearchResult(aSearch, aResult);
|
||||
|
||||
if (mSearchesOngoing == 0) {
|
||||
// If this is the last search to return, cleanup.
|
||||
PostSearchCleanup();
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
@ -1075,8 +1110,13 @@ nsAutoCompleteController::StartSearch(uint16_t aSearchType)
|
||||
NS_ENSURE_STATE(mInput);
|
||||
nsCOMPtr<nsIAutoCompleteInput> input = mInput;
|
||||
|
||||
for (uint32_t i = 0; i < mSearches.Length(); ++i) {
|
||||
nsCOMPtr<nsIAutoCompleteSearch> search = mSearches[i];
|
||||
// Iterate a copy of |mSearches| so that we don't run into trouble if the
|
||||
// array is mutated while we're still in the loop. An nsIAutoCompleteSearch
|
||||
// implementation could synchronously start a new search when StartSearch()
|
||||
// is called and that would lead to assertions down the way.
|
||||
nsCOMArray<nsIAutoCompleteSearch> searchesCopy(mSearches);
|
||||
for (uint32_t i = 0; i < searchesCopy.Length(); ++i) {
|
||||
nsCOMPtr<nsIAutoCompleteSearch> search = searchesCopy[i];
|
||||
|
||||
// Filter on search type. Not all the searches implement this interface,
|
||||
// in such a case just consider them delayed.
|
||||
@ -1107,6 +1147,7 @@ nsAutoCompleteController::StartSearch(uint16_t aSearchType)
|
||||
rv = search->StartSearch(mSearchString, searchParam, result, static_cast<nsIAutoCompleteObserver *>(this));
|
||||
if (NS_FAILED(rv)) {
|
||||
++mSearchesFailed;
|
||||
MOZ_ASSERT(mSearchesOngoing > 0);
|
||||
--mSearchesOngoing;
|
||||
}
|
||||
// Because of the joy of nested event loops (which can easily happen when some
|
||||
@ -1429,23 +1470,10 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes
|
||||
NS_ENSURE_STATE(mInput);
|
||||
nsCOMPtr<nsIAutoCompleteInput> input(mInput);
|
||||
|
||||
// If this is the first search result we are processing
|
||||
// we should clear out the previously cached results
|
||||
if (mFirstSearchResult) {
|
||||
ClearResults();
|
||||
mFirstSearchResult = false;
|
||||
}
|
||||
|
||||
uint16_t result = 0;
|
||||
if (aResult)
|
||||
aResult->GetSearchResult(&result);
|
||||
|
||||
// if our results are incremental, the search is still ongoing
|
||||
if (result != nsIAutoCompleteResult::RESULT_SUCCESS_ONGOING &&
|
||||
result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
|
||||
--mSearchesOngoing;
|
||||
}
|
||||
|
||||
uint32_t oldMatchCount = 0;
|
||||
uint32_t matchCount = 0;
|
||||
if (aResult)
|
||||
@ -1505,7 +1533,7 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes
|
||||
// get results in the future to avoid unnecessarily canceling searches.
|
||||
if (mRowCount || !minResults) {
|
||||
OpenPopup();
|
||||
} else if (result != nsIAutoCompleteResult::RESULT_NOMATCH_ONGOING) {
|
||||
} else if (mSearchesOngoing == 0) {
|
||||
ClosePopup();
|
||||
}
|
||||
}
|
||||
@ -1516,11 +1544,6 @@ nsAutoCompleteController::ProcessResult(int32_t aSearchIndex, nsIAutoCompleteRes
|
||||
CompleteDefaultIndex(resultIndex);
|
||||
}
|
||||
|
||||
if (mSearchesOngoing == 0) {
|
||||
// If this is the last search to return, cleanup.
|
||||
PostSearchCleanup();
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,8 @@ protected:
|
||||
nsresult ClearSearchTimer();
|
||||
void MaybeCompletePlaceholder();
|
||||
|
||||
void HandleSearchResult(nsIAutoCompleteSearch *aSearch,
|
||||
nsIAutoCompleteResult *aResult);
|
||||
nsresult ProcessResult(int32_t aSearchIndex, nsIAutoCompleteResult *aResult);
|
||||
nsresult PostSearchCleanup();
|
||||
|
||||
|
@ -43,7 +43,6 @@ EXTRA_JS_MODULES += [
|
||||
'InsecurePasswordUtils.jsm',
|
||||
'LoginHelper.jsm',
|
||||
'LoginManagerContent.jsm',
|
||||
'LoginManagerParent.jsm',
|
||||
'LoginRecipes.jsm',
|
||||
]
|
||||
|
||||
|
@ -14,12 +14,10 @@ Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
|
||||
* Mirrored in mobile/android/components/LoginManagerPrompter.js */
|
||||
const PROMPT_DISPLAYED = 0;
|
||||
|
||||
const PROMPT_ADD = 1;
|
||||
const PROMPT_ADD_OR_UPDATE = 1;
|
||||
const PROMPT_NOTNOW = 2;
|
||||
const PROMPT_NEVER = 3;
|
||||
|
||||
const PROMPT_UPDATE = 1;
|
||||
|
||||
/*
|
||||
* LoginManagerPromptFactory
|
||||
*
|
||||
@ -742,8 +740,6 @@ LoginManagerPrompter.prototype = {
|
||||
*/
|
||||
promptToSavePassword : function (aLogin) {
|
||||
var notifyObj = this._getPopupNote() || this._getNotifyBox();
|
||||
Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION").add(PROMPT_DISPLAYED);
|
||||
|
||||
if (notifyObj)
|
||||
this._showSaveLoginNotification(notifyObj, aLogin);
|
||||
else
|
||||
@ -783,6 +779,100 @@ LoginManagerPrompter.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays the PopupNotifications.jsm doorhanger for password save or change.
|
||||
*
|
||||
* @param {nsILoginInfo} login
|
||||
* Login to save or change. For changes, this login should contain the
|
||||
* new password.
|
||||
* @param {string} type
|
||||
* This is "password-save" or "password-change" depending on the
|
||||
* original notification type. This is used for telemetry and tests.
|
||||
*/
|
||||
_showLoginCaptureDoorhanger(login, type) {
|
||||
let { browser } = this._getNotifyWindow();
|
||||
|
||||
let msgNames = type == "password-save" ? {
|
||||
prompt: "rememberPasswordMsgNoUsername",
|
||||
buttonLabel: "notifyBarRememberPasswordButtonText",
|
||||
buttonAccessKey: "notifyBarRememberPasswordButtonAccessKey",
|
||||
} : {
|
||||
// We reuse the existing message, even if it expects a username, until we
|
||||
// switch to the final terminology in bug 1144856.
|
||||
prompt: "updatePasswordMsg",
|
||||
buttonLabel: "notifyBarUpdateButtonText",
|
||||
buttonAccessKey: "notifyBarUpdateButtonAccessKey",
|
||||
};
|
||||
|
||||
let histogramName = type == "password-save" ? "PWMGR_PROMPT_REMEMBER_ACTION"
|
||||
: "PWMGR_PROMPT_UPDATE_ACTION";
|
||||
let histogram = Services.telemetry.getHistogramById(histogramName);
|
||||
histogram.add(PROMPT_DISPLAYED);
|
||||
|
||||
// The main action is the "Remember" or "Update" button.
|
||||
let mainAction = {
|
||||
label: this._getLocalizedString(msgNames.buttonLabel),
|
||||
accessKey: this._getLocalizedString(msgNames.buttonAccessKey),
|
||||
callback: () => {
|
||||
histogram.add(PROMPT_ADD_OR_UPDATE);
|
||||
let foundLogins = Services.logins.findLogins({}, login.hostname,
|
||||
login.formSubmitURL,
|
||||
login.httpRealm);
|
||||
let logins = foundLogins.filter(l => l.username == login.username);
|
||||
if (logins.length == 0) {
|
||||
Services.logins.addLogin(login);
|
||||
} else if (logins.length == 1) {
|
||||
this._updateLogin(logins[0], login.password);
|
||||
} else {
|
||||
Cu.reportError("Unexpected match of multiple logins.");
|
||||
}
|
||||
browser.focus();
|
||||
}
|
||||
};
|
||||
|
||||
// Include a "Never for this site" button when saving a new password.
|
||||
let secondaryActions = type == "password-save" ? [{
|
||||
label: this._getLocalizedString("notifyBarNeverRememberButtonText"),
|
||||
accessKey: this._getLocalizedString("notifyBarNeverRememberButtonAccessKey"),
|
||||
callback: () => {
|
||||
histogram.add(PROMPT_NEVER);
|
||||
Services.logins.setLoginSavingEnabled(login.hostname, false);
|
||||
browser.focus();
|
||||
}
|
||||
}] : null;
|
||||
|
||||
let usernamePlaceholder = this._getLocalizedString("noUsernamePlaceholder");
|
||||
let displayHost = this._getShortDisplayHost(login.hostname);
|
||||
|
||||
this._getPopupNote().show(
|
||||
browser,
|
||||
"password",
|
||||
this._getLocalizedString(msgNames.prompt, [displayHost]),
|
||||
"password-notification-icon",
|
||||
mainAction,
|
||||
secondaryActions,
|
||||
{
|
||||
timeout: Date.now() + 10000,
|
||||
persistWhileVisible: true,
|
||||
passwordNotificationType: type,
|
||||
eventCallback: function (topic) {
|
||||
if (topic != "showing") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let chromeDoc = this.browser.ownerDocument;
|
||||
|
||||
chromeDoc.getElementById("password-notification-username")
|
||||
.setAttribute("placeholder", usernamePlaceholder);
|
||||
chromeDoc.getElementById("password-notification-username")
|
||||
.setAttribute("value", login.username);
|
||||
chromeDoc.getElementById("password-notification-password")
|
||||
.setAttribute("value", login.password);
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/*
|
||||
* _showSaveLoginNotification
|
||||
*
|
||||
@ -806,8 +896,6 @@ LoginManagerPrompter.prototype = {
|
||||
this._getLocalizedString("notifyBarRememberPasswordButtonText");
|
||||
var rememberButtonAccessKey =
|
||||
this._getLocalizedString("notifyBarRememberPasswordButtonAccessKey");
|
||||
var usernamePlaceholder =
|
||||
this._getLocalizedString("noUsernamePlaceholder");
|
||||
|
||||
var displayHost = this._getShortDisplayHost(aLogin.hostname);
|
||||
var notificationText = this._getLocalizedString(
|
||||
@ -818,58 +906,10 @@ LoginManagerPrompter.prototype = {
|
||||
// in scope here; set one to |this._pwmgr| so we can get back to pwmgr
|
||||
// without a getService() call.
|
||||
var pwmgr = this._pwmgr;
|
||||
let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_REMEMBER_ACTION");
|
||||
|
||||
// Notification is a PopupNotification
|
||||
if (aNotifyObj == this._getPopupNote()) {
|
||||
// "Remember" button
|
||||
var mainAction = {
|
||||
label: rememberButtonText,
|
||||
accessKey: rememberButtonAccessKey,
|
||||
callback: function(aNotifyObj, aButton) {
|
||||
promptHistogram.add(PROMPT_ADD);
|
||||
pwmgr.addLogin(aLogin);
|
||||
browser.focus();
|
||||
}
|
||||
};
|
||||
|
||||
var secondaryActions = [
|
||||
// "Never for this site" button
|
||||
{
|
||||
label: neverButtonText,
|
||||
accessKey: neverButtonAccessKey,
|
||||
callback: function(aNotifyObj, aButton) {
|
||||
promptHistogram.add(PROMPT_NEVER);
|
||||
pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
|
||||
browser.focus();
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
var { browser } = this._getNotifyWindow();
|
||||
|
||||
let eventCallback = function (topic) {
|
||||
if (topic != "showing") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let chromeDoc = this.browser.ownerDocument;
|
||||
|
||||
chromeDoc.getElementById("password-notification-username")
|
||||
.setAttribute("placeholder", usernamePlaceholder);
|
||||
chromeDoc.getElementById("password-notification-username")
|
||||
.setAttribute("value", aLogin.username);
|
||||
chromeDoc.getElementById("password-notification-password")
|
||||
.setAttribute("value", aLogin.password);
|
||||
};
|
||||
|
||||
aNotifyObj.show(browser, "password", notificationText,
|
||||
"password-notification-icon", mainAction,
|
||||
secondaryActions,
|
||||
{ timeout: Date.now() + 10000,
|
||||
persistWhileVisible: true,
|
||||
passwordNotificationType: "password-save",
|
||||
eventCallback });
|
||||
this._showLoginCaptureDoorhanger(aLogin, "password-save");
|
||||
} else {
|
||||
var notNowButtonText =
|
||||
this._getLocalizedString("notifyBarNotNowButtonText");
|
||||
@ -1030,8 +1070,6 @@ LoginManagerPrompter.prototype = {
|
||||
this._getLocalizedString("notifyBarUpdateButtonText");
|
||||
var changeButtonAccessKey =
|
||||
this._getLocalizedString("notifyBarUpdateButtonAccessKey");
|
||||
var usernamePlaceholder =
|
||||
this._getLocalizedString("noUsernamePlaceholder");
|
||||
|
||||
// We reuse the existing message, even if it expects a username, until we
|
||||
// switch to the final terminology in bug 1144856.
|
||||
@ -1044,44 +1082,10 @@ LoginManagerPrompter.prototype = {
|
||||
// without a getService() call.
|
||||
var self = this;
|
||||
|
||||
let promptHistogram = Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION");
|
||||
// Notification is a PopupNotification
|
||||
if (aNotifyObj == this._getPopupNote()) {
|
||||
// "Yes" button
|
||||
var mainAction = {
|
||||
label: changeButtonText,
|
||||
accessKey: changeButtonAccessKey,
|
||||
popup: null,
|
||||
callback: function(aNotifyObj, aButton) {
|
||||
self._updateLogin(aOldLogin, aNewPassword);
|
||||
promptHistogram.add(PROMPT_UPDATE);
|
||||
}
|
||||
};
|
||||
|
||||
var { browser } = this._getNotifyWindow();
|
||||
|
||||
let eventCallback = function (topic) {
|
||||
if (topic != "showing") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let chromeDoc = this.browser.ownerDocument;
|
||||
|
||||
chromeDoc.getElementById("password-notification-username")
|
||||
.setAttribute("placeholder", usernamePlaceholder);
|
||||
chromeDoc.getElementById("password-notification-username")
|
||||
.setAttribute("value", aOldLogin.username);
|
||||
chromeDoc.getElementById("password-notification-password")
|
||||
.setAttribute("value", aNewPassword);
|
||||
};
|
||||
|
||||
Services.telemetry.getHistogramById("PWMGR_PROMPT_UPDATE_ACTION").add(PROMPT_DISPLAYED);
|
||||
aNotifyObj.show(browser, "password", notificationText,
|
||||
"password-notification-icon", mainAction,
|
||||
null, { timeout: Date.now() + 10000,
|
||||
persistWhileVisible: true,
|
||||
passwordNotificationType: "password-change",
|
||||
eventCallback });
|
||||
aOldLogin.password = aNewPassword;
|
||||
this._showLoginCaptureDoorhanger(aOldLogin, "password-change");
|
||||
} else {
|
||||
var dontChangeButtonText =
|
||||
this._getLocalizedString("notifyBarDontChangeButtonText");
|
||||
|
@ -1562,7 +1562,7 @@ this.PlacesUtils = {
|
||||
uri = PlacesUtils.favicons.getFaviconLinkForIcon(uri);
|
||||
deferred.resolve(uri);
|
||||
} else {
|
||||
deferred.reject();
|
||||
deferred.reject("favicon not found for uri");
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
|
@ -103,7 +103,7 @@ Readability.prototype = {
|
||||
byline: /byline|author|dateline|writtenby/i,
|
||||
replaceFonts: /<(\/?)font[^>]*>/gi,
|
||||
normalize: /\s{2,}/g,
|
||||
videos: /https?:\/\/(www\.)?(youtube|vimeo)\.com/i,
|
||||
videos: /https?:\/\/(www\.)?(youtube|youtube-nocookie|player\.vimeo)\.com/i,
|
||||
nextLink: /(next|weiter|continue|>([^\|]|$)|»([^\|]|$))/i,
|
||||
prevLink: /(prev|earl|old|new|<|«)/i,
|
||||
whitespace: /^\s*$/,
|
||||
@ -125,6 +125,36 @@ Readability.prototype = {
|
||||
this._fixRelativeUris(articleContent);
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterate over a NodeList, which doesn't natively fully implement the Array
|
||||
* interface.
|
||||
*
|
||||
* For convenience, the current object context is applied to the provided
|
||||
* iterate function.
|
||||
*
|
||||
* @param NodeList nodeList The NodeList.
|
||||
* @param Function fn The iterate function.
|
||||
* @return void
|
||||
*/
|
||||
_forEachNode: function(nodeList, fn) {
|
||||
return Array.prototype.forEach.call(nodeList, fn, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Iterate over a NodeList, return true if any of the provided iterate
|
||||
* function calls returns true, false otherwise.
|
||||
*
|
||||
* For convenience, the current object context is applied to the
|
||||
* provided iterate function.
|
||||
*
|
||||
* @param NodeList nodeList The NodeList.
|
||||
* @param Function fn The iterate function.
|
||||
* @return Boolean
|
||||
*/
|
||||
_someNode: function(nodeList, fn) {
|
||||
return Array.prototype.some.call(nodeList, fn, this);
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts each <a> and <img> uri in the given element to an absolute URI.
|
||||
*
|
||||
@ -149,6 +179,10 @@ Readability.prototype = {
|
||||
if (uri[0] == "/")
|
||||
return prePath + uri;
|
||||
|
||||
// Dotslash relative URI.
|
||||
if (uri.indexOf("./") === 0)
|
||||
return pathBase + uri.slice(2);
|
||||
|
||||
// Standard relative URI; add entire path. pathBase already includes a
|
||||
// trailing "/".
|
||||
return pathBase + uri;
|
||||
@ -156,19 +190,18 @@ Readability.prototype = {
|
||||
|
||||
function convertRelativeURIs(tagName, propName) {
|
||||
var elems = articleContent.getElementsByTagName(tagName);
|
||||
for (var i = elems.length; --i >= 0;) {
|
||||
var elem = elems[i];
|
||||
this._forEachNode(elems, function(elem) {
|
||||
var relativeURI = elem.getAttribute(propName);
|
||||
if (relativeURI != null)
|
||||
elems[i].setAttribute(propName, toAbsoluteURI(relativeURI));
|
||||
}
|
||||
elem.setAttribute(propName, toAbsoluteURI(relativeURI));
|
||||
});
|
||||
}
|
||||
|
||||
// Fix links.
|
||||
convertRelativeURIs("a", "href");
|
||||
convertRelativeURIs.call(this, "a", "href");
|
||||
|
||||
// Fix images.
|
||||
convertRelativeURIs("img", "src");
|
||||
convertRelativeURIs.call(this, "img", "src");
|
||||
},
|
||||
|
||||
/**
|
||||
@ -224,19 +257,17 @@ Readability.prototype = {
|
||||
var doc = this._doc;
|
||||
|
||||
// Remove all style tags in head
|
||||
var styleTags = doc.getElementsByTagName("style");
|
||||
for (var st = styleTags.length - 1; st >= 0; st -= 1) {
|
||||
styleTags[st].parentNode.removeChild(styleTags[st]);
|
||||
}
|
||||
this._forEachNode(doc.getElementsByTagName("style"), function(styleNode) {
|
||||
styleNode.parentNode.removeChild(styleNode);
|
||||
});
|
||||
|
||||
if (doc.body) {
|
||||
this._replaceBrs(doc.body);
|
||||
}
|
||||
|
||||
var fonts = doc.getElementsByTagName("FONT");
|
||||
for (var i = fonts.length; --i >=0;) {
|
||||
this._setNodeTag(fonts[i], "SPAN");
|
||||
}
|
||||
this._forEachNode(doc.getElementsByTagName("font"), function(fontNode) {
|
||||
this._setNodeTag(fontNode, "SPAN");
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -262,9 +293,7 @@ Readability.prototype = {
|
||||
* <div>foo<br>bar<p>abc</p></div>
|
||||
*/
|
||||
_replaceBrs: function (elem) {
|
||||
var brs = elem.getElementsByTagName("br");
|
||||
for (var i = 0; i < brs.length; i++) {
|
||||
var br = brs[i];
|
||||
this._forEachNode(elem.getElementsByTagName("br"), function(br) {
|
||||
var next = br.nextSibling;
|
||||
|
||||
// Whether 2 or more <br> elements have been found and replaced with a
|
||||
@ -303,7 +332,7 @@ Readability.prototype = {
|
||||
next = sibling;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_setNodeTag: function (node, tag) {
|
||||
@ -326,6 +355,7 @@ Readability.prototype = {
|
||||
// Clean out junk from the article content
|
||||
this._cleanConditionally(articleContent, "form");
|
||||
this._clean(articleContent, "object");
|
||||
this._clean(articleContent, "embed");
|
||||
this._clean(articleContent, "h1");
|
||||
|
||||
// If there is only one h2, they are probably using it as a header
|
||||
@ -343,26 +373,23 @@ Readability.prototype = {
|
||||
this._cleanConditionally(articleContent, "div");
|
||||
|
||||
// Remove extra paragraphs
|
||||
var articleParagraphs = articleContent.getElementsByTagName('p');
|
||||
for (var i = articleParagraphs.length - 1; i >= 0; i -= 1) {
|
||||
var imgCount = articleParagraphs[i].getElementsByTagName('img').length;
|
||||
var embedCount = articleParagraphs[i].getElementsByTagName('embed').length;
|
||||
var objectCount = articleParagraphs[i].getElementsByTagName('object').length;
|
||||
this._forEachNode(articleContent.getElementsByTagName('p'), function(paragraph) {
|
||||
var imgCount = paragraph.getElementsByTagName('img').length;
|
||||
var embedCount = paragraph.getElementsByTagName('embed').length;
|
||||
var objectCount = paragraph.getElementsByTagName('object').length;
|
||||
// At this point, nasty iframes have been removed, only remain embedded video ones.
|
||||
var iframeCount = paragraph.getElementsByTagName('iframe').length;
|
||||
var totalCount = imgCount + embedCount + objectCount + iframeCount;
|
||||
|
||||
if (imgCount === 0 &&
|
||||
embedCount === 0 &&
|
||||
objectCount === 0 &&
|
||||
this._getInnerText(articleParagraphs[i], false) === '')
|
||||
articleParagraphs[i].parentNode.removeChild(articleParagraphs[i]);
|
||||
}
|
||||
if (totalCount === 0 && !this._getInnerText(paragraph, false))
|
||||
paragraph.parentNode.removeChild(paragraph);
|
||||
});
|
||||
|
||||
var brs = articleContent.getElementsByTagName("BR");
|
||||
for (var i = brs.length; --i >= 0;) {
|
||||
var br = brs[i];
|
||||
this._forEachNode(articleContent.getElementsByTagName("br"), function(br) {
|
||||
var next = this._nextElement(br.nextSibling);
|
||||
if (next && next.tagName == "P")
|
||||
br.parentNode.removeChild(br);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -529,8 +556,7 @@ Readability.prototype = {
|
||||
elementsToScore.push(node);
|
||||
} else {
|
||||
// EXPERIMENTAL
|
||||
for (var i = 0, il = node.childNodes.length; i < il; i += 1) {
|
||||
var childNode = node.childNodes[i];
|
||||
this._forEachNode(node.childNodes, function(childNode) {
|
||||
if (childNode.nodeType === Node.TEXT_NODE) {
|
||||
var p = doc.createElement('p');
|
||||
p.textContent = childNode.textContent;
|
||||
@ -538,7 +564,7 @@ Readability.prototype = {
|
||||
p.className = 'readability-styled';
|
||||
node.replaceChild(p, childNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
node = this._getNextNode(node);
|
||||
@ -551,17 +577,17 @@ Readability.prototype = {
|
||||
* A score is determined by things like number of commas, class names, etc. Maybe eventually link density.
|
||||
**/
|
||||
var candidates = [];
|
||||
for (var pt = 0; pt < elementsToScore.length; pt += 1) {
|
||||
var parentNode = elementsToScore[pt].parentNode;
|
||||
this._forEachNode(elementsToScore, function(elementToScore) {
|
||||
var parentNode = elementToScore.parentNode;
|
||||
var grandParentNode = parentNode ? parentNode.parentNode : null;
|
||||
var innerText = this._getInnerText(elementsToScore[pt]);
|
||||
var innerText = this._getInnerText(elementToScore);
|
||||
|
||||
if (!parentNode || typeof(parentNode.tagName) === 'undefined')
|
||||
continue;
|
||||
return;
|
||||
|
||||
// If this paragraph is less than 25 characters, don't even count it.
|
||||
if (innerText.length < 25)
|
||||
continue;
|
||||
return;
|
||||
|
||||
// Initialize readability data for the parent.
|
||||
if (typeof parentNode.readability === 'undefined') {
|
||||
@ -593,7 +619,7 @@ Readability.prototype = {
|
||||
|
||||
if (grandParentNode)
|
||||
grandParentNode.readability.contentScore += contentScore / 2;
|
||||
}
|
||||
});
|
||||
|
||||
// After we've calculated scores, loop through all of the possible
|
||||
// candidate nodes we found and find the one with the highest score.
|
||||
@ -650,9 +676,9 @@ Readability.prototype = {
|
||||
// below does some of that - but only if we've looked high enough up the DOM
|
||||
// tree.
|
||||
var parentOfTopCandidate = topCandidate.parentNode;
|
||||
var lastScore = topCandidate.readability.contentScore;
|
||||
// The scores shouldn't get too low.
|
||||
var scoreThreshold = topCandidate.readability.contentScore / 3;
|
||||
var lastScore = parentOfTopCandidate.readability.contentScore;
|
||||
var scoreThreshold = lastScore / 3;
|
||||
while (parentOfTopCandidate && parentOfTopCandidate.readability) {
|
||||
var parentScore = parentOfTopCandidate.readability.contentScore;
|
||||
if (parentScore < scoreThreshold)
|
||||
@ -662,6 +688,7 @@ Readability.prototype = {
|
||||
topCandidate = parentOfTopCandidate;
|
||||
break;
|
||||
}
|
||||
lastScore = parentOfTopCandidate.readability.contentScore;
|
||||
parentOfTopCandidate = parentOfTopCandidate.parentNode;
|
||||
}
|
||||
}
|
||||
@ -820,14 +847,13 @@ Readability.prototype = {
|
||||
var propertyPattern = /^\s*og\s*:\s*description\s*$/gi;
|
||||
|
||||
// Find description tags.
|
||||
for (var i = 0; i < metaElements.length; i++) {
|
||||
var element = metaElements[i];
|
||||
this._forEachNode(metaElements, function(element) {
|
||||
var elementName = element.getAttribute("name");
|
||||
var elementProperty = element.getAttribute("property");
|
||||
|
||||
if (elementName === "author") {
|
||||
metadata.byline = element.getAttribute("content");
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
var name = null;
|
||||
@ -846,7 +872,7 @@ Readability.prototype = {
|
||||
values[name] = content.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ("description" in values) {
|
||||
metadata.excerpt = values["description"];
|
||||
@ -867,14 +893,13 @@ Readability.prototype = {
|
||||
* @param Element
|
||||
**/
|
||||
_removeScripts: function(doc) {
|
||||
var scripts = doc.getElementsByTagName('script');
|
||||
for (var i = scripts.length - 1; i >= 0; i -= 1) {
|
||||
scripts[i].nodeValue="";
|
||||
scripts[i].removeAttribute('src');
|
||||
this._forEachNode(doc.getElementsByTagName('script'), function(scriptNode) {
|
||||
scriptNode.nodeValue = "";
|
||||
scriptNode.removeAttribute('src');
|
||||
|
||||
if (scripts[i].parentNode)
|
||||
scripts[i].parentNode.removeChild(scripts[i]);
|
||||
}
|
||||
if (scriptNode.parentNode)
|
||||
scriptNode.parentNode.removeChild(scriptNode);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -884,22 +909,17 @@ Readability.prototype = {
|
||||
*
|
||||
* @param Element
|
||||
**/
|
||||
_hasSinglePInsideElement: function(e) {
|
||||
_hasSinglePInsideElement: function(element) {
|
||||
// There should be exactly 1 element child which is a P:
|
||||
if (e.children.length != 1 || e.firstElementChild.tagName !== "P") {
|
||||
if (element.children.length != 1 || element.firstElementChild.tagName !== "P") {
|
||||
return false;
|
||||
}
|
||||
// And there should be no text nodes with real content
|
||||
var childNodes = e.childNodes;
|
||||
for (var i = childNodes.length; --i >= 0;) {
|
||||
var node = childNodes[i];
|
||||
if (node.nodeType == Node.TEXT_NODE &&
|
||||
this.REGEXPS.hasContent.test(node.textContent)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
// And there should be no text nodes with real content
|
||||
return !this._someNode(element.childNodes, function(node) {
|
||||
return node.nodeType === Node.TEXT_NODE &&
|
||||
this.REGEXPS.hasContent.test(node.textContent);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -907,14 +927,11 @@ Readability.prototype = {
|
||||
*
|
||||
* @param Element
|
||||
*/
|
||||
_hasChildBlockElement: function (e) {
|
||||
var length = e.children.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
var child = e.children[i];
|
||||
if (this.DIV_TO_P_ELEMS.indexOf(child.tagName) !== -1 || this._hasChildBlockElement(child))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
_hasChildBlockElement: function (element) {
|
||||
return this._someNode(element.childNodes, function(node) {
|
||||
return this.DIV_TO_P_ELEMS.indexOf(node.tagName) !== -1 ||
|
||||
this._hasChildBlockElement(node);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -922,11 +939,12 @@ Readability.prototype = {
|
||||
* This also strips out any excess whitespace to be found.
|
||||
*
|
||||
* @param Element
|
||||
* @param Boolean normalizeSpaces (default: true)
|
||||
* @return string
|
||||
**/
|
||||
_getInnerText: function(e, normalizeSpaces) {
|
||||
var textContent = e.textContent.trim();
|
||||
normalizeSpaces = (typeof normalizeSpaces === 'undefined') ? true : normalizeSpaces;
|
||||
var textContent = e.textContent.trim();
|
||||
|
||||
if (normalizeSpaces) {
|
||||
return textContent.replace(this.REGEXPS.normalize, " ");
|
||||
@ -985,14 +1003,17 @@ Readability.prototype = {
|
||||
* @param Element
|
||||
* @return number (float)
|
||||
**/
|
||||
_getLinkDensity: function(e) {
|
||||
var links = e.getElementsByTagName("a");
|
||||
var textLength = this._getInnerText(e).length;
|
||||
_getLinkDensity: function(element) {
|
||||
var textLength = this._getInnerText(element).length;
|
||||
if (textLength === 0)
|
||||
return;
|
||||
|
||||
var linkLength = 0;
|
||||
|
||||
for (var i = 0, il = links.length; i < il; i += 1) {
|
||||
linkLength += this._getInnerText(links[i]).length;
|
||||
}
|
||||
// XXX implement _reduceNodeList?
|
||||
this._forEachNode(element.getElementsByTagName("a"), function(linkNode) {
|
||||
linkLength += this._getInnerText(linkNode).length;
|
||||
});
|
||||
|
||||
return linkLength / textLength;
|
||||
},
|
||||
@ -1405,28 +1426,26 @@ Readability.prototype = {
|
||||
* @return void
|
||||
**/
|
||||
_clean: function(e, tag) {
|
||||
var targetList = e.getElementsByTagName(tag);
|
||||
var isEmbed = (tag === 'object' || tag === 'embed');
|
||||
var isEmbed = ["object", "embed", "iframe"].indexOf(tag) !== -1;
|
||||
|
||||
for (var y = targetList.length - 1; y >= 0; y -= 1) {
|
||||
this._forEachNode(e.getElementsByTagName(tag), function(element) {
|
||||
// Allow youtube and vimeo videos through as people usually want to see those.
|
||||
if (isEmbed) {
|
||||
var attributeValues = "";
|
||||
for (var i = 0, il = targetList[y].attributes.length; i < il; i += 1) {
|
||||
attributeValues += targetList[y].attributes[i].value + '|';
|
||||
}
|
||||
var attributeValues = [].map.call(element.attributes, function(attr) {
|
||||
return attr.value;
|
||||
}).join("|");
|
||||
|
||||
// First, check the elements attributes to see if any of them contain youtube or vimeo
|
||||
if (this.REGEXPS.videos.test(attributeValues))
|
||||
continue;
|
||||
return;
|
||||
|
||||
// Then check the elements inside this element for the same.
|
||||
if (this.REGEXPS.videos.test(targetList[y].innerHTML))
|
||||
continue;
|
||||
if (this.REGEXPS.videos.test(element.innerHTML))
|
||||
return;
|
||||
}
|
||||
|
||||
targetList[y].parentNode.removeChild(targetList[y]);
|
||||
}
|
||||
element.parentNode.removeChild(element);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1578,7 +1597,7 @@ Readability.prototype = {
|
||||
if (!metadata.excerpt) {
|
||||
var paragraphs = articleContent.getElementsByTagName("p");
|
||||
if (paragraphs.length > 0) {
|
||||
metadata.excerpt = paragraphs[0].textContent;
|
||||
metadata.excerpt = paragraphs[0].textContent.trim();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -274,14 +274,20 @@ FormAutoComplete.prototype = {
|
||||
this.log("getAutocompleteValues failed: " + aError.message);
|
||||
},
|
||||
handleCompletion: aReason => {
|
||||
// Check that the current query is still the one we created. Our
|
||||
// query might have been canceled shortly before completing, in
|
||||
// that case we don't want to call the callback anymore.
|
||||
if (query == this._pendingQuery) {
|
||||
this._pendingQuery = null;
|
||||
if (!aReason) {
|
||||
callback(results);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this._pendingQuery = FormHistory.getAutoCompleteResults(searchString, params, processResults);
|
||||
let query = FormHistory.getAutoCompleteResults(searchString, params, processResults);
|
||||
this._pendingQuery = query;
|
||||
},
|
||||
|
||||
/*
|
||||
@ -329,6 +335,7 @@ FormAutoCompleteChild.prototype = {
|
||||
|
||||
_debug: false,
|
||||
_enabled: true,
|
||||
_pendingSearch: null,
|
||||
|
||||
/*
|
||||
* init
|
||||
@ -361,7 +368,9 @@ FormAutoCompleteChild.prototype = {
|
||||
autoCompleteSearchAsync : function (aInputName, aUntrimmedSearchString, aField, aPreviousResult, aListener) {
|
||||
this.log("autoCompleteSearchAsync");
|
||||
|
||||
this._pendingListener = aListener;
|
||||
if (this._pendingSearch) {
|
||||
this.stopAutoCompleteSearch();
|
||||
}
|
||||
|
||||
let rect = BrowserUtils.getElementBoundingScreenRect(aField);
|
||||
|
||||
@ -383,12 +392,20 @@ FormAutoCompleteChild.prototype = {
|
||||
height: rect.height
|
||||
});
|
||||
|
||||
mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult",
|
||||
function searchFinished(message) {
|
||||
let search = this._pendingSearch = {};
|
||||
let searchFinished = message => {
|
||||
mm.removeMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
|
||||
|
||||
// Check whether stopAutoCompleteSearch() was called, i.e. the search
|
||||
// was cancelled, while waiting for a result.
|
||||
if (search != this._pendingSearch) {
|
||||
return;
|
||||
}
|
||||
this._pendingSearch = null;
|
||||
|
||||
let result = new FormAutoCompleteResult(
|
||||
null,
|
||||
[{text: res} for (res of message.data.results)],
|
||||
[for (res of message.data.results) {text: res}],
|
||||
null,
|
||||
null
|
||||
);
|
||||
@ -396,13 +413,14 @@ FormAutoCompleteChild.prototype = {
|
||||
aListener.onSearchCompletion(result);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
mm.addMessageListener("FormAutoComplete:AutoCompleteSearchAsyncResult", searchFinished);
|
||||
this.log("autoCompleteSearchAsync message was sent");
|
||||
},
|
||||
|
||||
stopAutoCompleteSearch : function () {
|
||||
this.log("stopAutoCompleteSearch");
|
||||
this._pendingSearch = null;
|
||||
},
|
||||
}; // end of FormAutoCompleteChild implementation
|
||||
|
||||
|
@ -6571,16 +6571,16 @@
|
||||
"n_buckets": 100,
|
||||
"description": "The peak number of open tabs in all windows for a session for devtools users."
|
||||
},
|
||||
"DEVTOOLS_TABS_OPEN_AVERAGE_EXPONENTIAL": {
|
||||
"DEVTOOLS_TABS_OPEN_AVERAGE_LINEAR": {
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"kind": "linear",
|
||||
"high": "101",
|
||||
"n_buckets": "100",
|
||||
"description": "The mean number of open tabs in all windows for a session for devtools users."
|
||||
},
|
||||
"DEVTOOLS_TABS_PINNED_PEAK_EXPONENTIAL": {
|
||||
"DEVTOOLS_TABS_PINNED_PEAK_LINEAR": {
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"kind": "linear",
|
||||
"high": "101",
|
||||
"n_buckets": "100",
|
||||
"description": "The peak number of pinned tabs (app tabs) in all windows for a session for devtools users."
|
||||
@ -7287,6 +7287,14 @@
|
||||
"releaseChannelCollection": "opt-out",
|
||||
"description": "Connection length for bi-directionally connected media"
|
||||
},
|
||||
"LOOP_SHARING_STATE_CHANGE": {
|
||||
"alert_emails": ["firefox-dev@mozilla.org", "mdeboer@mozilla.com"],
|
||||
"expires_in_version": "43",
|
||||
"kind": "count",
|
||||
"keyed": true,
|
||||
"releaseChannelCollection": "opt-in",
|
||||
"description": "Number of times the sharing feature has been enabled and disabled"
|
||||
},
|
||||
"E10S_AUTOSTART": {
|
||||
"expires_in_version": "never",
|
||||
"kind": "boolean",
|
||||
|
@ -69,25 +69,58 @@ this.__defineSetter__("_maxUsedThemes", function maxUsedThemesSetter(aVal) {
|
||||
var _themeIDBeingEnabled = null;
|
||||
var _themeIDBeingDisabled = null;
|
||||
|
||||
// Convert from the old storage format (in which the order of usedThemes
|
||||
// was combined with isThemeSelected to determine which theme was selected)
|
||||
// to the new one (where a selectedThemeID determines which theme is selected).
|
||||
(function migrateToNewStorageFormat() {
|
||||
let wasThemeSelected = false;
|
||||
try {
|
||||
wasThemeSelected = _prefs.getBoolPref("isThemeSelected");
|
||||
} catch(e) { }
|
||||
|
||||
if (wasThemeSelected) {
|
||||
_prefs.clearUserPref("isThemeSelected");
|
||||
let themes = [];
|
||||
try {
|
||||
themes = JSON.parse(_prefs.getComplexValue("usedThemes",
|
||||
Ci.nsISupportsString).data);
|
||||
} catch (e) { }
|
||||
|
||||
if (Array.isArray(themes) && themes[0]) {
|
||||
_prefs.setCharPref("selectedThemeID", themes[0].id);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
this.LightweightThemeManager = {
|
||||
get name() "LightweightThemeManager",
|
||||
|
||||
// Themes that can be added for an application. They can't be removed, and
|
||||
// will always show up at the top of the list.
|
||||
_builtInThemes: new Map(),
|
||||
|
||||
get usedThemes () {
|
||||
let themes = [];
|
||||
try {
|
||||
return JSON.parse(_prefs.getComplexValue("usedThemes",
|
||||
themes = JSON.parse(_prefs.getComplexValue("usedThemes",
|
||||
Ci.nsISupportsString).data);
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
themes.push(...this._builtInThemes.values());
|
||||
return themes;
|
||||
},
|
||||
|
||||
get currentTheme () {
|
||||
let selectedThemeID = null;
|
||||
try {
|
||||
if (_prefs.getBoolPref("isThemeSelected"))
|
||||
var data = this.usedThemes[0];
|
||||
selectedThemeID = _prefs.getCharPref("selectedThemeID");
|
||||
} catch (e) {}
|
||||
|
||||
return data || null;
|
||||
let data = null;
|
||||
if (selectedThemeID) {
|
||||
data = this.getUsedTheme(selectedThemeID);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
get currentThemeForDisplay () {
|
||||
@ -125,7 +158,7 @@ this.LightweightThemeManager = {
|
||||
|
||||
forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) {
|
||||
let theme = this.getUsedTheme(aId);
|
||||
if (!theme)
|
||||
if (!theme || LightweightThemeManager._builtInThemes.has(theme.id))
|
||||
return;
|
||||
|
||||
let wrapper = new AddonWrapper(theme);
|
||||
@ -141,6 +174,30 @@ this.LightweightThemeManager = {
|
||||
AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
|
||||
},
|
||||
|
||||
addBuiltInTheme: function LightweightThemeManager_addBuiltInTheme(theme) {
|
||||
if (!theme || !theme.id || this.usedThemes.some(t => t.id == theme.id)) {
|
||||
throw new Error("Trying to add invalid builtIn theme");
|
||||
}
|
||||
|
||||
this._builtInThemes.set(theme.id, theme);
|
||||
},
|
||||
|
||||
forgetBuiltInTheme: function LightweightThemeManager_forgetBuiltInTheme(id) {
|
||||
if (!this._builtInThemes.has(id)) {
|
||||
let currentTheme = this.currentTheme;
|
||||
if (currentTheme && currentTheme.id == id) {
|
||||
this.currentTheme = null;
|
||||
}
|
||||
}
|
||||
return this._builtInThemes.delete(id);
|
||||
},
|
||||
|
||||
clearBuiltInThemes: function LightweightThemeManager_clearBuiltInThemes() {
|
||||
for (let id of this._builtInThemes.keys()) {
|
||||
this.forgetBuiltInTheme(id);
|
||||
}
|
||||
},
|
||||
|
||||
previewTheme: function LightweightThemeManager_previewTheme(aData) {
|
||||
let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
|
||||
cancel.data = false;
|
||||
@ -242,7 +299,11 @@ this.LightweightThemeManager = {
|
||||
}
|
||||
}
|
||||
|
||||
_prefs.setBoolPref("isThemeSelected", aData != null);
|
||||
if (aData)
|
||||
_prefs.setCharPref("selectedThemeID", aData.id);
|
||||
else
|
||||
_prefs.clearUserPref("selectedThemeID");
|
||||
|
||||
_notifyWindows(aData);
|
||||
Services.obs.notifyObservers(null, "lightweight-theme-changed", null);
|
||||
},
|
||||
@ -462,7 +523,11 @@ function AddonWrapper(aTheme) {
|
||||
});
|
||||
|
||||
this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() {
|
||||
let permissions = AddonManager.PERM_CAN_UNINSTALL;
|
||||
let permissions = 0;
|
||||
|
||||
// Do not allow uninstall of builtIn themes.
|
||||
if (!LightweightThemeManager._builtInThemes.has(aTheme.id))
|
||||
permissions = AddonManager.PERM_CAN_UNINSTALL;
|
||||
if (this.userDisabled)
|
||||
permissions |= AddonManager.PERM_CAN_ENABLE;
|
||||
else
|
||||
@ -679,6 +744,9 @@ function _makeURI(aURL, aBaseURI)
|
||||
Services.io.newURI(aURL, null, aBaseURI);
|
||||
|
||||
function _updateUsedThemes(aList) {
|
||||
// Remove app-specific themes before saving them to the usedThemes pref.
|
||||
aList = aList.filter(theme => !LightweightThemeManager._builtInThemes.has(theme.id));
|
||||
|
||||
// Send uninstall events for all themes that need to be removed.
|
||||
while (aList.length > _maxUsedThemes) {
|
||||
let wrapper = new AddonWrapper(aList[aList.length - 1]);
|
||||
|
@ -19,18 +19,20 @@ function dummy(id) {
|
||||
};
|
||||
}
|
||||
|
||||
function hasPermission(aAddon, aPerm) {
|
||||
var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()];
|
||||
return !!(aAddon.permissions & perm);
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
|
||||
startupManager();
|
||||
|
||||
Services.prefs.setIntPref("lightweightThemes.maxUsedThemes", 8);
|
||||
|
||||
var temp = {};
|
||||
Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", temp);
|
||||
do_check_eq(typeof temp.LightweightThemeManager, "object");
|
||||
|
||||
var ltm = temp.LightweightThemeManager;
|
||||
let {LightweightThemeManager: ltm} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
|
||||
|
||||
do_check_eq(typeof ltm, "object");
|
||||
do_check_eq(typeof ltm.usedThemes, "object");
|
||||
do_check_eq(ltm.usedThemes.length, 0);
|
||||
do_check_eq(ltm.currentTheme, null);
|
||||
@ -511,4 +513,86 @@ function run_test() {
|
||||
Services.prefs.clearUserPref("lightweightThemes.maxUsedThemes");
|
||||
|
||||
do_check_eq(ltm.usedThemes.length, 30);
|
||||
|
||||
let usedThemes = ltm.usedThemes;
|
||||
for (let theme of usedThemes) {
|
||||
ltm.forgetUsedTheme(theme.id);
|
||||
}
|
||||
|
||||
// Check builtInTheme functionality for Bug 1094821
|
||||
do_check_eq(ltm._builtInThemes.toString(), "[object Map]");
|
||||
do_check_eq([...ltm._builtInThemes.entries()].length, 0);
|
||||
do_check_eq(ltm.usedThemes.length, 0);
|
||||
|
||||
ltm.addBuiltInTheme(dummy("builtInTheme0"));
|
||||
do_check_eq([...ltm._builtInThemes].length, 1);
|
||||
do_check_eq(ltm.usedThemes.length, 1);
|
||||
do_check_eq(ltm.usedThemes[0].id, "builtInTheme0");
|
||||
|
||||
ltm.addBuiltInTheme(dummy("builtInTheme1"));
|
||||
do_check_eq([...ltm._builtInThemes].length, 2);
|
||||
do_check_eq(ltm.usedThemes.length, 2);
|
||||
do_check_eq(ltm.usedThemes[1].id, "builtInTheme1");
|
||||
|
||||
// Clear all and then re-add
|
||||
ltm.clearBuiltInThemes();
|
||||
do_check_eq([...ltm._builtInThemes].length, 0);
|
||||
do_check_eq(ltm.usedThemes.length, 0);
|
||||
|
||||
ltm.addBuiltInTheme(dummy("builtInTheme0"));
|
||||
ltm.addBuiltInTheme(dummy("builtInTheme1"));
|
||||
do_check_eq([...ltm._builtInThemes].length, 2);
|
||||
do_check_eq(ltm.usedThemes.length, 2);
|
||||
|
||||
do_test_pending();
|
||||
|
||||
AddonManager.getAddonByID("builtInTheme0@personas.mozilla.org", aAddon => {
|
||||
// App specific theme can't be uninstalled or disabled,
|
||||
// but can be enabled (since it isn't already applied).
|
||||
do_check_eq(hasPermission(aAddon, "uninstall"), false);
|
||||
do_check_eq(hasPermission(aAddon, "disable"), false);
|
||||
do_check_eq(hasPermission(aAddon, "enable"), true);
|
||||
|
||||
ltm.currentTheme = dummy("x0");
|
||||
do_check_eq([...ltm._builtInThemes].length, 2);
|
||||
do_check_eq(ltm.usedThemes.length, 3);
|
||||
do_check_eq(ltm.usedThemes[0].id, "x0");
|
||||
do_check_eq(ltm.currentTheme.id, "x0");
|
||||
do_check_eq(ltm.usedThemes[1].id, "builtInTheme0");
|
||||
do_check_eq(ltm.usedThemes[2].id, "builtInTheme1");
|
||||
|
||||
Assert.throws(() => { ltm.addBuiltInTheme(dummy("builtInTheme0")) },
|
||||
"Exception is thrown adding a duplicate theme");
|
||||
Assert.throws(() => { ltm.addBuiltInTheme("not a theme object") },
|
||||
"Exception is thrown adding an invalid theme");
|
||||
|
||||
AddonManager.getAddonByID("x0@personas.mozilla.org", aAddon => {
|
||||
// Currently applied (non-app-specific) can be uninstalled or disabled,
|
||||
// but can't be enabled (since it's already applied).
|
||||
do_check_eq(hasPermission(aAddon, "uninstall"), true);
|
||||
do_check_eq(hasPermission(aAddon, "disable"), true);
|
||||
do_check_eq(hasPermission(aAddon, "enable"), false);
|
||||
|
||||
ltm.forgetUsedTheme("x0");
|
||||
do_check_eq(ltm.currentTheme, null);
|
||||
|
||||
// Removing the currently applied app specific theme should unapply it
|
||||
ltm.currentTheme = ltm.getUsedTheme("builtInTheme0");
|
||||
do_check_eq(ltm.currentTheme.id, "builtInTheme0");
|
||||
do_check_true(ltm.forgetBuiltInTheme("builtInTheme0"));
|
||||
do_check_eq(ltm.currentTheme, null);
|
||||
|
||||
do_check_eq([...ltm._builtInThemes].length, 1);
|
||||
do_check_eq(ltm.usedThemes.length, 1);
|
||||
|
||||
do_check_true(ltm.forgetBuiltInTheme("builtInTheme1"));
|
||||
do_check_false(ltm.forgetBuiltInTheme("not-an-existing-theme-id"));
|
||||
|
||||
do_check_eq([...ltm._builtInThemes].length, 0);
|
||||
do_check_eq(ltm.usedThemes.length, 0);
|
||||
do_check_eq(ltm.currentTheme, null);
|
||||
|
||||
do_test_finished();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ function run_test() {
|
||||
previewURL: "http://localhost/data/preview.png",
|
||||
iconURL: "http://localhost/data/icon.png"
|
||||
}]));
|
||||
Services.prefs.setBoolPref("lightweightThemes.isThemeSelected", true);
|
||||
Services.prefs.setCharPref("lightweightThemes.selectedThemeID", "1");
|
||||
|
||||
let stagedXPIs = profileDir.clone();
|
||||
stagedXPIs.append("staged-xpis");
|
||||
|
@ -257,6 +257,7 @@ body {
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #c1c1c1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.button[hidden] {
|
||||
|
Loading…
Reference in New Issue
Block a user