Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2014-11-07 17:35:52 -08:00
commit ba8a408b7d
67 changed files with 2622 additions and 461 deletions

View File

@ -183,7 +183,11 @@ pref("app.update.metro.enabled", true);
pref("app.update.silent", false);
// If set to true, the hamburger button will show badges for update events.
#ifdef MOZ_DEV_EDITION
pref("app.update.badge", true);
#else
pref("app.update.badge", false);
#endif
// If set to true, the Update Service will apply updates in the background
// when it finishes downloading them.

View File

@ -86,6 +86,7 @@ let DevEdition = {
this.styleSheet.removeEventListener("load", this);
gBrowser.tabContainer._positionPinnedTabs();
ToolbarIconColor.inferFromText();
Services.obs.notifyObservers(window, "devedition-theme-state-changed", true);
}
},
@ -96,12 +97,15 @@ let DevEdition = {
'xml-stylesheet', styleSheetAttr);
this.styleSheet.addEventListener("load", this);
document.insertBefore(this.styleSheet, document.documentElement);
// NB: we'll notify observers once the stylesheet has fully loaded, see
// handleEvent above.
} else if (!deveditionThemeEnabled && this.styleSheet) {
this.styleSheet.removeEventListener("load", this);
this.styleSheet.remove();
this.styleSheet = null;
gBrowser.tabContainer._positionPinnedTabs();
ToolbarIconColor.inferFromText();
Services.obs.notifyObservers(window, "devedition-theme-state-changed", false);
}
},

View File

@ -513,6 +513,8 @@
<menuitem id="menu_browserToolbox"
observes="devtoolsMenuBroadcaster_BrowserToolbox"
accesskey="&browserToolboxMenu.accesskey;"/>
<menuitem id="menu_browserContentToolbox"
observes="devtoolsMenuBroadcaster_BrowserContentToolbox"/>
<menuitem id="menu_browserConsole"
observes="devtoolsMenuBroadcaster_BrowserConsole"
accesskey="&browserConsoleCmd.accesskey;"/>

View File

@ -100,6 +100,7 @@
<command id="Tools:DevAppMgr" oncommand="gDevToolsBrowser.openAppManager(gBrowser);" disabled="true" hidden="true"/>
<command id="Tools:WebIDE" oncommand="gDevToolsBrowser.openWebIDE();" disabled="true" hidden="true"/>
<command id="Tools:BrowserToolbox" oncommand="BrowserToolboxProcess.init();" disabled="true" hidden="true"/>
<command id="Tools:BrowserContentToolbox" oncommand="gDevToolsBrowser.openContentProcessToolbox();" disabled="true" hidden="true"/>
<command id="Tools:BrowserConsole" oncommand="HUDService.toggleBrowserConsole();"/>
<command id="Tools:Scratchpad" oncommand="Scratchpad.openScratchpad();"/>
<command id="Tools:ResponsiveUI" oncommand="ResponsiveUI.toggle();"/>
@ -207,6 +208,9 @@
<broadcaster id="devtoolsMenuBroadcaster_BrowserToolbox"
label="&browserToolboxMenu.label;"
command="Tools:BrowserToolbox"/>
<broadcaster id="devtoolsMenuBroadcaster_BrowserContentToolbox"
label="&browserContentToolboxMenu.label;"
command="Tools:BrowserContentToolbox"/>
<broadcaster id="devtoolsMenuBroadcaster_BrowserConsole"
label="&browserConsoleCmd.label;"
key="key_browserConsole"

View File

@ -474,7 +474,7 @@ skip-if = true # Bug 1005420 - fails intermittently. also with e10s enabled: biz
[browser_visibleTabs_bookmarkAllTabs.js]
[browser_visibleTabs_contextMenu.js]
[browser_visibleTabs_tabPreview.js]
skip-if = (os == "win" && !debug) # Bug 1007418
skip-if = (os == "win" && !debug) || e10s # Bug 1007418
[browser_web_channel.js]
[browser_windowopen_reflows.js]
skip-if = buildapp == 'mulet'

View File

@ -19,6 +19,7 @@ const kToolbarVisibilityBtn = "customization-toolbar-visibility-button";
const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar";
const kDeveditionThemePref = "browser.devedition.theme.enabled";
const kDeveditionButtonPref = "browser.devedition.theme.showCustomizeButton";
const kDeveditionChangedNotification = "devedition-theme-state-changed";
const kMaxTransitionDurationMs = 2000;
const kPanelItemContextMenu = "customizationPanelItemContextMenu";
@ -64,9 +65,11 @@ function CustomizeMode(aWindow) {
this.paletteEmptyNotice = this.document.getElementById("customization-empty");
this.paletteSpacer = this.document.getElementById("customization-spacer");
this.tipPanel = this.document.getElementById("customization-tipPanel");
let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") {
let lwthemeButton = this.document.getElementById("customization-lwtheme-button");
let deveditionButton = this.document.getElementById("customization-devedition-theme-button");
lwthemeButton.setAttribute("hidden", "true");
deveditionButton.setAttribute("hidden", "true");
}
#ifdef CAN_DRAW_IN_TITLEBAR
this._updateTitlebarButton();
@ -74,7 +77,7 @@ function CustomizeMode(aWindow) {
#endif
this._updateDevEditionThemeButton();
Services.prefs.addObserver(kDeveditionButtonPref, this, false);
Services.prefs.addObserver(kDeveditionThemePref, this, false);
Services.obs.addObserver(this, kDeveditionChangedNotification, false);
this.window.addEventListener("unload", this);
};
@ -111,7 +114,7 @@ CustomizeMode.prototype = {
Services.prefs.removeObserver(kDrawInTitlebarPref, this);
#endif
Services.prefs.removeObserver(kDeveditionButtonPref, this);
Services.prefs.removeObserver(kDeveditionThemePref, this);
Services.obs.removeObserver(this, kDeveditionChangedNotification);
},
toggle: function() {
@ -470,6 +473,7 @@ CustomizeMode.prototype = {
toolbar.removeAttribute("customizing");
this.window.PanelUI.endBatchUpdate();
delete this._lastLightweightTheme;
this._changed = false;
this._transitioning = false;
this._handler.isExitingCustomizeMode = false;
@ -1481,7 +1485,6 @@ CustomizeMode.prototype = {
#ifdef CAN_DRAW_IN_TITLEBAR
this._updateTitlebarButton();
#endif
this._updateDevEditionThemeButton();
break;
case "lightweight-theme-window-updated":
if (aSubject == this.window) {
@ -1493,6 +1496,13 @@ CustomizeMode.prototype = {
}
}
break;
case kDeveditionChangedNotification:
if (aSubject == this.window) {
this._updateDevEditionThemeButton();
this._updateResetButton();
this._updateUndoResetButton();
}
break;
}
},
@ -1520,7 +1530,7 @@ CustomizeMode.prototype = {
_updateDevEditionThemeButton: function() {
let button = this.document.getElementById("customization-devedition-theme-button");
let themeEnabled = Services.prefs.getBoolPref(kDeveditionThemePref);
let themeEnabled = !!this.window.DevEdition.styleSheet;
if (themeEnabled) {
button.setAttribute("checked", "true");
} else {
@ -1535,9 +1545,24 @@ CustomizeMode.prototype = {
}
},
toggleDevEditionTheme: function() {
const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}";
let button = this.document.getElementById("customization-devedition-theme-button");
let preferenceValue = button.hasAttribute("checked");
Services.prefs.setBoolPref(kDeveditionThemePref, preferenceValue);
let shouldEnable = button.hasAttribute("checked");
Services.prefs.setBoolPref(kDeveditionThemePref, shouldEnable);
let currentLWT = LightweightThemeManager.currentTheme;
if (currentLWT && shouldEnable) {
this._lastLightweightTheme = currentLWT;
AddonManager.getAddonByID(DEFAULT_THEME_ID, function(aDefaultTheme) {
// Theoretically, this could race if people are /very/ quick in switching
// something else here, so doublecheck:
if (button.hasAttribute("checked")) {
aDefaultTheme.userDisabled = false;
}
});
} else if (!currentLWT && !shouldEnable && this._lastLightweightTheme) {
LightweightThemeManager.currentTheme = this._lastLightweightTheme;
}
},
_onDragStart: function(aEvent) {

View File

@ -114,6 +114,11 @@ add_task(function() {
is(deveditionThemeButton.hasAttribute("checked"), defaultValue, "Devedition theme button should reflect pref value");
is(undoResetButton.hidden, true, "Undo reset button should be hidden at start of test");
Services.prefs.setBoolPref(prefName, !defaultValue);
//XXXgijs this line should be removed once bug 1094509 lands
Services.prefs.setCharPref("devtools.theme", "dark");
yield waitForCondition(() => !restoreDefaultsButton.disabled);
ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled when pref changed");
is(deveditionThemeButton.hasAttribute("checked"), !defaultValue, "Devedition theme button should reflect changed pref value");
ok(!CustomizableUI.inDefaultState, "With devedition theme flipped, no longer default");
@ -134,6 +139,8 @@ add_task(function() {
is(Services.prefs.getBoolPref(prefName), !defaultValue, "Undo-reset goes back to previous pref value");
is(undoResetButton.hidden, true, "Undo reset button should be hidden after undo-reset clicked");
//XXXgijs this line should be removed once bug 1094509 lands
Services.prefs.clearUserPref("devtools.theme");
Services.prefs.clearUserPref(prefName);
ok(CustomizableUI.inDefaultState, "In default state after pref cleared");
is(undoResetButton.hidden, true, "Undo reset button should be hidden at end of test");

View File

@ -273,6 +273,12 @@ loop.shared.actions = (function() {
sessionToken: String,
sessionId: String,
expires: Number
}),
/**
* Used to indicate the user wishes to leave the room.
*/
LeaveRoom: Action.define("leaveRoom", {
})
};
})();

View File

@ -52,10 +52,12 @@ loop.store.ActiveRoomStore = (function() {
this._dispatcher.register(this, [
"roomFailure",
"setupWindowData",
"fetchServerData",
"updateRoomInfo",
"joinRoom",
"joinedRoom",
"windowUnload"
"windowUnload",
"leaveRoom"
]);
/**
@ -151,6 +153,26 @@ loop.store.ActiveRoomStore = (function() {
}.bind(this));
},
/**
* Execute fetchServerData event action from the dispatcher. Although
* this is to fetch the server data - for rooms on the standalone client,
* we don't actually need to get any data. Therefore we just save the
* data that is given to us for when the user chooses to join the room.
*
* @param {sharedActions.FetchServerData} actionData
*/
fetchServerData: function(actionData) {
if (actionData.windowType !== "room") {
// Nothing for us to do here, leave it to other stores.
return;
}
this.setStoreState({
roomToken: actionData.token,
roomState: ROOM_STATES.READY
});
},
/**
* Handles the updateRoomInfo action. Updates the room data and
* sets the state to `READY`.
@ -213,6 +235,13 @@ loop.store.ActiveRoomStore = (function() {
this._leaveRoom();
},
/**
* Handles a room being left.
*/
leaveRoom: function() {
this._leaveRoom();
},
/**
* Handles setting of the refresh timeout callback.
*

View File

@ -99,6 +99,7 @@
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
<script type="text/javascript" src="js/standaloneRoomViews.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>

View File

@ -0,0 +1,188 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true */
/**
* The StandaloneMozLoop implementation reflects that of the mozLoop API for Loop
* in the desktop code. Not all functions are implemented.
*/
var loop = loop || {};
loop.StandaloneMozLoop = (function(mozL10n) {
"use strict";
/**
* The maximum number of clients that we currently support.
*/
var ROOM_MAX_CLIENTS = 2;
/**
* Validates a data object to confirm it has the specified properties.
*
* @param {Object} data The data object to verify
* @param {Array} schema The validation schema
* @return Returns all properties if valid, or an empty object if no properties
* have been specified.
*/
function validate(data, schema) {
if (!schema) {
return {};
}
return new loop.validate.Validator(schema).validate(data);
}
/**
* Generic handler for XHR failures.
*
* @param {Function} callback Callback(err)
* @param jqXHR See jQuery docs
* @param textStatus See jQuery docs
* @param errorThrown See jQuery docs
*/
function failureHandler(callback, jqXHR, textStatus, errorThrown) {
var jsonErr = jqXHR && jqXHR.responseJSON || {};
var message = "HTTP " + jqXHR.status + " " + errorThrown;
// Create an error with server error `errno` code attached as a property
var err = new Error(message);
err.errno = jsonErr.errno;
callback(err);
}
/**
* StandaloneMozLoopRooms is used as part of StandaloneMozLoop to define
* the rooms sub-object. We do it this way so that we can share the options
* information from the parent.
*/
var StandaloneMozLoopRooms = function(options) {
options = options || {};
if (!options.baseServerUrl) {
throw new Error("missing required baseServerUrl");
}
this._baseServerUrl = options.baseServerUrl;
};
StandaloneMozLoopRooms.prototype = {
/**
* Internal function to actually perform a post to a room.
*
* @param {String} roomToken The rom token.
* @param {String} sessionToken The sessionToken for the room if known
* @param {Object} roomData The data to send with the request
* @param {Array} expectedProps The expected properties we should receive from the
* server
* @param {Function} callback The callback for when the request completes. The
* first parameter is non-null on error, the second parameter
* is the response data.
*/
_postToRoom: function(roomToken, sessionToken, roomData, expectedProps, callback) {
var req = $.ajax({
url: this._baseServerUrl + "/rooms/" + roomToken,
method: "POST",
contentType: "application/json",
dataType: "json",
data: JSON.stringify(roomData),
beforeSend: function(xhr) {
if (sessionToken) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(sessionToken));
}
}
});
req.done(function(responseData) {
try {
callback(null, validate(responseData, expectedProps));
} catch (err) {
console.error("Error requesting call info", err.message);
callback(err);
}
}.bind(this));
req.fail(failureHandler.bind(this, callback));
},
/**
* Joins a room
*
* @param {String} roomToken The room token.
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`.
*/
join: function(roomToken, callback) {
this._postToRoom(roomToken, null, {
action: "join",
displayName: mozL10n.get("rooms_display_name_guest"),
clientMaxSize: ROOM_MAX_CLIENTS
}, {
apiKey: String,
sessionId: String,
sessionToken: String,
expires: Number
}, callback);
},
/**
* Refreshes a room
*
* @param {String} roomToken The room token.
* @param {String} sessionToken The session token for the session that has been
* joined
* @param {Function} callback Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`.
*/
refreshMembership: function(roomToken, sessionToken, callback) {
this._postToRoom(roomToken, sessionToken, {
action: "refresh",
sessionToken: sessionToken
}, {
expires: Number
}, callback);
},
/**
* Leaves a room. Although this is an sync function, no data is returned
* from the server.
*
* @param {String} roomToken The room token.
* @param {String} sessionToken The session token for the session that has been
* joined
* @param {Function} callback Optional. Function that will be invoked once the operation
* finished. The first argument passed will be an
* `Error` object or `null`.
*/
leave: function(roomToken, sessionToken, callback) {
if (!callback) {
callback = function(error) {
if (error) {
console.error(error);
}
};
}
this._postToRoom(roomToken, sessionToken, {
action: "leave",
sessionToken: sessionToken
}, null, callback);
}
};
var StandaloneMozLoop = function(options) {
options = options || {};
if (!options.baseServerUrl) {
throw new Error("missing required baseServerUrl");
}
this._baseServerUrl = options.baseServerUrl;
this.rooms = new StandaloneMozLoopRooms(options);
};
return StandaloneMozLoop;
})(navigator.mozL10n);

View File

@ -10,12 +10,16 @@ var loop = loop || {};
loop.standaloneRoomViews = (function() {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
var sharedActions = loop.shared.actions;
var StandaloneRoomView = React.createClass({displayName: 'StandaloneRoomView',
mixins: [Backbone.Events],
propTypes: {
activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
},
getInitialState: function() {
@ -41,12 +45,34 @@ loop.standaloneRoomViews = (function() {
this.stopListening(this.props.activeRoomStore);
},
joinRoom: function() {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
},
leaveRoom: function() {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
},
// XXX Implement tests for this view when we do the proper views
// - bug 1074705 and others
render: function() {
return (
React.DOM.div(null,
React.DOM.div(null, this.state.roomState)
)
);
switch(this.state.roomState) {
case ROOM_STATES.READY: {
return (
React.DOM.div(null, React.DOM.button({onClick: this.joinRoom}, "Join"))
);
}
case ROOM_STATES.JOINED: {
return (
React.DOM.div(null, React.DOM.button({onClick: this.leaveRoom}, "Leave"))
);
}
default: {
return (
React.DOM.div(null, this.state.roomState)
);
}
}
}
});

View File

@ -10,12 +10,16 @@ var loop = loop || {};
loop.standaloneRoomViews = (function() {
"use strict";
var ROOM_STATES = loop.store.ROOM_STATES;
var sharedActions = loop.shared.actions;
var StandaloneRoomView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
activeRoomStore:
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired
React.PropTypes.instanceOf(loop.store.ActiveRoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
},
getInitialState: function() {
@ -41,12 +45,34 @@ loop.standaloneRoomViews = (function() {
this.stopListening(this.props.activeRoomStore);
},
joinRoom: function() {
this.props.dispatcher.dispatch(new sharedActions.JoinRoom());
},
leaveRoom: function() {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
},
// XXX Implement tests for this view when we do the proper views
// - bug 1074705 and others
render: function() {
return (
<div>
<div>{this.state.roomState}</div>
</div>
);
switch(this.state.roomState) {
case ROOM_STATES.READY: {
return (
<div><button onClick={this.joinRoom}>Join</button></div>
);
}
case ROOM_STATES.JOINED: {
return (
<div><button onClick={this.leaveRoom}>Leave</button></div>
);
}
default: {
return (
<div>{this.state.roomState}</div>
);
}
}
}
});

View File

@ -893,7 +893,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired,
activeRoomStore: React.PropTypes.instanceOf(
loop.store.ActiveRoomStore).isRequired
loop.store.ActiveRoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
getInitialState: function() {
@ -937,7 +938,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
case "room": {
return (
loop.standaloneRoomViews.StandaloneRoomView({
activeRoomStore: this.props.activeRoomStore}
activeRoomStore: this.props.activeRoomStore,
dispatcher: this.props.dispatcher}
)
);
}
@ -958,6 +960,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/
function init() {
var helper = new sharedUtils.Helper();
var standaloneMozLoop = new loop.StandaloneMozLoop({
baseServerUrl: loop.config.serverUrl
});
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
@ -991,9 +996,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
// XXX Bug 1074702 will introduce a mozLoop compatible object for
// the standalone rooms.
mozLoop: {}
mozLoop: standaloneMozLoop
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());
});
React.renderComponent(WebappRootView({
@ -1004,7 +1011,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
sdk: OT,
feedbackApiClient: feedbackApiClient,
standaloneAppStore: standaloneAppStore,
activeRoomStore: activeRoomStore}
activeRoomStore: activeRoomStore,
dispatcher: dispatcher}
), document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated

View File

@ -893,7 +893,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
standaloneAppStore: React.PropTypes.instanceOf(
loop.store.StandaloneAppStore).isRequired,
activeRoomStore: React.PropTypes.instanceOf(
loop.store.ActiveRoomStore).isRequired
loop.store.ActiveRoomStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
getInitialState: function() {
@ -938,6 +939,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
return (
<loop.standaloneRoomViews.StandaloneRoomView
activeRoomStore={this.props.activeRoomStore}
dispatcher={this.props.dispatcher}
/>
);
}
@ -958,6 +960,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/
function init() {
var helper = new sharedUtils.Helper();
var standaloneMozLoop = new loop.StandaloneMozLoop({
baseServerUrl: loop.config.serverUrl
});
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
@ -991,9 +996,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var activeRoomStore = new loop.store.ActiveRoomStore({
dispatcher: dispatcher,
// XXX Bug 1074702 will introduce a mozLoop compatible object for
// the standalone rooms.
mozLoop: {}
mozLoop: standaloneMozLoop
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());
});
React.renderComponent(<WebappRootView
@ -1005,6 +1012,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
feedbackApiClient={feedbackApiClient}
standaloneAppStore={standaloneAppStore}
activeRoomStore={activeRoomStore}
dispatcher={dispatcher}
/>, document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated

View File

@ -115,6 +115,7 @@ rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start
rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
rooms_room_joined_label=Someone has joined the conversation!
rooms_room_join_label=Join the conversation
rooms_display_name_guest=Guest
## LOCALIZATION_NOTE(standalone_title_with_status): {{clientShortname}} will be
## replaced by the brand name and {{currentStatus}} will be replaced

View File

@ -6,7 +6,7 @@
<head>
<meta charset="utf-8">
<title>Loop desktop-local mocha tests</title>
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-2.0.1.css">
</head>
<body>
<div id="mocha">
@ -22,7 +22,7 @@
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-1.17.1.js"></script>
<script src="../shared/vendor/mocha-2.0.1.js"></script>
<script src="../shared/vendor/chai-1.9.0.js"></script>
<script src="../shared/vendor/sinon-1.9.0.js"></script>
<script>

View File

@ -159,6 +159,26 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#fetchServerData", function() {
it("should save the token", function() {
store.fetchServerData(new sharedActions.FetchServerData({
windowType: "room",
token: "fakeToken"
}));
expect(store.getStoreState().roomToken).eql("fakeToken");
});
it("should set the state to `READY`", function() {
store.fetchServerData(new sharedActions.FetchServerData({
windowType: "room",
token: "fakeToken"
}));
expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
});
});
describe("#updateRoomInfo", function() {
var fakeRoomInfo;
@ -172,13 +192,13 @@ describe("loop.store.ActiveRoomStore", function () {
});
it("should set the state to READY", function() {
store.updateRoomInfo(fakeRoomInfo);
store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
it("should save the room information", function() {
store.updateRoomInfo(fakeRoomInfo);
store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
var state = store.getStoreState();
expect(state.roomName).eql(fakeRoomInfo.roomName);
@ -247,13 +267,13 @@ describe("loop.store.ActiveRoomStore", function () {
});
it("should set the state to `JOINED`", function() {
store.joinedRoom(fakeJoinedData);
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
expect(store._storeState.roomState).eql(ROOM_STATES.JOINED);
});
it("should store the session and api values", function() {
store.joinedRoom(fakeJoinedData);
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
var state = store.getStoreState();
expect(state.apiKey).eql(fakeJoinedData.apiKey);
@ -263,7 +283,7 @@ describe("loop.store.ActiveRoomStore", function () {
it("should call mozLoop.rooms.refreshMembership before the expiresTime",
function() {
store.joinedRoom(fakeJoinedData);
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
sandbox.clock.tick(fakeJoinedData.expires * 1000);
@ -277,7 +297,7 @@ describe("loop.store.ActiveRoomStore", function () {
fakeMozLoop.rooms.refreshMembership.callsArgWith(2,
null, {expires: 40});
store.joinedRoom(fakeJoinedData);
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
// Clock tick for the first expiry time (which
// sets up the refreshMembership).
@ -296,7 +316,7 @@ describe("loop.store.ActiveRoomStore", function () {
var fakeError = new Error("fake");
fakeMozLoop.rooms.refreshMembership.callsArgWith(2, fakeError);
store.joinedRoom(fakeJoinedData);
store.joinedRoom(new sharedActions.JoinedRoom(fakeJoinedData));
// Clock tick for the first expiry time (which
// sets up the refreshMembership).
@ -342,4 +362,37 @@ describe("loop.store.ActiveRoomStore", function () {
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
});
describe("#leaveRoom", function() {
beforeEach(function() {
store.setStoreState({
roomState: ROOM_STATES.JOINED,
roomToken: "fakeToken",
sessionToken: "1627384950"
});
});
it("should clear any existing timeout", function() {
sandbox.stub(window, "clearTimeout");
store._timeout = {};
store.leaveRoom();
sinon.assert.calledOnce(clearTimeout);
});
it("should call mozLoop.rooms.leave", function() {
store.leaveRoom();
sinon.assert.calledOnce(fakeMozLoop.rooms.leave);
sinon.assert.calledWithExactly(fakeMozLoop.rooms.leave,
"fakeToken", "1627384950");
});
it("should set the state to ready", function() {
store.leaveRoom();
expect(store._storeState.roomState).eql(ROOM_STATES.READY);
});
});
});

View File

@ -6,7 +6,7 @@
<head>
<meta charset="utf-8">
<title>Loop shared mocha tests</title>
<link rel="stylesheet" media="all" href="vendor/mocha-1.17.1.css">
<link rel="stylesheet" media="all" href="vendor/mocha-2.0.1.css">
</head>
<body>
<div id="mocha">
@ -23,7 +23,7 @@
<script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
<!-- test dependencies -->
<script src="vendor/mocha-1.17.1.js"></script>
<script src="vendor/mocha-2.0.1.js"></script>
<script src="vendor/chai-1.9.0.js"></script>
<script src="vendor/sinon-1.9.0.js"></script>
<script>

View File

@ -6,7 +6,7 @@
<head>
<meta charset="utf-8">
<title>Loop mocha tests</title>
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-1.17.1.css">
<link rel="stylesheet" media="all" href="../shared/vendor/mocha-2.0.1.css">
</head>
<body>
<div id="mocha">
@ -22,7 +22,7 @@
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
<script src="../../standalone/content/libs/l10n-gaia-02ca67948fe8.js"></script>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-1.17.1.js"></script>
<script src="../shared/vendor/mocha-2.0.1.js"></script>
<script src="../shared/vendor/chai-1.9.0.js"></script>
<script src="../shared/vendor/sinon-1.9.0.js"></script>
<script src="../shared/sdk_mock.js"></script>
@ -44,11 +44,13 @@
<script src="../../standalone/content/js/multiplexGum.js"></script>
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->
<!-- Test scripts -->
<script src="standalone_client_test.js"></script>
<script src="standaloneAppStore_test.js"></script>
<script src="standaloneMozLoop_test.js"></script>
<script src="webapp_test.js"></script>
<script src="multiplexGum_test.js"></script>
<script>

View File

@ -199,7 +199,7 @@ describe("loop.standaloneMedia._MultiplexGum", function() {
it("should not call a getPermsAndCacheMedia success callback at the time" +
" of gUM success callback fires",
function(done) {
function() {
var fakeLocalStream = {};
multiplexGum.userMedia.localStream = fakeLocalStream;
navigator.originalGum.callsArgWith(1, fakeLocalStream);
@ -219,23 +219,21 @@ describe("loop.standaloneMedia._MultiplexGum", function() {
}, function() {
sinon.assert.fail("error callback should not have fired");
reject();
done();
});
});
promiseCalledOnce.then(function() {
return promiseCalledOnce.then(function() {
defaultGum(null, function gUMSuccess(localStream2) {
expect(localStream2).to.eql(fakeLocalStream);
expect(multiplexGum.userMedia).to.have.property('pending', false);
expect(multiplexGum.userMedia.successCallbacks.length).to.equal(0);
done();
});
});
});
it("should not call a getPermsAndCacheMedia error callback when the " +
" gUM error callback fires",
function(done) {
function() {
var fakeError = "monkeys ate the stream";
multiplexGum.userMedia.error = fakeError;
navigator.originalGum.callsArgWith(2, fakeError);
@ -244,7 +242,6 @@ describe("loop.standaloneMedia._MultiplexGum", function() {
multiplexGum.getPermsAndCacheMedia(null, function() {
sinon.assert.fail("success callback should not have fired");
reject();
done();
}, function gPACMError(errString) {
expect(errString).to.eql(fakeError);
expect(multiplexGum.userMedia).to.have.property('pending', false);
@ -256,12 +253,11 @@ describe("loop.standaloneMedia._MultiplexGum", function() {
});
});
promiseCalledOnce.then(function() {
return promiseCalledOnce.then(function() {
defaultGum(null, function() {},
function gUMError(errString) {
expect(errString).to.eql(fakeError);
expect(multiplexGum.userMedia).to.have.property('pending', false);
done();
});
});
});

View File

@ -0,0 +1,178 @@
/* 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/. */
var expect = chai.expect;
describe("loop.StandaloneMozLoop", function() {
"use strict";
var sandbox, fakeXHR, requests, callback, mozLoop;
var fakeToken, fakeBaseServerUrl, fakeServerErrorDescription;
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
fakeXHR.xhr.onCreate = function (xhr) {
requests.push(xhr);
};
fakeBaseServerUrl = "http://fake.api";
fakeServerErrorDescription = {
code: 401,
errno: 101,
error: "error",
message: "invalid token",
info: "error info"
};
callback = sinon.spy();
mozLoop = new loop.StandaloneMozLoop({
baseServerUrl: fakeBaseServerUrl
});
});
afterEach(function() {
sandbox.restore();
});
describe("#constructor", function() {
it("should require a baseServerUrl setting", function() {
expect(function() {
new loop.StandaloneMozLoop();
}).to.Throw(Error, /required/);
});
});
describe("#rooms.join", function() {
it("should POST to the server", function() {
mozLoop.rooms.join("fakeToken", callback);
expect(requests).to.have.length.of(1);
expect(requests[0].url).eql(fakeBaseServerUrl + "/rooms/fakeToken");
expect(requests[0].method).eql("POST");
var requestData = JSON.parse(requests[0].requestBody);
expect(requestData.action).eql("join");
});
it("should call the callback with success parameters", function() {
mozLoop.rooms.join("fakeToken", callback);
var sessionData = {
apiKey: "12345",
sessionId: "54321",
sessionToken: "another token",
expires: 20
};
requests[0].respond(200, {"Content-Type": "application/json"},
JSON.stringify(sessionData));
sinon.assert.calledOnce(callback);
sinon.assert.calledWithExactly(callback, null, sessionData);
});
it("should call the callback with failure parameters", function() {
mozLoop.rooms.join("fakeToken", callback);
requests[0].respond(401, {"Content-Type": "application/json"},
JSON.stringify(fakeServerErrorDescription));
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /HTTP 401 Unauthorized/.test(err.message);
}));
});
});
describe("#rooms.refreshMembership", function() {
var mozLoop, fakeServerErrorDescription;
beforeEach(function() {
mozLoop = new loop.StandaloneMozLoop({
baseServerUrl: fakeBaseServerUrl
});
fakeServerErrorDescription = {
code: 401,
errno: 101,
error: "error",
message: "invalid token",
info: "error info"
};
});
it("should POST to the server", function() {
mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
expect(requests).to.have.length.of(1);
expect(requests[0].url).eql(fakeBaseServerUrl + "/rooms/fakeToken");
expect(requests[0].method).eql("POST");
expect(requests[0].requestHeaders.Authorization)
.eql("Basic " + btoa("fakeSessionToken"));
var requestData = JSON.parse(requests[0].requestBody);
expect(requestData.action).eql("refresh");
expect(requestData.sessionToken).eql("fakeSessionToken");
});
it("should call the callback with success parameters", function() {
mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
var responseData = {
expires: 20
};
requests[0].respond(200, {"Content-Type": "application/json"},
JSON.stringify(responseData));
sinon.assert.calledOnce(callback);
sinon.assert.calledWithExactly(callback, null, responseData);
});
it("should call the callback with failure parameters", function() {
mozLoop.rooms.refreshMembership("fakeToken", "fakeSessionToken", callback);
requests[0].respond(401, {"Content-Type": "application/json"},
JSON.stringify(fakeServerErrorDescription));
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /HTTP 401 Unauthorized/.test(err.message);
}));
});
});
describe("#rooms.leave", function() {
it("should POST to the server", function() {
mozLoop.rooms.leave("fakeToken", "fakeSessionToken", callback);
expect(requests).to.have.length.of(1);
expect(requests[0].url).eql(fakeBaseServerUrl + "/rooms/fakeToken");
expect(requests[0].method).eql("POST");
expect(requests[0].requestHeaders.Authorization)
.eql("Basic " + btoa("fakeSessionToken"));
var requestData = JSON.parse(requests[0].requestBody);
expect(requestData.action).eql("leave");
});
it("should call the callback with success parameters", function() {
mozLoop.rooms.leave("fakeToken", "fakeSessionToken", callback);
requests[0].respond(204);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithExactly(callback, null, {});
});
it("should call the callback with failure parameters", function() {
mozLoop.rooms.leave("fakeToken", "fakeSessionToken", callback);
requests[0].respond(401, {"Content-Type": "application/json"},
JSON.stringify(fakeServerErrorDescription));
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /HTTP 401 Unauthorized/.test(err.message);
}));
});
});
});

View File

@ -20,11 +20,18 @@ XPCOMUtils.defineLazyModuleGetter(this, "console",
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer",
"resource://gre/modules/devtools/dbg-server.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DebuggerClient",
"resource://gre/modules/devtools/dbg-client.jsm");
const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99;
const bundle = Services.strings.createBundle("chrome://browser/locale/devtools/toolbox.properties");
/**
* The method name to use for ES6 iteration. If symbols are enabled in this
* build, use Symbol.iterator; otherwise "@@iterator".
@ -593,6 +600,7 @@ let gDevToolsBrowser = {
let remoteEnabled = chromeEnabled && devtoolsRemoteEnabled &&
Services.prefs.getBoolPref("devtools.debugger.chrome-enabled");
toggleCmd("Tools:BrowserToolbox", remoteEnabled);
toggleCmd("Tools:BrowserContentToolbox", remoteEnabled && win.gMultiProcessBrowser);
// Enable Error Console?
let consoleEnabled = Services.prefs.getBoolPref("devtools.errorconsole.enabled");
@ -688,6 +696,61 @@ let gDevToolsBrowser = {
}
},
_getContentProcessTarget: function () {
// Create a DebuggerServer in order to connect locally to it
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
let transport = DebuggerServer.connectPipe();
let client = new DebuggerClient(transport);
let deferred = promise.defer();
client.connect(() => {
client.mainRoot.listProcesses(response => {
// Do nothing if there is only one process, the parent process.
let contentProcesses = response.processes.filter(p => (!p.parent));
if (contentProcesses.length < 1) {
let msg = bundle.GetStringFromName("toolbox.noContentProcess.message");
Services.prompt.alert(null, "", msg);
deferred.reject("No content processes available.");
return;
}
// Otherwise, arbitrary connect to the unique content process.
client.attachProcess(contentProcesses[0].id)
.then(response => {
let options = {
form: response.form,
client: client,
chrome: true
};
return devtools.TargetFactory.forRemoteTab(options);
})
.then(target => {
// Ensure closing the connection in order to cleanup
// the debugger client and also the server created in the
// content process
target.on("close", () => {
client.close();
});
deferred.resolve(target);
});
});
});
return deferred.promise;
},
openContentProcessToolbox: function () {
this._getContentProcessTarget()
.then(target => {
// Display a new toolbox, in a new window, with debugger by default
return gDevTools.showToolbox(target, "jsdebugger",
devtools.Toolbox.HostType.WINDOW);
});
},
/**
* Install WebIDE widget
*/

View File

@ -0,0 +1,320 @@
/* 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/. */
const Cu = Components.utils;
const {Services} = Cu.import("resource://gre/modules/Services.jsm");
const {require} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools;
const {AppManager} = require("devtools/webide/app-manager");
const {Connection} = require("devtools/client/connection-manager");
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
let devicePrefsKeys = {};
let table;
window.addEventListener("load", function onLoad() {
window.removeEventListener("load", onLoad);
document.getElementById("close").onclick = CloseUI;
AppManager.on("app-manager-update", OnAppManagerUpdate);
document.getElementById("device-preferences").onchange = UpdatePref;
document.getElementById("device-preferences").onclick = CheckReset;
document.getElementById("search-bar").onkeyup = document.getElementById("search-bar").onclick = SearchPref;
document.getElementById("custom-value").onclick = UpdateNewPref;
document.getElementById("custom-value-type").onchange = ClearNewPrefs;
document.getElementById("add-custom-preference").onkeyup = CheckNewPrefSubmit;
BuildUI();
}, true);
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
AppManager.off("app-manager-update", OnAppManagerUpdate);
});
function CloseUI() {
window.parent.UI.openProject();
}
function OnAppManagerUpdate(event, what) {
if (what == "connection" || what == "list-tabs-response") {
BuildUI();
}
}
function RenderByType(input, name, value, customType) {
value = customType || typeof value;
switch (value) {
case "boolean":
input.setAttribute("data-type", "boolean");
input.setAttribute("type", "checkbox");
break;
case "number":
input.setAttribute("data-type", "number");
input.setAttribute("type", "number");
break;
default:
input.setAttribute("data-type", "string");
input.setAttribute("type", "text");
break;
}
return input;
}
let defaultPref; // Used by tests
function ResetToDefault(name, input, button) {
AppManager.preferenceFront.clearUserPref(name);
let dataType = input.getAttribute("data-type");
let tr = document.getElementById("row-" + name);
switch (dataType) {
case "boolean":
defaultPref = AppManager.preferenceFront.getBoolPref(name);
defaultPref.then(boolean => {
input.checked = boolean;
}, () => {
input.checked = false;
tr.parentNode.removeChild(tr);
});
break;
case "number":
defaultPref = AppManager.preferenceFront.getIntPref(name);
defaultPref.then(number => {
input.value = number;
}, () => {
tr.parentNode.removeChild(tr);
});
break;
default:
defaultPref = AppManager.preferenceFront.getCharPref(name);
defaultPref.then(string => {
input.value = string;
}, () => {
tr.parentNode.removeChild(tr);
});
break;
}
button.classList.add("hide");
}
function SaveByType(options) {
let prefName = options.id;
let inputType = options.type;
let value = options.value;
let input = document.getElementById(prefName);
switch (inputType) {
case "boolean":
AppManager.preferenceFront.setBoolPref(prefName, input.checked);
break;
case "number":
AppManager.preferenceFront.setIntPref(prefName, value);
break;
default:
AppManager.preferenceFront.setCharPref(prefName, value);
break;
}
}
function CheckNewPrefSubmit(event) {
if (event.keyCode === 13) {
document.getElementById("custom-value").click();
}
}
function ClearNewPrefs() {
let customTextEl = table.querySelector("#custom-value-text");
if (customTextEl.checked) {
customTextEl.checked = false;
} else {
customTextEl.value = "";
}
UpdateFieldType();
}
function UpdateFieldType() {
let customValueType = table.querySelector("#custom-value-type").value;
let customTextEl = table.querySelector("#custom-value-text");
let customText = customTextEl.value;
if (customValueType.length === 0) {
return false;
}
switch (customValueType) {
case "boolean":
customTextEl.type = "checkbox";
customText = customTextEl.checked;
break;
case "number":
customText = parseInt(customText, 10) || 0;
customTextEl.type = "number";
break;
default:
customTextEl.type = "text";
break;
}
return customValueType;
}
function UpdateNewPref(event) {
let customValueType = UpdateFieldType();
if (!customValueType) {
return;
}
let customRow = table.querySelector("tr:nth-of-type(2)");
let customTextEl = table.querySelector("#custom-value-text");
let customTextNameEl = table.querySelector("#custom-value-name");
if (customTextEl.validity.valid) {
let customText = customTextEl.value;
if (customValueType === "boolean") {
customText = customTextEl.checked;
}
let customTextName = customTextNameEl.value.replace(/[^A-Za-z0-9\.\-_]/gi, "");
GenerateField(customTextName, customText, true, customValueType, customRow);
SaveByType({
id: customTextName,
type: customValueType,
value: customText
});
customTextNameEl.value = "";
ClearNewPrefs();
}
}
function CheckReset(event) {
if (event.target.classList.contains("reset")) {
let btnId = event.target.getAttribute("data-id");
let input = document.getElementById(btnId);
ResetToDefault(btnId, input, event.target);
}
}
function UpdatePref(event) {
if (event.target) {
let inputType = event.target.getAttribute("data-type");
let inputValue = event.target.checked || event.target.value;
if (event.target.nodeName == "input" &&
event.target.validity.valid &&
event.target.classList.contains("editable")) {
let id = event.target.id;
if (inputType == "boolean") {
if (event.target.checked) {
inputValue = true;
} else {
inputValue = false;
}
}
SaveByType({
id: id,
type: inputType,
value: inputValue
});
document.getElementById("btn-" + id).classList.remove("hide");
}
}
}
function GenerateField(name, value, hasUserValue, customType, newPreferenceRow) {
if (name.length < 1) {
return;
}
let sResetDefault = Strings.GetStringFromName("devicepreferences_reset_default");
devicePrefsKeys[name] = true;
let input = document.createElement("input");
let tr = document.createElement("tr");
tr.setAttribute("id", "row-" + name);
tr.classList.add("edit-row");
let td = document.createElement("td");
td.classList.add("preference-name");
td.textContent = name;
tr.appendChild(td);
td = document.createElement("td");
input.classList.add("editable");
input.setAttribute("id", name);
input = RenderByType(input, name, value, customType);
if (customType === "boolean" || input.type === "checkbox") {
input.checked = value;
} else {
input.value = value;
}
td.appendChild(input);
tr.appendChild(td);
td = document.createElement("td");
td.setAttribute("id", "td-" + name);
let button = document.createElement("button");
button.setAttribute("data-id", name);
button.setAttribute("id", "btn-" + name);
button.classList.add("reset");
button.textContent = sResetDefault;
td.appendChild(button);
if (!hasUserValue) {
button.classList.add("hide");
}
tr.appendChild(td);
// If this is a new preference, add it to the top of the table.
if (newPreferenceRow) {
let existingPref = table.querySelector("#" + name);
if (!existingPref) {
table.insertBefore(tr, newPreferenceRow);
} else {
existingPref.value = value;
}
} else {
table.appendChild(tr);
}
}
function SearchPref(event) {
if (event.target.value.length) {
let stringMatch = new RegExp(event.target.value, "i");
for (let key in devicePrefsKeys) {
let row = document.getElementById("row-" + key);
if (key.match(stringMatch)) {
row.classList.remove("hide");
} else if (row) {
row.classList.add("hide");
}
}
} else {
var trs = document.getElementsByTagName("tr");
for (let i = 0; i < trs.length; i++) {
trs[i].classList.remove("hide");
}
}
}
function BuildUI() {
table = document.querySelector("table");
let trs = table.querySelectorAll("tr:not(#add-custom-preference)");
for (var i = 0; i < trs.length; i++) {
table.removeChild(trs[i]);
}
if (AppManager.connection &&
AppManager.connection.status == Connection.Status.CONNECTED &&
AppManager.preferenceFront) {
AppManager.preferenceFront.getAllPrefs().then(json => {
let devicePrefs = Object.keys(json);
devicePrefs.sort();
for (let i = 0; i < devicePrefs.length; i++) {
GenerateField(devicePrefs[i], json[devicePrefs[i]].value, json[devicePrefs[i]].hasUserValue);
}
});
} else {
CloseUI();
}
}

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!DOCTYPE html [
<!ENTITY % webideDTD SYSTEM "chrome://browser/locale/devtools/webide.dtd" >
%webideDTD;
]>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf8"/>
<link rel="stylesheet" href="chrome://webide/skin/deck.css" type="text/css"/>
<link rel="stylesheet" href="chrome://webide/skin/devicepreferences.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://webide/content/devicepreferences.js"></script>
</head>
<body>
<header>
<div id="controls">
<a id="close">&deck_close;</a>
</div>
<h1>&devicepreferences_title;</h1>
<div id="search">
<input type="text" id="search-bar" placeholder="&devicepreferences_search;"/>
</div>
</header>
<table id="device-preferences">
<tr id="add-custom-preference">
<td>
<select id="custom-value-type">
<option value="" selected="selected">&devicepreferences_typenone;</option>
<option value="boolean">&devicepreferences_typeboolean;</option>
<option value="number">&devicepreferences_typenumber;</option>
<option value="string">&devicepreferences_typestring;</option>
</select>
<input type="text" id="custom-value-name" placeholder="&devicepreferences_newname;"/>
</td>
<td class="custom-input">
<input type="text" id="custom-value-text" placeholder="&devicepreferences_newtext;"/>
</td>
<td>
<button id="custom-value" class="new-editable">&devicepreferences_addnew;</button>
</td>
</tr>
</table>
</body>
</html>

View File

@ -20,3 +20,5 @@ webide.jar:
content/prefs.xhtml (prefs.xhtml)
content/monitor.xhtml (monitor.xhtml)
content/monitor.js (monitor.js)
content/devicepreferences.js (devicepreferences.js)
content/devicepreferences.xhtml (devicepreferences.xhtml)

View File

@ -765,6 +765,7 @@ let UI = {
document.querySelector("#cmd_play").setAttribute("disabled", "true");
document.querySelector("#cmd_stop").setAttribute("disabled", "true");
document.querySelector("#cmd_toggleToolbox").setAttribute("disabled", "true");
document.querySelector("#cmd_showDevicePrefs").setAttribute("disabled", "true");
return;
}
@ -827,6 +828,7 @@ let UI = {
let permissionsCmd = document.querySelector("#cmd_showPermissionsTable");
let detailsCmd = document.querySelector("#cmd_showRuntimeDetails");
let disconnectCmd = document.querySelector("#cmd_disconnectRuntime");
let devicePrefsCmd = document.querySelector("#cmd_showDevicePrefs");
let box = document.querySelector("#runtime-actions");
@ -837,6 +839,9 @@ let UI = {
permissionsCmd.removeAttribute("disabled");
screenshotCmd.removeAttribute("disabled");
}
if (AppManager.preferenceFront) {
devicePrefsCmd.removeAttribute("disabled");
}
disconnectCmd.removeAttribute("disabled");
runtimePanelButton.setAttribute("active", "true");
} else {
@ -844,6 +849,7 @@ let UI = {
permissionsCmd.setAttribute("disabled", "true");
screenshotCmd.setAttribute("disabled", "true");
disconnectCmd.setAttribute("disabled", "true");
devicePrefsCmd.setAttribute("disabled", "true");
runtimePanelButton.removeAttribute("active");
}
@ -1181,6 +1187,10 @@ let Cmds = {
UI.selectDeckPanel("runtimedetails");
},
showDevicePrefs: function() {
UI.selectDeckPanel("devicepreferences");
},
showMonitor: function() {
UI.selectDeckPanel("monitor");
},

View File

@ -35,6 +35,7 @@
<command id="cmd_newApp" oncommand="Cmds.newApp()" label="&projectMenu_newApp_label;"/>
<command id="cmd_importPackagedApp" oncommand="Cmds.importPackagedApp()" label="&projectMenu_importPackagedApp_label;"/>
<command id="cmd_importHostedApp" oncommand="Cmds.importHostedApp()" label="&projectMenu_importHostedApp_label;"/>
<command id="cmd_showDevicePrefs" label="&runtimeMenu_showDevicePrefs_label;" oncommand="Cmds.showDevicePrefs()"/>
<command id="cmd_removeProject" oncommand="Cmds.removeProject()" label="&projectMenu_remove_label;"/>
<command id="cmd_showProjectPanel" oncommand="Cmds.showProjectPanel()"/>
<command id="cmd_showRuntimePanel" oncommand="Cmds.showRuntimePanel()"/>
@ -78,6 +79,7 @@
<menuitem command="cmd_takeScreenshot" accesskey="&runtimeMenu_takeScreenshot_accesskey;"/>
<menuitem command="cmd_showPermissionsTable" accesskey="&runtimeMenu_showPermissionTable_accesskey;"/>
<menuitem command="cmd_showRuntimeDetails" accesskey="&runtimeMenu_showDetails_accesskey;"/>
<menuitem command="cmd_showDevicePrefs" accesskey="&runtimeMenu_showDevicePrefs_accesskey;"/>
<menuseparator/>
<menuitem command="cmd_disconnectRuntime" accesskey="&runtimeMenu_disconnect_accesskey;"/>
</menupopup>
@ -165,6 +167,7 @@
<vbox flex="1" id="runtime-actions">
<toolbarbutton class="panel-item" id="runtime-details" command="cmd_showRuntimeDetails"/>
<toolbarbutton class="panel-item" id="runtime-permissions" command="cmd_showPermissionsTable"/>
<toolbarbutton class="panel-item" id="runtime-preferences" command="cmd_showDevicePrefs"/>
<toolbarbutton class="panel-item" id="runtime-screenshot" command="cmd_takeScreenshot"/>
<toolbarbutton class="panel-item" id="runtime-disconnect" command="cmd_disconnectRuntime"/>
</vbox>
@ -182,6 +185,7 @@
<iframe id="deck-panel-permissionstable" flex="1" src="permissionstable.xhtml"/>
<iframe id="deck-panel-runtimedetails" flex="1" src="runtimedetails.xhtml"/>
<iframe id="deck-panel-monitor" flex="1" lazysrc="monitor.xhtml"/>
<iframe id="deck-panel-devicepreferences" flex="1" src="devicepreferences.xhtml"/>
</deck>
<splitter hidden="true" class="devtools-horizontal-splitter" orient="vertical"/>
<!-- toolbox iframe will be inserted here -->

View File

@ -38,3 +38,4 @@ support-files =
[test_deviceinfo.html]
[test_autoconnect_runtime.html]
[test_telemetry.html]
[test_device_preferences.html]

View File

@ -173,6 +173,25 @@ function removeTab(aTab, aWindow) {
return deferred.promise;
}
function connectToLocalRuntime(aWindow) {
info("Loading local runtime.");
let panelNode = aWindow.document.querySelector("#runtime-panel");
let items = panelNode.querySelectorAll(".runtime-panel-item-other");
is(items.length, 2, "Found 2 custom runtime buttons");
let deferred = promise.defer();
aWindow.AppManager.on("app-manager-update", function onUpdate(e,w) {
if (w == "list-tabs-response") {
aWindow.AppManager.off("app-manager-update", onUpdate);
deferred.resolve();
}
});
items[1].click();
return deferred.promise;
}
function handleError(aError) {
ok(false, "Got an error: " + aError.message + "\n" + aError.stack);
finish();

View File

@ -0,0 +1,242 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
}
let win = yield openWebIDE();
let prefIframe = win.document.querySelector("#deck-panel-devicepreferences");
yield documentIsLoaded(prefIframe.contentWindow.document);
win.AppManager.update("runtimelist");
yield connectToLocalRuntime(win);
yield nextTick();
let prefs = win.document.querySelector("#cmd_showDevicePrefs");
ok(!prefs.hasAttribute("disabled"), "device prefs cmd enabled");
let deck = win.document.querySelector("#deck");
win.Cmds.showDevicePrefs();
is(deck.selectedPanel, prefIframe, "device preferences iframe selected");
yield nextTick();
let doc = prefIframe.contentWindow.document;
let fields = doc.querySelectorAll(".editable");
let preference = "accessibility.blockautorefresh";
let found = false;
// Trigger existing field change
for (let field of fields) {
if (field.id == preference) {
let button = doc.getElementById("btn-" + preference);
found = true;
ok(button.classList.contains("hide"), "Default field detected");
field.value = "custom";
field.click();
ok(!button.classList.contains("hide"), "Custom field detected");
break;
}
}
ok(found, "Found accessibility preference line");
// Add new preferences
found = false;
let customName = doc.querySelector("#custom-value-name");
let customValue = doc.querySelector("#custom-value-text");
let customValueType = doc.querySelector("#custom-value-type");
let customBtn = doc.querySelector("#custom-value");
let change = doc.createEvent("HTMLEvents");
change.initEvent("change", false, true);
// Add a new custom string preference
customValueType.value = "string";
customValueType.dispatchEvent(change);
customName.value = "new-string-pref!";
customValue.value = "test";
customBtn.click();
let newPref = doc.querySelector("#new-string-pref");
if (newPref) {
found = true;
is(newPref.type, "text", "Custom type is a string");
is(newPref.value, "test", "Custom string new value is correct");
}
ok(found, "Found new string preference line");
is(customName.value, "", "Custom string name reset");
is(customValue.value, "", "Custom string value reset");
// Add a new custom value with the <enter> key
found = false;
customName.value = "new-string-pref-two";
customValue.value = "test";
let newField = doc.querySelector("#add-custom-preference");
let enter = doc.createEvent("KeyboardEvent");
enter.initKeyEvent(
"keyup", true, true, null, false, false, false, false, 13, 0);
newField.dispatchEvent(enter);
newPref = doc.querySelector("#new-string-pref-two");
if (newPref) {
found = true;
is(newPref.type, "text", "Custom type is a string");
is(newPref.value, "test", "Custom string new value is correct");
}
ok(found, "Found new string preference line");
is(customName.value, "", "Custom string name reset");
is(customValue.value, "", "Custom string value reset");
// Edit existing custom string preference
newPref.value = "test2";
newPref.click();
is(newPref.value, "test2", "Custom string existing value is correct");
// Add a new custom integer preference with a valid integer
customValueType.value = "number";
customValueType.dispatchEvent(change);
customName.value = "new-integer-pref";
customValue.value = 1;
found = false;
customBtn.click();
newPref = doc.querySelector("#new-integer-pref");
if (newPref) {
found = true;
is(newPref.type, "number", "Custom type is a number");
is(newPref.value, 1, "Custom integer value is correct");
}
ok(found, "Found new integer preference line");
is(customName.value, "", "Custom integer name reset");
is(customValue.value, 0, "Custom integer value reset");
// Edit existing custom integer preference
newPref.value = 3;
newPref.click();
is(newPref.value, 3, "Custom integer existing value is correct");
// Reset a custom preference
let resetBtn = doc.querySelector("#btn-new-integer-pref");
resetBtn.click();
try {
yield prefIframe.contentWindow.defaultPref;
} catch(err) {
let prefRow = doc.querySelector("#row-new-integer-pref");
if (!prefRow) {
found = false;
}
ok(!found, "Custom preference removed");
}
// Reset an existing preference
let existingPref = doc.getElementById("accessibility.accesskeycausesactivation");
existingPref.click();
is(existingPref.checked, false, "Existing boolean value is correct");
resetBtn = doc.getElementById("btn-accessibility.accesskeycausesactivation");
resetBtn.click();
yield prefIframe.contentWindow.defaultPref;
ok(resetBtn.classList.contains("hide"), true, "Reset button hidden");
is(existingPref.checked, true, "Existing preference reset");
// Add a new custom boolean preference
customValueType.value = "boolean";
customValueType.dispatchEvent(change);
customName.value = "new-boolean-pref";
customValue.checked = true;
found = false;
customBtn.click();
newPref = doc.querySelector("#new-boolean-pref");
if (newPref) {
found = true;
is(newPref.type, "checkbox", "Custom type is a checkbox");
is(newPref.checked, true, "Custom boolean value is correctly true");
}
ok(found, "Found new boolean preference line");
// Mouse event trigger
var mouseClick = new MouseEvent("click", {
canBubble: true,
cancelable: true,
view: doc.parent,
});
found = false;
customValueType.value = "boolean";
customValueType.dispatchEvent(change);
customName.value = "new-boolean-pref2";
customValue.dispatchEvent(mouseClick);
customBtn.dispatchEvent(mouseClick);
newPref = doc.querySelector("#new-boolean-pref2");
if (newPref) {
found = true;
is(newPref.checked, true, "Custom boolean value is correctly false");
}
ok(found, "Found new second boolean preference line");
is(customName.value, "", "Custom boolean name reset");
is(customValue.checked, false, "Custom boolean value reset");
// Edit existing custom boolean preference
newPref.click();
is(newPref.checked, false, "Custom boolean existing value is correct");
// Search for a non-existent field
let searchField = doc.querySelector("#search-bar");
searchField.value = "![o_O]!";
searchField.click();
let preferencesTotal = doc.querySelectorAll("tr.edit-row").length;
let hiddenPreferences = doc.querySelectorAll("tr.hide");
is(hiddenPreferences.length, preferencesTotal, "Search keyword not found");
// Search for existing fields
searchField.value = "debugger";
searchField.click();
hiddenPreferences = doc.querySelectorAll("tr.hide");
isnot(hiddenPreferences.length, preferencesTotal, "Search keyword found");
doc.querySelector("#close").click();
ok(!deck.selectedPanel, "No panel selected");
DebuggerServer.destroy();
yield closeWebIDE(win);
SimpleTest.finish();
}).then(null, e => {
ok(false, "Exception: " + e);
SimpleTest.finish();
});
}
</script>
</body>
</html>

View File

@ -34,23 +34,7 @@
yield documentIsLoaded(permIframe.contentWindow.document);
yield documentIsLoaded(infoIframe.contentWindow.document);
win.AppManager._rebuildRuntimeList();
let panelNode = win.document.querySelector("#runtime-panel");
let items = panelNode.querySelectorAll(".runtime-panel-item-other");
is(items.length, 2, "Found 2 other runtimes button");
let deferred = promise.defer();
win.AppManager.on("app-manager-update", function onUpdate(e,w) {
if (w == "list-tabs-response") {
win.AppManager.off("app-manager-update", onUpdate);
deferred.resolve();
}
});
items[1].click();
yield deferred.promise;
yield connectToLocalRuntime(win);
yield nextTick();

View File

@ -0,0 +1,75 @@
/* 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/. */
html, body {
background: white;
}
.action {
display: inline;
}
.action[hidden] {
display: none;
}
#device-preferences {
font-family: sans-serif;
padding-left: 6px;
width: 100%;
table-layout: auto;
margin-top: 110px;
}
#custom-value-name {
width: 70%;
}
header {
background-color: rgba(255, 255, 255, 0.8);
border-bottom: 1px solid #EEE;
position: fixed;
top: 0;
left: 0;
right: 0;
height: 90px;
padding: 10px 20px;
}
#device-preferences td {
background-color: #f1f1f1;
border-bottom: 1px solid #ccc;
border-right: 1px solid #fff;
width: 33.3%;
}
#device-preferences td.preference-name {
width: 50%;
min-width: 400px;
word-break: break-all;
}
#device-preferences button {
display: inline-block;
font-family: sans-serif;
font-size: 0.7rem;
white-space: nowrap;
}
#device-preferences tr.hide, #device-preferences button.hide {
display: none;
}
#device-preferences .custom-input {
width: 300px;
}
#search {
margin-bottom: 20px;
width: 100%;
}
#search-bar {
width: 80%;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -15,3 +15,4 @@ webide.jar:
skin/runtimedetails.css (runtimedetails.css)
skin/permissionstable.css (permissionstable.css)
skin/monitor.css (monitor.css)
skin/devicepreferences.css (devicepreferences.css)

View File

@ -216,6 +216,7 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
#runtime-details,
#runtime-screenshot,
#runtime-permissions,
#runtime-preferences,
#runtime-disconnect,
#runtime-panel-nousbdevice,
#runtime-panel-noadbhelper,
@ -230,6 +231,7 @@ panel > .panel-arrowcontainer > .panel-arrowcontent {
#runtime-details { -moz-image-region: rect(156px,438px,182px,412px) }
#runtime-screenshot { -moz-image-region: rect(130px,438px,156px,412px) }
#runtime-permissions { -moz-image-region: rect(104px,438px,130px,412px) }
#runtime-preferences { -moz-image-region: rect(104px,462px,129px,438px) }
#runtime-disconnect { -moz-image-region: rect(52px,438px,78px,412px) }
#runtime-panel-nousbdevice { -moz-image-region: rect(156px,438px,182px,412px) }
#runtime-panel-noadbhelper { -moz-image-region: rect(234px,438px,260px,412px) }

View File

@ -268,6 +268,11 @@ These should match what Safari and other Apple applications use on OS X Lion. --
<!ENTITY browserToolboxMenu.label "Browser Toolbox">
<!ENTITY browserToolboxMenu.accesskey "e">
<!-- LOCALIZATION NOTE (browserContentToolboxMenu.label): This is the label for the
- application menu item that opens the browser content toolbox UI in the Tools menu.
- This toolbox allows to debug the chrome of the content process in multiprocess builds. -->
<!ENTITY browserContentToolboxMenu.label "Browser Content Toolbox">
<!ENTITY devToolbarCloseButton.tooltiptext "Close Developer Toolbar">
<!ENTITY devToolbarMenu.label "Developer Toolbar">
<!ENTITY devToolbarMenu.accesskey "v">

View File

@ -78,3 +78,8 @@ options.darkTheme.label=Dark theme
# LOCALIZATION NOTE (options.lightTheme.label)
# Used as a label for light theme
options.lightTheme.label=Light theme
# LOCALIZATION NOTE (toolbox.noContentProcess.message)
# Used as a message in the alert displayed when trying to open a browser
# content toolbox and there is no content process running
toolbox.noContentProcess.message=No content process running.

View File

@ -39,6 +39,8 @@
<!ENTITY runtimeMenu_showDetails_accesskey "E">
<!ENTITY runtimeMenu_showMonitor_label "Monitor">
<!ENTITY runtimeMenu_showMonitor_accesskey "M">
<!ENTITY runtimeMenu_showDevicePrefs_label "Device Preferences">
<!ENTITY runtimeMenu_showDevicePrefs_accesskey "D">
<!ENTITY viewMenu_label "View">
<!ENTITY viewMenu_accesskey "V">
@ -132,6 +134,17 @@
<!ENTITY runtimedetails_requestPrivileges "request higher privileges">
<!ENTITY runtimedetails_privilegesWarning "(Will reboot device. Requires root access.)">
<!-- Device Preferences -->
<!ENTITY devicepreferences_title "Device Preferences">
<!ENTITY devicepreferences_search "Search preferences">
<!ENTITY devicepreferences_newname "New preference name">
<!ENTITY devicepreferences_newtext "Preference value">
<!ENTITY devicepreferences_addnew "Add new preference">
<!ENTITY devicepreferences_typeboolean "Boolean">
<!ENTITY devicepreferences_typenumber "Integer">
<!ENTITY devicepreferences_typestring "String">
<!ENTITY devicepreferences_typenone "Select a type">
<!-- Monitor -->
<!ENTITY monitor_title "Monitor">
<!ENTITY monitor_help "Help">

View File

@ -72,3 +72,6 @@ status_valid=VALID
status_warning=WARNINGS
status_error=ERRORS
status_unknown=UNKNOWN
# Preferences
devicepreferences_reset_default=Reset to default

View File

@ -227,6 +227,13 @@ window:not([chromehidden~="toolbar"]) #urlbar-wrapper {
background-color: var(--tab-background-color);
}
.tabbrowser-tab[pinned][titlechanged]:not([selected="true"]) > .tab-stack > .tab-content {
/* The -2px in `calc` is the height of `tabToolbarNavbarOverlap` plus a 1px offset from the center */
background-image: radial-gradient(22px at center calc(100% - 2px), rgba(76,158,217,0.9) 13%, rgba(0,0,0,0.4) 16%, rgba(29,79,115,0) 70%);
background-position: center;
background-size: 100%;
}
.tabbrowser-arrowscrollbox > .scrollbutton-down:not([disabled]):hover,
.tabbrowser-arrowscrollbox > .scrollbutton-up:not([disabled]):hover,
.tabbrowser-tab:hover {

View File

@ -68,6 +68,7 @@ import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
import org.mozilla.gecko.tiles.TilesRecorder;
import org.mozilla.gecko.toolbar.AutocompleteHandler;
import org.mozilla.gecko.toolbar.BrowserToolbar;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.toolbar.ToolbarProgressView;
import org.mozilla.gecko.util.Clipboard;
import org.mozilla.gecko.util.EventCallback;
@ -245,6 +246,8 @@ public class BrowserApp extends GeckoApp
// The tab to be selected on editing mode exit.
private Integer mTargetTabForEditingMode;
private final TabEditingState mLastTabEditingState = new TabEditingState();
// The animator used to toggle HomePager visibility has a race where if the HomePager is shown
// (starting the animation), the HomePager is hidden, and the HomePager animation completes,
// both the web content and the HomePager will be hidden. This flag is used to prevent the
@ -313,17 +316,61 @@ public class BrowserApp extends GeckoApp
case BOOKMARK_REMOVED:
showBookmarkRemovedToast();
break;
case UNSELECTED:
// We receive UNSELECTED immediately after the SELECTED listeners run
// so we are ensured that the unselectedTabEditingText has not changed.
if (tab.isEditing()) {
// Copy to avoid constructing new objects.
tab.getEditingState().copyFrom(mLastTabEditingState);
}
break;
}
if (NewTabletUI.isEnabled(this) && msg == TabEvents.SELECTED) {
// Note: this is a duplicate call if maybeSwitchToTab
// is called, though the call is idempotent.
mBrowserToolbar.cancelEdit();
updateEditingModeForTab(tab);
}
super.onTabChanged(tab, msg, data);
}
private void updateEditingModeForTab(final Tab selectedTab) {
// (bug 1086983 comment 11) Because the tab may be selected from the gecko thread and we're
// running this code on the UI thread, the selected tab argument may not still refer to the
// selected tab. However, that means this code should be run again and the initial state
// changes will be overridden. As an optimization, we can skip this update, but it may have
// unknown side-effects so we don't.
if (!Tabs.getInstance().isSelectedTab(selectedTab)) {
Log.w(LOGTAG, "updateEditingModeForTab: Given tab is expected to be selected tab");
}
saveTabEditingState(mLastTabEditingState);
if (selectedTab.isEditing()) {
enterEditingMode();
restoreTabEditingState(selectedTab.getEditingState());
} else {
mBrowserToolbar.cancelEdit();
}
}
private void saveTabEditingState(final TabEditingState editingState) {
mBrowserToolbar.saveTabEditingState(editingState);
editingState.setIsBrowserSearchShown(mBrowserSearch.getUserVisibleHint());
}
private void restoreTabEditingState(final TabEditingState editingState) {
mBrowserToolbar.restoreTabEditingState(editingState);
// Since changing the editing text will show/hide browser search, this
// must be called after we restore the editing state in the edit text View.
if (editingState.isBrowserSearchShown()) {
showBrowserSearch();
} else {
hideBrowserSearch();
}
}
private void showBookmarkAddedToast() {
getButtonToast().show(false,
getResources().getString(R.string.bookmark_added),
@ -838,6 +885,11 @@ public class BrowserApp extends GeckoApp
mBrowserToolbar.setOnStartEditingListener(new BrowserToolbar.OnStartEditingListener() {
@Override
public void onStartEditing() {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null) {
selectedTab.setIsEditing(true);
}
// Temporarily disable doorhanger notifications.
mDoorHangerPopup.disable();
}
@ -846,6 +898,11 @@ public class BrowserApp extends GeckoApp
mBrowserToolbar.setOnStopEditingListener(new BrowserToolbar.OnStopEditingListener() {
@Override
public void onStopEditing() {
final Tab selectedTab = Tabs.getInstance().getSelectedTab();
if (selectedTab != null) {
selectedTab.setIsEditing(false);
}
selectTargetTabForEditingMode();
// Since the underlying LayerView is set visible in hideHomePager, we would
@ -1817,6 +1874,11 @@ public class BrowserApp extends GeckoApp
return false;
}
final Tab oldTab = tabs.getSelectedTab();
if (oldTab != null) {
oldTab.setIsEditing(false);
}
// Set the target tab to null so it does not get selected (on editing
// mode exit) in lieu of the tab we are about to select.
mTargetTabForEditingMode = null;

View File

@ -7,15 +7,12 @@ package org.mozilla.gecko;
import android.content.Context;
import android.content.SharedPreferences;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.util.HardwareUtils;
public class NewTabletUI {
// This value should be in sync with preferences_display.xml. On non-release
// builds, the preference UI will be hidden and the (unused) default
// preference UI value will still be 'true'.
private static final boolean DEFAULT = !AppConstants.RELEASE_BUILD;
// This value should be in sync with preferences_display.xml.
private static final boolean DEFAULT = false;
private static Boolean sNewTabletUI;

View File

@ -70,7 +70,7 @@ public final class ReadingListHelper implements GeckoEventListener, NativeEventL
}
case "Reader:ListStatusRequest": {
handleReadingListStatusRequest(message.getString("url"));
handleReadingListStatusRequest(callback, message.getString("url"));
break;
}
}
@ -155,7 +155,7 @@ public final class ReadingListHelper implements GeckoEventListener, NativeEventL
* Gecko (ReaderMode) requests the page ReadingList status, to display
* the proper ReaderMode banner icon (readinglist-add / readinglist-remove).
*/
private void handleReadingListStatusRequest(final String url) {
private void handleReadingListStatusRequest(final EventCallback callback, final String url) {
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
@ -168,11 +168,10 @@ public final class ReadingListHelper implements GeckoEventListener, NativeEventL
json.put("inReadingList", inReadingList);
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error - failed to return inReadingList status", e);
return;
}
GeckoAppShell.sendEventToGecko(
GeckoEvent.createBroadcastEvent("Reader:ListStatusReturn", json.toString()));
// Return the json object to fulfill the promise.
callback.sendSuccess(json.toString());
}
});
}

View File

@ -22,6 +22,7 @@ import org.mozilla.gecko.favicons.LoadFaviconTask;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.favicons.RemoteFavicon;
import org.mozilla.gecko.gfx.Layer;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.ContentResolver;
@ -77,6 +78,9 @@ public class Tab {
private volatile int mRecordingCount;
private String mMostRecentHomePanel;
private boolean mIsEditing;
private final TabEditingState mEditingState = new TabEditingState();
public static final int STATE_DELAYED = 0;
public static final int STATE_LOADING = 1;
public static final int STATE_SUCCESS = 2;
@ -845,4 +849,16 @@ public class Tab {
public boolean isRecording() {
return mRecordingCount > 0;
}
public boolean isEditing() {
return mIsEditing;
}
public void setIsEditing(final boolean isEditing) {
this.mIsEditing = isEditing;
}
public TabEditingState getEditingState() {
return mEditingState;
}
}

View File

@ -27,7 +27,7 @@
<CheckBoxPreference android:key="android.not_a_preference.new_tablet_ui"
android:title="@string/new_tablet_pref"
android:defaultValue="true" />
android:defaultValue="false" />
<PreferenceCategory android:title="@string/pref_category_advanced">

View File

@ -7,6 +7,8 @@ package org.mozilla.gecko.tests;
import java.util.Locale;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.PrefsHelper;
@ -42,6 +44,8 @@ public class testOSLocale extends BaseTest {
private final Object waiter = new Object();
public void fetch() throws InterruptedException {
// Wait for any pending changes to have taken. Bug 1092580.
GeckoAppShell.sendEventToGeckoSync(GeckoEvent.createNoOpEvent());
synchronized (waiter) {
PrefsHelper.getPrefs(TO_FETCH, this);
waiter.wait(MAX_WAIT_MS);
@ -113,7 +117,6 @@ public class testOSLocale extends BaseTest {
// This never changes.
final String SELECTED_LOCALES = "es-es,fr,";
// Expected, from es-ES's intl.properties:
final String EXPECTED = SELECTED_LOCALES +
(isMultiLocaleBuild ? "es,en-us,en" : // Expected, from es-ES's intl.properties.

View File

@ -422,6 +422,18 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout
}
}
public void saveTabEditingState(final TabEditingState editingState) {
urlEditLayout.saveTabEditingState(editingState);
}
public void restoreTabEditingState(final TabEditingState editingState) {
if (!isEditing()) {
throw new IllegalStateException("Expected to be editing");
}
urlEditLayout.restoreTabEditingState(editingState);
}
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
Log.d(LOGTAG, "onTabChanged: " + msg);
@ -916,4 +928,29 @@ public abstract class BrowserToolbar extends ThemedRelativeLayout
setBackgroundResource(R.drawable.url_bar_bg);
editCancel.onLightweightThemeReset();
}
public static class TabEditingState {
// The edited text from the most recent time this tab was unselected.
protected String lastEditingText;
protected int selectionStart;
protected int selectionEnd;
public boolean isBrowserSearchShown;
public void copyFrom(final TabEditingState s2) {
lastEditingText = s2.lastEditingText;
selectionStart = s2.selectionStart;
selectionEnd = s2.selectionEnd;
isBrowserSearchShown = s2.isBrowserSearchShown;
}
public boolean isBrowserSearchShown() {
return isBrowserSearchShown;
}
public void setIsBrowserSearchShown(final boolean isShown) {
isBrowserSearchShown = isShown;
}
}
}

View File

@ -11,6 +11,7 @@ import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnCommitListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnDismissListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.OnFilterListener;
import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
import org.mozilla.gecko.widget.ThemedLinearLayout;
import android.content.Context;
@ -127,4 +128,15 @@ public class ToolbarEditLayout extends ThemedLinearLayout {
String getText() {
return mEditText.getText().toString();
}
protected void saveTabEditingState(final TabEditingState editingState) {
editingState.lastEditingText = mEditText.getNonAutocompleteText();
editingState.selectionStart = mEditText.getSelectionStart();
editingState.selectionEnd = mEditText.getSelectionEnd();
}
protected void restoreTabEditingState(final TabEditingState editingState) {
mEditText.setText(editingState.lastEditingText);
mEditText.setSelection(editingState.selectionStart, editingState.selectionEnd);
}
}

View File

@ -160,6 +160,10 @@ public class ToolbarEditText extends CustomEditText
setCursorVisible(true);
}
protected String getNonAutocompleteText() {
return getNonAutocompleteText(getText());
}
/**
* Get the portion of text that is not marked as autocomplete text.
*

View File

@ -35,7 +35,6 @@ let AboutReader = function(doc, win) {
Services.obs.addObserver(this, "Reader:FaviconReturn", false);
Services.obs.addObserver(this, "Reader:Added", false);
Services.obs.addObserver(this, "Reader:Removed", false);
Services.obs.addObserver(this, "Reader:ListStatusReturn", false);
Services.obs.addObserver(this, "Gesture:DoubleTap", false);
this._article = null;
@ -205,23 +204,6 @@ AboutReader.prototype = {
break;
}
case "Reader:ListStatusReturn": {
let args = JSON.parse(aData);
if (args.url == this._article.url) {
if (this._isReadingListItem != args.inReadingList) {
let isInitialStateChange = (this._isReadingListItem == -1);
this._isReadingListItem = args.inReadingList;
this._updateToggleButton();
// Display the toolbar when all its initial component states are known
if (isInitialStateChange) {
this._setToolbarVisibility(true);
}
}
}
break;
}
case "Gesture:DoubleTap": {
let args = JSON.parse(aData);
let scrollBy;
@ -275,7 +257,6 @@ AboutReader.prototype = {
case "unload":
Services.obs.removeObserver(this, "Reader:Added");
Services.obs.removeObserver(this, "Reader:Removed");
Services.obs.removeObserver(this, "Reader:ListStatusReturn");
Services.obs.removeObserver(this, "Gesture:DoubleTap");
break;
}
@ -303,9 +284,23 @@ AboutReader.prototype = {
},
_requestReadingListStatus: function Reader_requestReadingListStatus() {
Messaging.sendRequest({
Messaging.sendRequestForResult({
type: "Reader:ListStatusRequest",
url: this._article.url
}).then((data) => {
let args = JSON.parse(data);
if (args.url == this._article.url) {
if (this._isReadingListItem != args.inReadingList) {
let isInitialStateChange = (this._isReadingListItem == -1);
this._isReadingListItem = args.inReadingList;
this._updateToggleButton();
// Display the toolbar when all its initial component states are known
if (isInitialStateChange) {
this._setToolbarVisibility(true);
}
}
}
});
},

View File

@ -25,6 +25,7 @@ ContentProcessSingleton.prototype = {
case "app-startup": {
Services.obs.addObserver(this, "console-api-log-event", false);
Services.obs.addObserver(this, "xpcom-shutdown", false);
cpmm.addMessageListener("DevTools:InitDebuggerServer", this);
break;
}
case "console-api-log-event": {
@ -56,9 +57,19 @@ ContentProcessSingleton.prototype = {
case "xpcom-shutdown":
Services.obs.removeObserver(this, "console-api-log-event");
Services.obs.removeObserver(this, "xpcom-shutdown");
cpmm.removeMessageListener("DevTools:InitDebuggerServer", this);
break;
}
},
receiveMessage: function (message) {
// load devtools component on-demand
// Only reply if we are in a real content process
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
let {init} = Cu.import("resource://gre/modules/devtools/content-server.jsm", {});
init(message);
}
},
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentProcessSingleton]);

View File

@ -5191,6 +5191,20 @@
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'listAddons' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_LOCAL_LISTPROCESSES_MS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000",
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'listProcesses' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_REMOTE_LISTPROCESSES_MS": {
"expires_in_version": "never",
"kind": "exponential",
"high": "10000",
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took a 'listProcesses' request to go round trip."
},
"DEVTOOLS_DEBUGGER_RDP_LOCAL_DELETE_MS": {
"expires_in_version": "never",
"kind": "exponential",

View File

@ -6,7 +6,7 @@
/* General utilities used throughout devtools. */
var { Ci, Cu } = require("chrome");
var { Ci, Cu, Cc, components } = require("chrome");
var Services = require("Services");
var promise = require("promise");
var { setTimeout } = require("Timer");
@ -407,3 +407,146 @@ exports.defineLazyModuleGetter = function defineLazyModuleGetter(aObject, aName,
return temp[aSymbol || aName];
});
};
exports.defineLazyGetter(this, "NetUtil", () => {
return Cu.import("resource://gre/modules/NetUtil.jsm", {}).NetUtil;
});
/**
* Performs a request to load the desired URL and returns a promise.
*
* @param aURL String
* The URL we will request.
* @param aOptions Object
* An object with the following optional properties:
* - loadFromCache: if false, will bypass the cache and
* always load fresh from the network (default: true)
* @returns Promise
* A promise of the document at that URL, as a string.
*
* XXX: It may be better to use nsITraceableChannel to get to the sources
* without relying on caching when we can (not for eval, etc.):
* http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
*/
exports.fetch = function fetch(aURL, aOptions={ loadFromCache: true }) {
let deferred = promise.defer();
let scheme;
let url = aURL.split(" -> ").pop();
let charset;
let contentType;
try {
scheme = Services.io.extractScheme(url);
} catch (e) {
// In the xpcshell tests, the script url is the absolute path of the test
// file, which will make a malformed URI error be thrown. Add the file
// scheme prefix ourselves.
url = "file://" + url;
scheme = Services.io.extractScheme(url);
}
dump('scheme: ' + scheme);
switch (scheme) {
case "file":
case "chrome":
case "resource":
try {
NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
if (!components.isSuccessCode(aStatus)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatus
+ " after NetUtil.asyncFetch for url = "
+ url));
return;
}
let source = NetUtil.readInputStreamToString(aStream, aStream.available());
contentType = aRequest.contentType;
deferred.resolve(source);
aStream.close();
});
} catch (ex) {
deferred.reject(ex);
}
break;
default:
let channel;
try {
channel = Services.io.newChannel(url, null, null);
} catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
// On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
// newChannel won't be able to handle it.
url = "file:///" + url;
channel = Services.io.newChannel(url, null, null);
}
let chunks = [];
let streamListener = {
onStartRequest: function(aRequest, aContext, aStatusCode) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStartRequest handler for url = "
+ url));
}
},
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStopRequest handler for url = "
+ url));
return;
}
charset = channel.contentCharset;
contentType = channel.contentType;
deferred.resolve(chunks.join(""));
}
};
channel.loadFlags = aOptions.loadFromCache
? channel.LOAD_FROM_CACHE
: channel.LOAD_BYPASS_CACHE;
try {
channel.asyncOpen(streamListener, null);
} catch(e) {
deferred.reject(new Error("Request failed for '"
+ url
+ "': "
+ e.message));
}
break;
}
return deferred.promise.then(source => {
return {
content: convertToUnicode(source, charset),
contentType: contentType
};
});
}
/**
* Convert a given string, encoded in a given character set, to unicode.
*
* @param string aString
* A string.
* @param string aCharset
* A character set.
*/
function convertToUnicode(aString, aCharset=null) {
// Decoding primitives.
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
try {
converter.charset = aCharset || "UTF-8";
return converter.ConvertToUnicode(aString);
} catch(e) {
return aString;
}
}

View File

@ -594,6 +594,21 @@ DebuggerClient.prototype = {
});
},
/**
* Attach to a process in order to get the form of a ChildProcessActor.
*
* @param string aId
* The ID for the process to attach (returned by `listProcesses`).
*/
attachProcess: function (aId) {
let packet = {
to: 'root',
type: 'attachProcess',
id: aId
}
return this.request(packet);
},
/**
* Release an object actor.
*
@ -1292,6 +1307,15 @@ RootClient.prototype = {
listAddons: DebuggerClient.requester({ type: "listAddons" },
{ telemetry: "LISTADDONS" }),
/**
* List the running processes.
*
* @param function aOnResponse
* Called with the response packet.
*/
listProcesses: DebuggerClient.requester({ type: "listProcesses" },
{ telemetry: "LISTPROCESSES" }),
/**
* Description of protocol's actors and methods.
*

View File

@ -0,0 +1,94 @@
/* 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/. */
"use strict";
const { Cc, Ci, Cu } = require("chrome");
const { ChromeDebuggerActor } = require("devtools/server/actors/script");
const { WebConsoleActor } = require("devtools/server/actors/webconsole");
const makeDebugger = require("devtools/server/actors/utils/make-debugger");
const { ActorPool } = require("devtools/server/main");
const Services = require("Services");
function ChildProcessActor(aConnection) {
this.conn = aConnection;
this._contextPool = new ActorPool(this.conn);
this.conn.addActorPool(this._contextPool);
this._threadActor = null;
// Use a see-everything debugger
this.makeDebugger = makeDebugger.bind(null, {
findDebuggees: dbg => dbg.findAllGlobals(),
shouldAddNewGlobalAsDebuggee: global => true
});
// Scope into which the webconsole executes:
// An empty sandbox with chrome privileges
let systemPrincipal = Cc["@mozilla.org/systemprincipal;1"]
.createInstance(Ci.nsIPrincipal);
let sandbox = Cu.Sandbox(systemPrincipal);
this._consoleScope = sandbox;
}
exports.ChildProcessActor = ChildProcessActor;
ChildProcessActor.prototype = {
actorPrefix: "process",
get isRootActor() true,
get exited() {
return !this._contextPool;
},
get url() {
return undefined;
},
get window() {
return this._consoleScope;
},
form: function() {
if (!this._consoleActor) {
this._consoleActor = new WebConsoleActor(this.conn, this);
this._contextPool.addActor(this._consoleActor);
}
if (!this._threadActor) {
this._threadActor = new ChromeDebuggerActor(this.conn, this);
this._contextPool.addActor(this._threadActor);
}
return {
actor: this.actorID,
name: "Content process",
consoleActor: this._consoleActor.actorID,
chromeDebugger: this._threadActor.actorID,
traits: {
highlightable: false,
networkMonitor: false,
},
};
},
disconnect: function() {
this.conn.removeActorPool(this._contextPool);
this._contextPool = null;
},
preNest: function() {
// TODO: freeze windows
// window mediator doesn't work in child.
// it doesn't throw, but doesn't return any window
},
postNest: function() {
},
};
ChildProcessActor.prototype.requestTypes = {
};

View File

@ -6,7 +6,7 @@
"use strict";
const { Ci, Cu } = require("chrome");
const { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const { ActorPool, appendExtraActors, createExtraActors } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
@ -18,6 +18,10 @@ DevToolsUtils.defineLazyGetter(this, "StyleSheetActor", () => {
return require("devtools/server/actors/stylesheets").StyleSheetActor;
});
DevToolsUtils.defineLazyGetter(this, "ppmm", () => {
return Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster);
});
/* Root actor for the remote debugging protocol. */
/**
@ -358,6 +362,28 @@ RootActor.prototype = {
this._parameters.addonList.onListChanged = null;
},
onListProcesses: function () {
let processes = [];
for (let i = 0; i < ppmm.childCount; i++) {
processes.push({
id: i, // XXX: may not be a perfect id, but process message manager doesn't expose anything...
parent: i == 0, // XXX Weak, but appear to be stable
tabCount: undefined, // TODO: exposes process message manager on frameloaders in order to compute this
});
}
return { processes: processes };
},
onAttachProcess: function (aRequest) {
let mm = ppmm.getChildAt(aRequest.id);
if (!mm) {
return { error: "noProcess",
message: "There is no process with id '" + aRequest.id + "'." };
}
return DebuggerServer.connectToContent(this.conn, mm)
.then(form => ({ form: form }));
},
/* This is not in the spec, but it's used by tests. */
onEcho: function (aRequest) {
/*
@ -431,6 +457,8 @@ RootActor.prototype = {
RootActor.prototype.requestTypes = {
"listTabs": RootActor.prototype.onListTabs,
"listAddons": RootActor.prototype.onListAddons,
"listProcesses": RootActor.prototype.onListProcesses,
"attachProcess": RootActor.prototype.onAttachProcess,
"echo": RootActor.prototype.onEcho,
"protocolDescription": RootActor.prototype.onProtocolDescription
};

View File

@ -11,7 +11,7 @@ const { Cc, Ci, Cu, components, ChromeWorker } = require("chrome");
const { ActorPool, getOffsetColumn } = require("devtools/server/actors/common");
const { DebuggerServer } = require("devtools/server/main");
const DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
const { dbg_assert, dumpn, update } = DevToolsUtils;
const { dbg_assert, dumpn, update, fetch } = DevToolsUtils;
const { SourceMapConsumer, SourceMapGenerator } = require("source-map");
const promise = require("promise");
const PromiseDebugging = require("PromiseDebugging");
@ -22,10 +22,6 @@ const mapURIToAddonID = require("./utils/map-uri-to-addon-id");
const { defer, resolve, reject, all } = require("devtools/toolkit/deprecated-sync-thenables");
const { CssLogic } = require("devtools/styleinspector/css-logic");
DevToolsUtils.defineLazyGetter(this, "NetUtil", () => {
return Cu.import("resource://gre/modules/NetUtil.jsm", {}).NetUtil;
});
let TYPED_ARRAY_CLASSES = ["Uint8Array", "Uint8ClampedArray", "Uint16Array",
"Uint32Array", "Int8Array", "Int16Array", "Int32Array", "Float32Array",
"Float64Array"];
@ -5372,139 +5368,6 @@ function isNotNull(aThing) {
return aThing !== null;
}
/**
* Performs a request to load the desired URL and returns a promise.
*
* @param aURL String
* The URL we will request.
* @returns Promise
* A promise of the document at that URL, as a string.
*
* XXX: It may be better to use nsITraceableChannel to get to the sources
* without relying on caching when we can (not for eval, etc.):
* http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
*/
function fetch(aURL, aOptions={ loadFromCache: true }) {
let deferred = defer();
let scheme;
let url = aURL.split(" -> ").pop();
let charset;
let contentType;
try {
scheme = Services.io.extractScheme(url);
} catch (e) {
// In the xpcshell tests, the script url is the absolute path of the test
// file, which will make a malformed URI error be thrown. Add the file
// scheme prefix ourselves.
url = "file://" + url;
scheme = Services.io.extractScheme(url);
}
switch (scheme) {
case "file":
case "chrome":
case "resource":
try {
NetUtil.asyncFetch(url, function onFetch(aStream, aStatus, aRequest) {
if (!components.isSuccessCode(aStatus)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatus
+ " after NetUtil.asyncFetch for url = "
+ url));
return;
}
let source = NetUtil.readInputStreamToString(aStream, aStream.available());
contentType = aRequest.contentType;
deferred.resolve(source);
aStream.close();
});
} catch (ex) {
deferred.reject(ex);
}
break;
default:
let channel;
try {
channel = Services.io.newChannel(url, null, null);
} catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
// On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
// newChannel won't be able to handle it.
url = "file:///" + url;
channel = Services.io.newChannel(url, null, null);
}
let chunks = [];
let streamListener = {
onStartRequest: function(aRequest, aContext, aStatusCode) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStartRequest handler for url = "
+ url));
}
},
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStopRequest handler for url = "
+ url));
return;
}
charset = channel.contentCharset;
contentType = channel.contentType;
deferred.resolve(chunks.join(""));
}
};
channel.loadFlags = aOptions.loadFromCache
? channel.LOAD_FROM_CACHE
: channel.LOAD_BYPASS_CACHE;
try {
channel.asyncOpen(streamListener, null);
} catch(e) {
deferred.reject(new Error("Request failed for '"
+ url
+ "': "
+ e.message));
}
break;
}
return deferred.promise.then(source => {
return {
content: convertToUnicode(source, charset),
contentType: contentType
};
});
}
/**
* Convert a given string, encoded in a given character set, to unicode.
*
* @param string aString
* A string.
* @param string aCharset
* A character set.
*/
function convertToUnicode(aString, aCharset=null) {
// Decoding primitives.
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
try {
converter.charset = aCharset || "UTF-8";
return converter.ConvertToUnicode(aString);
} catch(e) {
return aString;
}
}
/**
* Report the given error in the error console and to stdout.
*

View File

@ -559,13 +559,15 @@ WebConsoleActor.prototype =
startedListeners.push(listener);
break;
case "FileActivity":
if (!this.consoleProgressListener) {
this.consoleProgressListener =
new ConsoleProgressListener(this.window, this);
if (this.window instanceof Ci.nsIDOMWindow) {
if (!this.consoleProgressListener) {
this.consoleProgressListener =
new ConsoleProgressListener(this.window, this);
}
this.consoleProgressListener.startMonitor(this.consoleProgressListener.
MONITOR_FILE_ACTIVITY);
startedListeners.push(listener);
}
this.consoleProgressListener.startMonitor(this.consoleProgressListener.
MONITOR_FILE_ACTIVITY);
startedListeners.push(listener);
break;
case "ReflowActivity":
if (!this.consoleReflowListener) {

View 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/. */
"use strict";
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cu = Components.utils;
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { DevToolsLoader } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
this.EXPORTED_SYMBOLS = ["init"];
let started = false;
function init(msg) {
if (started) {
return;
}
started = true;
// Init a custom, invisible DebuggerServer, in order to not pollute
// the debugger with all devtools modules, nor break the debugger itself with using it
// in the same process.
let devtools = new DevToolsLoader();
devtools.invisibleToDebugger = true;
devtools.main("devtools/server/main");
let { DebuggerServer, ActorPool } = devtools;
if (!DebuggerServer.initialized) {
DebuggerServer.init();
}
// In case of apps being loaded in parent process, DebuggerServer is already
// initialized, but child specific actors are not registered.
// Otherwise, for child process, we need to load actors the first
// time we load child.js
DebuggerServer.addChildActors();
let mm = msg.target;
mm.QueryInterface(Ci.nsISyncMessageSender);
let prefix = msg.data.prefix;
// Connect both parent/child processes debugger servers RDP via message managers
let conn = DebuggerServer.connectToParent(prefix, mm);
let { ChildProcessActor } = devtools.require("devtools/server/actors/child-process");
let actor = new ChildProcessActor(conn);
let actorPool = new ActorPool(conn);
actorPool.addActor(actor);
conn.addActorPool(actorPool);
let response = {actor: actor.form()};
mm.sendAsyncMessage("debug:content-process-actor", response);
mm.addMessageListener("debug:content-process-destroy", function onDestroy() {
mm.removeMessageListener("debug:content-process-destroy", onDestroy);
DebuggerServer.destroy();
started = false;
});
}

View File

@ -704,6 +704,69 @@ var DebuggerServer = {
return this._onConnection(transport, aPrefix, true);
},
connectToContent: function (aConnection, aMm) {
let deferred = defer();
let prefix = aConnection.allocID("content-process");
let actor, childTransport;
aMm.addMessageListener("debug:content-process-actor", function listener(msg) {
// Arbitrarily choose the first content process to reply
// XXX: This code needs to be updated if we use more than one content process
aMm.removeMessageListener("debug:content-process-actor", listener);
// Pipe Debugger message from/to parent/child via the message manager
childTransport = new ChildDebuggerTransport(aMm, prefix);
childTransport.hooks = {
onPacket: aConnection.send.bind(aConnection),
onClosed: function () {}
};
childTransport.ready();
aConnection.setForwarding(prefix, childTransport);
dumpn("establishing forwarding for process with prefix " + prefix);
actor = msg.json.actor;
deferred.resolve(actor);
});
aMm.sendAsyncMessage("DevTools:InitDebuggerServer", {
prefix: prefix
});
function onDisconnect() {
Services.obs.removeObserver(onMessageManagerDisconnect, "message-manager-disconnect");
events.off(aConnection, "closed", onDisconnect);
if (childTransport) {
// If we have a child transport, the actor has already
// been created. We need to stop using this message manager.
childTransport.close();
childTransport = null;
aConnection.cancelForwarding(prefix);
// ... and notify the child process to clean the tab actors.
try {
aMm.sendAsyncMessage("debug:content-process-destroy");
} catch(e) {}
}
}
let onMessageManagerDisconnect = DevToolsUtils.makeInfallible(function (subject, topic, data) {
if (subject == aMm) {
onDisconnect();
aConnection.send({ from: actor.actor, type: "tabDetached" });
}
}).bind(this);
Services.obs.addObserver(onMessageManagerDisconnect,
"message-manager-disconnect", false);
events.on(aConnection, "closed", onDisconnect);
return deferred.promise;
},
/**
* Connect to a child process.
*

View File

@ -21,6 +21,7 @@ SOURCES += [
FINAL_LIBRARY = 'xul'
EXTRA_JS_MODULES.devtools += [
'content-server.jsm',
'dbg-server.jsm',
]
@ -34,6 +35,7 @@ EXTRA_JS_MODULES.devtools.server += [
EXTRA_JS_MODULES.devtools.server.actors += [
'actors/call-watcher.js',
'actors/canvas.js',
'actors/child-process.js',
'actors/childtab.js',
'actors/common.js',
'actors/csscoverage.js',

View File

@ -73,3 +73,5 @@ skip-if = buildapp == 'mulet'
[test_preference.html]
[test_connectToChild.html]
skip-if = buildapp == 'mulet'
[test_attachProcess.html]
skip-if = buildapp == 'mulet'

View File

@ -0,0 +1,96 @@
<SDOCTYPv HTM.>
<html>
<!--
Bug 1060093 - Test DebuggerServer.attachProcess
-->
<head>
<meta charset="utf-8">
<title>Mozilla Bug</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script type="application/javascript;version=1.8">
let Cu = Components.utils;
let Cc = Components.classes;
let Ci = Components.interfaces;
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
window.onload = function() {
SimpleTest.waitForExplicitFinish();
SpecialPowers.pushPrefEnv({
"set": [
// Always log packets when running tests.
["devtools.debugger.log", true],
["dom.mozBrowserFramesEnabled", true]
]
}, runTests);
}
function runTests() {
// Create a remote iframe with a message manager
let iframe = document.createElement("iframe");
iframe.mozbrowser = true;
iframe.setAttribute("remote", "true");
iframe.setAttribute("src", "data:text/html,foo");
document.body.appendChild(iframe);
let mm = iframe.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader.messageManager;
// Instantiate a minimal server
if (!DebuggerServer.initialized) {
DebuggerServer.init(function () { return true; });
}
if (!DebuggerServer.createRootActor) {
DebuggerServer.addBrowserActors();
}
function firstClient() {
// Fake a first connection to the content process
let transport = DebuggerServer.connectPipe();
let client = new DebuggerClient(transport);
client.connect(() => {
client.mainRoot.listProcesses(response => {
ok(response.processes.length >= 2, "Got at least the parent process and one child");
// Connect to the first content processe available
let content = response.processes.filter(p => (!p.parent))[0];
client.attachProcess(content.id).then(response => {
let actor = response.form;
ok(actor.consoleActor, "Got the console actor");
ok(actor.chromeDebugger, "Got the thread actor");
// Ensure sending at least one request to an actor...
client.request({
to: actor.consoleActor,
type: "evaluateJS",
text: "var a = 42; a"
}, function (response) {
ok(response.result, 42, "console.eval worked");
client.close(cleanup);
});
});
});
});
}
function cleanup() {
DebuggerServer.destroy();
iframe.remove();
SimpleTest.finish()
}
firstClient();
}
</script>
</pre>
</body>
</html>

View File

@ -4622,7 +4622,7 @@ mozilla::BrowserTabsRemoteAutostart()
!Preferences::GetBool("layers.acceleration.force-enabled", false);
#if defined(XP_MACOSX)
accelDisabled = !nsCocoaFeatures::AccelerateByDefault();
accelDisabled = accelDisabled || !nsCocoaFeatures::AccelerateByDefault();
#endif
// Check for blocked drivers