Merge fx-team to m-c a=merge

This commit is contained in:
Wes Kocher 2015-04-21 15:15:48 -07:00
commit 103ab1ed0e
88 changed files with 1864 additions and 1100 deletions

View File

@ -326,7 +326,9 @@
@RESPATH@/components/toolkit_finalizationwitness.xpt
@RESPATH@/components/toolkit_formautofill.xpt
@RESPATH@/components/toolkit_osfile.xpt
#ifdef NIGHTLY_BUILD
@RESPATH@/components/toolkit_perfmonitoring.xpt
#endif
@RESPATH@/components/toolkit_xulstore.xpt
@RESPATH@/components/toolkitprofile.xpt
#ifdef MOZ_ENABLE_XREMOTE

View File

@ -1180,8 +1180,8 @@ nsContextMenu.prototype = {
// Helper function to wait for appropriate MIME-type headers and
// then prompt the user with a file picker
saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc,
linkDownload) {
saveHelper: function(linkURL, linkText, dialogTitle, bypassCache, doc, docURI,
windowID, linkDownload) {
// canonical def in nsURILoader.h
const NS_ERROR_SAVE_LINK_AS_TIMEOUT = 0x805d0020;
@ -1216,7 +1216,10 @@ nsContextMenu.prototype = {
const promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
getService(Ci.nsIPromptService);
promptSvc.alert(doc.defaultView, title, msg);
const wm = Cc["@mozilla.org/appshell/window-mediator;1"].
getService(Ci.nsIWindowMediator);
let window = wm.getOuterWindowWithId(windowID);
promptSvc.alert(window, title, msg);
} catch (ex) {}
return;
}
@ -1236,8 +1239,8 @@ nsContextMenu.prototype = {
if (aStatusCode == NS_ERROR_SAVE_LINK_AS_TIMEOUT) {
// do it the old fashioned way, which will pick the best filename
// it can without waiting.
saveURL(linkURL, linkText, dialogTitle, bypassCache, false,
BrowserUtils.makeURIFromCPOW(doc.documentURIObject), doc);
saveURL(linkURL, linkText, dialogTitle, bypassCache, false, docURI,
doc);
}
if (this.extListener)
this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
@ -1306,7 +1309,7 @@ nsContextMenu.prototype = {
channel.loadFlags |= flags;
if (channel instanceof Ci.nsIHttpChannel) {
channel.referrer = BrowserUtils.makeURIFromCPOW(doc.documentURIObject);
channel.referrer = docURI;
if (channel instanceof Ci.nsIHttpChannelInternal)
channel.forceAllowThirdPartyCookie = true;
}
@ -1326,6 +1329,8 @@ nsContextMenu.prototype = {
saveLink: function() {
urlSecurityCheck(this.linkURL, this.principal);
this.saveHelper(this.linkURL, this.linkText, null, true, this.ownerDoc,
gContextMenuContentData.documentURIObject,
gContextMenuContentData.frameOuterWindowID,
this.linkDownload);
},
@ -1338,23 +1343,23 @@ nsContextMenu.prototype = {
// Save URL of the clicked upon image, video, or audio.
saveMedia: function() {
var doc = this.target.ownerDocument;
let referrerURI = gContextMenuContentData.documentURIObject;
if (this.onCanvas) {
// Bypass cache, since it's a data: URL.
saveImageURL(this.target.toDataURL(), "canvas.png", "SaveImageTitle",
true, false, gContextMenuContentData.documentURIObject,
doc);
true, false, referrerURI, doc);
}
else if (this.onImage) {
urlSecurityCheck(this.mediaURL, this.principal);
let uri = gContextMenuContentData.documentURIObject;
saveImageURL(this.mediaURL, null, "SaveImageTitle", false,
false, uri, doc, gContextMenuContentData.contentType,
false, referrerURI, doc, gContextMenuContentData.contentType,
gContextMenuContentData.contentDisposition);
}
else if (this.onVideo || this.onAudio) {
urlSecurityCheck(this.mediaURL, this.principal);
var dialogTitle = this.onVideo ? "SaveVideoTitle" : "SaveAudioTitle";
this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, "");
this.saveHelper(this.mediaURL, null, dialogTitle, false, doc, referrerURI,
gContextMenuContentData.frameOuterWindowID, "");
}
},
@ -1662,7 +1667,7 @@ nsContextMenu.prototype = {
},
printFrame: function CM_printFrame() {
PrintUtils.print(this.target.ownerDocument.defaultView);
PrintUtils.print(this.target.ownerDocument.defaultView, this.browser);
},
switchPageDirection: function CM_switchPageDirection() {

View File

@ -1,8 +1,5 @@
let AddonManager = Cu.import("resource://gre/modules/AddonManager.jsm", {}).AddonManager;
let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
let AddonWatcher = Cu.import("resource://gre/modules/AddonWatcher.jsm", {}).AddonWatcher;
const ADDON_TYPE_SERVICE = "service";
const ID_SUFFIX = "@services.mozilla.org";

View File

@ -49,14 +49,13 @@ loop.conversation = (function(mozL10n) {
case "outgoing": {
return (React.createElement(CallControllerView, {
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop}
));
mozLoop: this.props.mozLoop}));
}
case "room": {
return (React.createElement(DesktopRoomConversationView, {
dispatcher: this.props.dispatcher,
roomStore: this.props.roomStore}
));
mozLoop: this.props.mozLoop,
roomStore: this.props.roomStore}));
}
case "failed": {
return React.createElement(GenericFailureView, {cancelCall: this.closeWindow});

View File

@ -49,14 +49,13 @@ loop.conversation = (function(mozL10n) {
case "outgoing": {
return (<CallControllerView
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
/>);
mozLoop={this.props.mozLoop} />);
}
case "room": {
return (<DesktopRoomConversationView
dispatcher={this.props.dispatcher}
roomStore={this.props.roomStore}
/>);
mozLoop={this.props.mozLoop}
roomStore={this.props.roomStore} />);
}
case "failed": {
return <GenericFailureView cancelCall={this.closeWindow} />;

View File

@ -228,7 +228,6 @@ loop.conversationViews = (function(mozL10n) {
},
render: function() {
/* jshint ignore:start */
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
@ -276,7 +275,6 @@ loop.conversationViews = (function(mozL10n) {
)
)
);
/* jshint ignore:end */
}
});
@ -293,7 +291,6 @@ loop.conversationViews = (function(mozL10n) {
render: function() {
var mode = this.props.mode;
return (
/* jshint ignore:start */
React.createElement("div", {className: "btn-chevron-menu-group"},
React.createElement("div", {className: "btn-group"},
React.createElement("button", {className: "btn btn-accept",
@ -310,7 +307,6 @@ loop.conversationViews = (function(mozL10n) {
)
)
)
/* jshint ignore:end */
);
}
});

View File

@ -228,7 +228,6 @@ loop.conversationViews = (function(mozL10n) {
},
render: function() {
/* jshint ignore:start */
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
@ -276,7 +275,6 @@ loop.conversationViews = (function(mozL10n) {
</div>
</div>
);
/* jshint ignore:end */
}
});
@ -293,7 +291,6 @@ loop.conversationViews = (function(mozL10n) {
render: function() {
var mode = this.props.mode;
return (
/* jshint ignore:start */
<div className="btn-chevron-menu-group">
<div className="btn-group">
<button className="btn btn-accept"
@ -310,7 +307,6 @@ loop.conversationViews = (function(mozL10n) {
</div>
</div>
</div>
/* jshint ignore:end */
);
}
});

View File

@ -31,6 +31,8 @@ loop.roomViews = (function(mozL10n) {
componentWillMount: function() {
this.listenTo(this.props.roomStore, "change:activeRoom",
this._onActiveRoomStateChanged);
this.listenTo(this.props.roomStore, "change:error",
this._onRoomError);
},
componentWillUnmount: function() {
@ -46,6 +48,15 @@ loop.roomViews = (function(mozL10n) {
}
},
_onRoomError: function() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({error: this.props.roomStore.getStoreState("error")});
}
},
getInitialState: function() {
var storeState = this.props.roomStore.getStoreState("activeRoom");
return _.extend({
@ -56,11 +67,12 @@ loop.roomViews = (function(mozL10n) {
};
var SocialShareDropdown = React.createClass({displayName: "SocialShareDropdown",
mixins: [ActiveRoomStoreMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
show: React.PropTypes.bool.isRequired
roomUrl: React.PropTypes.string,
show: React.PropTypes.bool.isRequired,
socialShareButtonAvailable: React.PropTypes.bool,
socialShareProviders: React.PropTypes.array
},
handleToolbarAddButtonClick: function(event) {
@ -79,20 +91,20 @@ loop.roomViews = (function(mozL10n) {
event.preventDefault();
var origin = event.currentTarget.dataset.provider;
var provider = this.state.socialShareProviders.filter(function(provider) {
var provider = this.props.socialShareProviders.filter(function(provider) {
return provider.origin == origin;
})[0];
this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
provider: provider,
roomUrl: this.state.roomUrl,
roomUrl: this.props.roomUrl,
previews: []
}));
},
render: function() {
// Don't render a thing when no data has been fetched yet.
if (!this.state.socialShareProviders) {
if (!this.props.socialShareProviders) {
return null;
}
@ -100,13 +112,13 @@ loop.roomViews = (function(mozL10n) {
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"share-button-unavailable": !this.state.socialShareButtonAvailable,
"share-button-unavailable": !this.props.socialShareButtonAvailable,
"hide": !this.props.show
});
// When the button is not yet available, we offer to put it in the navbar
// for the user.
if (!this.state.socialShareButtonAvailable) {
if (!this.props.socialShareButtonAvailable) {
return (
React.createElement("div", {className: shareDropdown},
React.createElement("div", {className: "share-panel-header"},
@ -134,9 +146,9 @@ loop.roomViews = (function(mozL10n) {
React.createElement("i", {className: "icon icon-add-share-service"}),
React.createElement("span", null, mozL10n.get("share_add_service_button"))
),
this.state.socialShareProviders.length ? React.createElement("li", {className: "dropdown-menu-separator"}) : null,
this.props.socialShareProviders.length ? React.createElement("li", {className: "dropdown-menu-separator"}) : null,
this.state.socialShareProviders.map(function(provider, idx) {
this.props.socialShareProviders.map(function(provider, idx) {
return (
React.createElement("li", {className: "dropdown-menu-item",
key: "provider-" + idx,
@ -157,30 +169,24 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin,
sharedMixins.DropdownMenuMixin],
mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
showContext: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return {
copiedUrl: false,
newRoomName: "",
error: null,
newRoomName: ""
};
},
componentWillMount: function() {
this.listenTo(this.props.roomStore, "change:error",
this.onRoomError);
},
componentWillUnmount: function() {
this.stopListening(this.props.roomStore);
},
handleTextareaKeyDown: function(event) {
// Submit the form as soon as the user press Enter in that field
// Note: We're using a textarea instead of a simple text input to display
@ -195,7 +201,7 @@ loop.roomViews = (function(mozL10n) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.RenameRoom({
roomToken: this.state.roomToken,
roomToken: this.props.roomData.roomToken,
newRoomName: this.state.newRoomName
}));
},
@ -204,14 +210,14 @@ loop.roomViews = (function(mozL10n) {
event.preventDefault();
this.props.dispatcher.dispatch(
new sharedActions.EmailRoomUrl({roomUrl: this.state.roomUrl}));
new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
},
handleCopyButtonClick: function(event) {
event.preventDefault();
this.props.dispatcher.dispatch(
new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
new sharedActions.CopyRoomUrl({roomUrl: this.props.roomData.roomUrl}));
this.setState({copiedUrl: true});
},
@ -222,51 +228,100 @@ loop.roomViews = (function(mozL10n) {
this.toggleDropdownMenu();
},
onRoomError: function() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({error: this.props.roomStore.getStoreState("error")});
}
},
render: function() {
if (!this.props.show) {
return null;
}
var cx = React.addons.classSet;
return (
React.createElement("div", {className: "room-invitation-overlay"},
React.createElement("p", {className: cx({"error": !!this.state.error,
"error-display-area": true})},
mozL10n.get("rooms_name_change_failed_label")
),
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("textarea", {rows: "2", type: "text", className: "input-room-name",
valueLink: this.linkState("newRoomName"),
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("rooms_name_this_room_label")})
),
React.createElement("p", null, mozL10n.get("invite_header_text")),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: "btn btn-info btn-email",
onClick: this.handleEmailButtonClick},
mozL10n.get("email_link_button")
React.createElement("div", {className: "room-invitation-content"},
React.createElement("p", {className: cx({"error": !!this.props.error,
"error-display-area": true})},
mozL10n.get("rooms_name_change_failed_label")
),
React.createElement("button", {className: "btn btn-info btn-copy",
onClick: this.handleCopyButtonClick},
this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")
React.createElement("form", {onSubmit: this.handleFormSubmit},
React.createElement("textarea", {rows: "2", type: "text", className: "input-room-name",
valueLink: this.linkState("newRoomName"),
onBlur: this.handleFormSubmit,
onKeyDown: this.handleTextareaKeyDown,
placeholder: mozL10n.get("rooms_name_this_room_label")})
),
React.createElement("button", {className: "btn btn-info btn-share",
ref: "anchor",
onClick: this.handleShareButtonClick},
mozL10n.get("share_button3")
)
React.createElement("p", null, mozL10n.get("invite_header_text")),
React.createElement("div", {className: "btn-group call-action-group"},
React.createElement("button", {className: "btn btn-info btn-email",
onClick: this.handleEmailButtonClick},
mozL10n.get("email_link_button")
),
React.createElement("button", {className: "btn btn-info btn-copy",
onClick: this.handleCopyButtonClick},
this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")
),
React.createElement("button", {className: "btn btn-info btn-share",
ref: "anchor",
onClick: this.handleShareButtonClick},
mozL10n.get("share_button3")
)
),
React.createElement(SocialShareDropdown, {dispatcher: this.props.dispatcher,
roomUrl: this.props.roomData.roomUrl,
show: this.state.showMenu,
ref: "menu"})
),
React.createElement(SocialShareDropdown, {dispatcher: this.props.dispatcher,
roomStore: this.props.roomStore,
show: this.state.showMenu,
ref: "menu"})
React.createElement(DesktopRoomContextView, {
roomData: this.props.roomData,
show: this.props.showContext})
)
);
}
});
var DesktopRoomContextView = React.createClass({displayName: "DesktopRoomContextView",
propTypes: {
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired
},
componentWillReceiveProps: function(nextProps) {
// When the 'show' prop is changed from outside this component, we do need
// to update the state.
if (("show" in nextProps) && nextProps.show !== this.props.show) {
this.setState({ show: nextProps.show });
}
},
getInitialState: function() {
return { show: this.props.show };
},
handleCloseClick: function() {
this.setState({ show: false });
},
render: function() {
if (!this.state.show)
return null;
var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
var thumbnail = URL && URL.thumbnail || "";
var URLDescription = URL && URL.description || "";
var location = URL && URL.location || "";
return (
React.createElement("div", {className: "room-context"},
React.createElement("img", {className: "room-context-thumbnail", src: thumbnail}),
React.createElement("div", {className: "room-context-content"},
React.createElement("div", {className: "room-context-label"}, mozL10n.get("context_inroom_label")),
React.createElement("div", {className: "room-context-description"}, URLDescription),
React.createElement("a", {className: "room-context-url", href: location, target: "_blank"}, location),
this.props.roomData.roomDescription ?
React.createElement("div", {className: "room-context-comment"}, this.props.roomData.roomDescription) :
null,
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick})
)
)
);
}
@ -285,18 +340,8 @@ loop.roomViews = (function(mozL10n) {
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_renderInvitationOverlay: function() {
if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
return (
React.createElement(DesktopRoomInvitationView, {
roomStore: this.props.roomStore,
dispatcher: this.props.dispatcher})
);
}
return null;
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired
},
componentWillUpdate: function(nextProps, nextState) {
@ -341,6 +386,17 @@ loop.roomViews = (function(mozL10n) {
}));
},
_shouldRenderInvitationOverlay: function() {
return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
},
_shouldRenderContextView: function() {
return !!(
this.props.mozLoop.getLoopPref("contextInConverations.enabled") &&
(this.state.roomContextUrls || this.state.roomDescription)
);
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -358,6 +414,10 @@ loop.roomViews = (function(mozL10n) {
visible: true
};
var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
var shouldRenderContextView = this._shouldRenderContextView();
var roomData = this.props.roomStore.getStoreState("activeRoom");
switch(this.state.roomState) {
case ROOM_STATES.FAILED:
case ROOM_STATES.FULL: {
@ -378,7 +438,12 @@ loop.roomViews = (function(mozL10n) {
default: {
return (
React.createElement("div", {className: "room-conversation-wrapper"},
this._renderInvitationOverlay(),
React.createElement(DesktopRoomInvitationView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
roomData: roomData,
show: shouldRenderInvitationOverlay,
showContext: shouldRenderContextView}),
React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation room-conversation"},
React.createElement("div", {className: "media nested"},
@ -396,7 +461,10 @@ loop.roomViews = (function(mozL10n) {
hangup: this.leaveRoom,
screenShare: screenShareData})
)
)
),
React.createElement(DesktopRoomContextView, {
roomData: roomData,
show: !shouldRenderInvitationOverlay && shouldRenderContextView})
)
);
}
@ -407,6 +475,7 @@ loop.roomViews = (function(mozL10n) {
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
SocialShareDropdown: SocialShareDropdown,
DesktopRoomContextView: DesktopRoomContextView,
DesktopRoomConversationView: DesktopRoomConversationView,
DesktopRoomInvitationView: DesktopRoomInvitationView
};

View File

@ -31,6 +31,8 @@ loop.roomViews = (function(mozL10n) {
componentWillMount: function() {
this.listenTo(this.props.roomStore, "change:activeRoom",
this._onActiveRoomStateChanged);
this.listenTo(this.props.roomStore, "change:error",
this._onRoomError);
},
componentWillUnmount: function() {
@ -46,6 +48,15 @@ loop.roomViews = (function(mozL10n) {
}
},
_onRoomError: function() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({error: this.props.roomStore.getStoreState("error")});
}
},
getInitialState: function() {
var storeState = this.props.roomStore.getStoreState("activeRoom");
return _.extend({
@ -56,11 +67,12 @@ loop.roomViews = (function(mozL10n) {
};
var SocialShareDropdown = React.createClass({
mixins: [ActiveRoomStoreMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
show: React.PropTypes.bool.isRequired
roomUrl: React.PropTypes.string,
show: React.PropTypes.bool.isRequired,
socialShareButtonAvailable: React.PropTypes.bool,
socialShareProviders: React.PropTypes.array
},
handleToolbarAddButtonClick: function(event) {
@ -79,20 +91,20 @@ loop.roomViews = (function(mozL10n) {
event.preventDefault();
var origin = event.currentTarget.dataset.provider;
var provider = this.state.socialShareProviders.filter(function(provider) {
var provider = this.props.socialShareProviders.filter(function(provider) {
return provider.origin == origin;
})[0];
this.props.dispatcher.dispatch(new sharedActions.ShareRoomUrl({
provider: provider,
roomUrl: this.state.roomUrl,
roomUrl: this.props.roomUrl,
previews: []
}));
},
render: function() {
// Don't render a thing when no data has been fetched yet.
if (!this.state.socialShareProviders) {
if (!this.props.socialShareProviders) {
return null;
}
@ -100,13 +112,13 @@ loop.roomViews = (function(mozL10n) {
var shareDropdown = cx({
"share-service-dropdown": true,
"dropdown-menu": true,
"share-button-unavailable": !this.state.socialShareButtonAvailable,
"share-button-unavailable": !this.props.socialShareButtonAvailable,
"hide": !this.props.show
});
// When the button is not yet available, we offer to put it in the navbar
// for the user.
if (!this.state.socialShareButtonAvailable) {
if (!this.props.socialShareButtonAvailable) {
return (
<div className={shareDropdown}>
<div className="share-panel-header">
@ -134,9 +146,9 @@ loop.roomViews = (function(mozL10n) {
<i className="icon icon-add-share-service"></i>
<span>{mozL10n.get("share_add_service_button")}</span>
</li>
{this.state.socialShareProviders.length ? <li className="dropdown-menu-separator"/> : null}
{this.props.socialShareProviders.length ? <li className="dropdown-menu-separator"/> : null}
{
this.state.socialShareProviders.map(function(provider, idx) {
this.props.socialShareProviders.map(function(provider, idx) {
return (
<li className="dropdown-menu-item"
key={"provider-" + idx}
@ -157,30 +169,24 @@ loop.roomViews = (function(mozL10n) {
* Desktop room invitation view (overlay).
*/
var DesktopRoomInvitationView = React.createClass({
mixins: [ActiveRoomStoreMixin, React.addons.LinkedStateMixin,
sharedMixins.DropdownMenuMixin],
mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
error: React.PropTypes.object,
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired,
showContext: React.PropTypes.bool.isRequired
},
getInitialState: function() {
return {
copiedUrl: false,
newRoomName: "",
error: null,
newRoomName: ""
};
},
componentWillMount: function() {
this.listenTo(this.props.roomStore, "change:error",
this.onRoomError);
},
componentWillUnmount: function() {
this.stopListening(this.props.roomStore);
},
handleTextareaKeyDown: function(event) {
// Submit the form as soon as the user press Enter in that field
// Note: We're using a textarea instead of a simple text input to display
@ -195,7 +201,7 @@ loop.roomViews = (function(mozL10n) {
event.preventDefault();
this.props.dispatcher.dispatch(new sharedActions.RenameRoom({
roomToken: this.state.roomToken,
roomToken: this.props.roomData.roomToken,
newRoomName: this.state.newRoomName
}));
},
@ -204,14 +210,14 @@ loop.roomViews = (function(mozL10n) {
event.preventDefault();
this.props.dispatcher.dispatch(
new sharedActions.EmailRoomUrl({roomUrl: this.state.roomUrl}));
new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
},
handleCopyButtonClick: function(event) {
event.preventDefault();
this.props.dispatcher.dispatch(
new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
new sharedActions.CopyRoomUrl({roomUrl: this.props.roomData.roomUrl}));
this.setState({copiedUrl: true});
},
@ -222,51 +228,100 @@ loop.roomViews = (function(mozL10n) {
this.toggleDropdownMenu();
},
onRoomError: function() {
// Only update the state if we're mounted, to avoid the problem where
// stopListening doesn't nuke the active listeners during a event
// processing.
if (this.isMounted()) {
this.setState({error: this.props.roomStore.getStoreState("error")});
}
},
render: function() {
if (!this.props.show) {
return null;
}
var cx = React.addons.classSet;
return (
<div className="room-invitation-overlay">
<p className={cx({"error": !!this.state.error,
"error-display-area": true})}>
{mozL10n.get("rooms_name_change_failed_label")}
</p>
<form onSubmit={this.handleFormSubmit}>
<textarea rows="2" type="text" className="input-room-name"
valueLink={this.linkState("newRoomName")}
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("rooms_name_this_room_label")} />
</form>
<p>{mozL10n.get("invite_header_text")}</p>
<div className="btn-group call-action-group">
<button className="btn btn-info btn-email"
onClick={this.handleEmailButtonClick}>
{mozL10n.get("email_link_button")}
</button>
<button className="btn btn-info btn-copy"
onClick={this.handleCopyButtonClick}>
{this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")}
</button>
<button className="btn btn-info btn-share"
ref="anchor"
onClick={this.handleShareButtonClick}>
{mozL10n.get("share_button3")}
</button>
<div className="room-invitation-content">
<p className={cx({"error": !!this.props.error,
"error-display-area": true})}>
{mozL10n.get("rooms_name_change_failed_label")}
</p>
<form onSubmit={this.handleFormSubmit}>
<textarea rows="2" type="text" className="input-room-name"
valueLink={this.linkState("newRoomName")}
onBlur={this.handleFormSubmit}
onKeyDown={this.handleTextareaKeyDown}
placeholder={mozL10n.get("rooms_name_this_room_label")} />
</form>
<p>{mozL10n.get("invite_header_text")}</p>
<div className="btn-group call-action-group">
<button className="btn btn-info btn-email"
onClick={this.handleEmailButtonClick}>
{mozL10n.get("email_link_button")}
</button>
<button className="btn btn-info btn-copy"
onClick={this.handleCopyButtonClick}>
{this.state.copiedUrl ? mozL10n.get("copied_url_button") :
mozL10n.get("copy_url_button2")}
</button>
<button className="btn btn-info btn-share"
ref="anchor"
onClick={this.handleShareButtonClick}>
{mozL10n.get("share_button3")}
</button>
</div>
<SocialShareDropdown dispatcher={this.props.dispatcher}
roomUrl={this.props.roomData.roomUrl}
show={this.state.showMenu}
ref="menu"/>
</div>
<DesktopRoomContextView
roomData={this.props.roomData}
show={this.props.showContext} />
</div>
);
}
});
var DesktopRoomContextView = React.createClass({
propTypes: {
// This data is supplied by the activeRoomStore.
roomData: React.PropTypes.object.isRequired,
show: React.PropTypes.bool.isRequired
},
componentWillReceiveProps: function(nextProps) {
// When the 'show' prop is changed from outside this component, we do need
// to update the state.
if (("show" in nextProps) && nextProps.show !== this.props.show) {
this.setState({ show: nextProps.show });
}
},
getInitialState: function() {
return { show: this.props.show };
},
handleCloseClick: function() {
this.setState({ show: false });
},
render: function() {
if (!this.state.show)
return null;
var URL = this.props.roomData.roomContextUrls && this.props.roomData.roomContextUrls[0];
var thumbnail = URL && URL.thumbnail || "";
var URLDescription = URL && URL.description || "";
var location = URL && URL.location || "";
return (
<div className="room-context">
<img className="room-context-thumbnail" src={thumbnail}/>
<div className="room-context-content">
<div className="room-context-label">{mozL10n.get("context_inroom_label")}</div>
<div className="room-context-description">{URLDescription}</div>
<a className="room-context-url" href={location} target="_blank">{location}</a>
{this.props.roomData.roomDescription ?
<div className="room-context-comment">{this.props.roomData.roomDescription}</div> :
null}
<button className="room-context-btn-close"
onClick={this.handleCloseClick}/>
</div>
<SocialShareDropdown dispatcher={this.props.dispatcher}
roomStore={this.props.roomStore}
show={this.state.showMenu}
ref="menu"/>
</div>
);
}
@ -285,18 +340,8 @@ loop.roomViews = (function(mozL10n) {
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
},
_renderInvitationOverlay: function() {
if (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS) {
return (
<DesktopRoomInvitationView
roomStore={this.props.roomStore}
dispatcher={this.props.dispatcher} />
);
}
return null;
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired
},
componentWillUpdate: function(nextProps, nextState) {
@ -341,6 +386,17 @@ loop.roomViews = (function(mozL10n) {
}));
},
_shouldRenderInvitationOverlay: function() {
return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
},
_shouldRenderContextView: function() {
return !!(
this.props.mozLoop.getLoopPref("contextInConverations.enabled") &&
(this.state.roomContextUrls || this.state.roomDescription)
);
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -358,6 +414,10 @@ loop.roomViews = (function(mozL10n) {
visible: true
};
var shouldRenderInvitationOverlay = this._shouldRenderInvitationOverlay();
var shouldRenderContextView = this._shouldRenderContextView();
var roomData = this.props.roomStore.getStoreState("activeRoom");
switch(this.state.roomState) {
case ROOM_STATES.FAILED:
case ROOM_STATES.FULL: {
@ -378,7 +438,12 @@ loop.roomViews = (function(mozL10n) {
default: {
return (
<div className="room-conversation-wrapper">
{this._renderInvitationOverlay()}
<DesktopRoomInvitationView
dispatcher={this.props.dispatcher}
error={this.state.error}
roomData={roomData}
show={shouldRenderInvitationOverlay}
showContext={shouldRenderContextView} />
<div className="video-layout-wrapper">
<div className="conversation room-conversation">
<div className="media nested">
@ -397,6 +462,9 @@ loop.roomViews = (function(mozL10n) {
screenShare={screenShareData} />
</div>
</div>
<DesktopRoomContextView
roomData={roomData}
show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
</div>
);
}
@ -407,6 +475,7 @@ loop.roomViews = (function(mozL10n) {
return {
ActiveRoomStoreMixin: ActiveRoomStoreMixin,
SocialShareDropdown: SocialShareDropdown,
DesktopRoomContextView: DesktopRoomContextView,
DesktopRoomConversationView: DesktopRoomConversationView,
DesktopRoomInvitationView: DesktopRoomInvitationView
};

View File

@ -853,12 +853,25 @@ html, .fx-embedded, #main,
background: rgba(0,0,0,.6);
/* This matches .fx-embedded .conversation toolbar height */
top: 26px;
height: calc(100% - 26px);
right: 0;
bottom: 0;
left: 0;
text-align: center;
color: #fff;
z-index: 1010;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
align-items: stretch;
}
.room-invitation-content {
order: 1;
flex: 1 1 auto;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
.room-invitation-overlay .error-display-area.error,
@ -883,8 +896,8 @@ html, .fx-embedded, #main,
color: #d74345;
}
.room-invitation-overlay form {
padding: 6em 0 2em 0;
.room-invitation-overlay .btn-group {
padding: 0;
}
.room-invitation-overlay textarea {
@ -900,11 +913,6 @@ html, .fx-embedded, #main,
border-radius: .5em;
}
.room-invitation-overlay .btn-group {
position: absolute;
bottom: 10px;
}
.share-service-dropdown {
color: #000;
text-align: start;
@ -962,6 +970,112 @@ body[dir=rtl] .share-service-dropdown .share-panel-header {
background-image: url("../img/icons-16x16.svg#add-active");
}
.room-context {
background: rgba(0,0,0,.6);
border-top: 2px solid #444;
border-bottom: 2px solid #444;
padding: .5rem;
max-height: 120px;
position: absolute;
left: 0;
bottom: 0;
width: 100%;
font-size: .9em;
display: flex;
flex-flow: row nowrap;
align-content: flex-start;
align-items: flex-start;
}
.room-invitation-overlay .room-context {
position: relative;
left: auto;
bottom: auto;
order: 2;
flex: 0 1 auto;
}
.room-context-thumbnail {
width: 100px;
max-height: 200px;
-moz-margin-end: 1ch;
margin-bottom: 1em;
order: 1;
flex: 0 1 auto;
}
body[dir=rtl] .room-context-thumbnail {
order: 2;
}
.room-context-thumbnail[src=""] {
display: none;
}
.room-context-content {
order: 2;
flex: 1 1 auto;
text-align: start;
}
body[dir=rtl] .room-context-content {
order: 1;
}
.room-context-label {
margin-bottom: 1em;
}
.room-context-label,
.room-context-description {
color: #fff;
}
.room-context-comment {
color: #707070;
}
.room-context-description,
.room-context-comment {
word-wrap: break-word;
}
.room-context-url {
color: #59A1D7;
font-style: italic;
text-decoration: none;
margin-bottom: 1em;
}
.room-context-url:hover {
text-decoration: underline;
}
.room-context-btn-close {
position: absolute;
right: 5px;
top: 5px;
width: 8px;
height: 8px;
background-color: transparent;
background-image: url("../img/icons-10x10.svg#close");
background-size: 8px 8px;
background-repeat: no-repeat;
border: 0;
padding: 0;
cursor: pointer;
}
.room-context-btn-close:hover,
.room-context-btn-close:hover:active {
background-image: url("../img/icons-10x10.svg#close-active");
}
body[dir=rtl] .room-context-btn-close {
right: auto;
left: 5px;
}
/* Standalone rooms */
.standalone .room-conversation-wrapper {

View File

@ -401,6 +401,8 @@ loop.shared.actions = (function() {
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
*/
SetupRoomInfo: Action.define("setupRoomInfo", {
// roomContextUrls: Array - Optional.
// roomDescription: String - Optional.
// roomName: String - Optional.
roomOwner: String,
roomToken: String,
@ -416,7 +418,7 @@ loop.shared.actions = (function() {
* @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
*/
UpdateRoomInfo: Action.define("updateRoomInfo", {
// context: Object - Optional.
// description: String - Optional.
// roomName: String - Optional.
roomOwner: String,
roomUrl: String

View File

@ -23,6 +23,13 @@ loop.store.ActiveRoomStore = (function() {
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
var OPTIONAL_ROOMINFO_FIELDS = {
urls: "roomContextUrls",
description: "roomDescription",
roomInfoFailure: "roomInfoFailure",
roomName: "roomName"
};
/**
* Active room store.
*
@ -84,6 +91,8 @@ loop.store.ActiveRoomStore = (function() {
roomContextUrls: null,
// The roomCryptoKey to decode the context data if necessary.
roomCryptoKey: null,
// The description for a room as stored in the context data.
roomDescription: null,
// Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
roomInfoFailure: null,
// The name of the room.
@ -185,6 +194,8 @@ loop.store.ActiveRoomStore = (function() {
this.dispatchAction(new sharedActions.SetupRoomInfo({
roomToken: actionData.roomToken,
roomContextUrls: roomData.decryptedContext.urls,
roomDescription: roomData.decryptedContext.description,
roomName: roomData.decryptedContext.roomName,
roomOwner: roomData.roomOwner,
roomUrl: roomData.roomUrl,
@ -275,6 +286,7 @@ loop.store.ActiveRoomStore = (function() {
.then(function(decryptedResult) {
var realResult = JSON.parse(decryptedResult);
roomInfoData.description = realResult.description;
roomInfoData.urls = realResult.urls;
roomInfoData.roomName = realResult.roomName;
@ -299,6 +311,8 @@ loop.store.ActiveRoomStore = (function() {
}
this.setStoreState({
roomContextUrls: actionData.roomContextUrls,
roomDescription: actionData.roomDescription,
roomName: actionData.roomName,
roomOwner: actionData.roomOwner,
roomState: ROOM_STATES.READY,
@ -324,13 +338,18 @@ loop.store.ActiveRoomStore = (function() {
* @param {sharedActions.UpdateRoomInfo} actionData
*/
updateRoomInfo: function(actionData) {
this.setStoreState({
roomContextUrls: actionData.urls,
roomInfoFailure: actionData.roomInfoFailure,
roomName: actionData.roomName,
var newState = {
roomOwner: actionData.roomOwner,
roomUrl: actionData.roomUrl
};
// Iterate over the optional fields that _may_ be present on the actionData
// object.
Object.keys(OPTIONAL_ROOMINFO_FIELDS).forEach(function(field) {
if (actionData[field]) {
newState[OPTIONAL_ROOMINFO_FIELDS[field]] = actionData[field];
}
});
this.setStoreState(newState);
},
/**

View File

@ -66,11 +66,9 @@ loop.shared.views = (function(_, l10n) {
render: function() {
return (
/* jshint ignore:start */
React.createElement("button", {className: this._getClasses(),
title: this._getTitle(),
onClick: this.handleClick})
/* jshint ignore:end */
);
}
});
@ -451,7 +449,6 @@ loop.shared.views = (function(_, l10n) {
"local-stream": true,
"local-stream-audio": !this.state.video.enabled
});
/* jshint ignore:start */
return (
React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation in-call"},
@ -468,7 +465,6 @@ loop.shared.views = (function(_, l10n) {
)
)
);
/* jshint ignore:end */
}
});

View File

@ -66,11 +66,9 @@ loop.shared.views = (function(_, l10n) {
render: function() {
return (
/* jshint ignore:start */
<button className={this._getClasses()}
title={this._getTitle()}
onClick={this.handleClick}></button>
/* jshint ignore:end */
);
}
});
@ -451,7 +449,6 @@ loop.shared.views = (function(_, l10n) {
"local-stream": true,
"local-stream-audio": !this.state.video.enabled
});
/* jshint ignore:start */
return (
<div className="video-layout-wrapper">
<div className="conversation in-call">
@ -468,7 +465,6 @@ loop.shared.views = (function(_, l10n) {
</div>
</div>
);
/* jshint ignore:end */
}
});

View File

@ -137,7 +137,8 @@ describe("loop.conversation", function() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversation.AppControllerView, {
roomStore: roomStore,
dispatcher: dispatcher
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
}));
}

View File

@ -8,8 +8,8 @@ describe("loop.roomViews", function () {
var ROOM_STATES = loop.store.ROOM_STATES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow;
var fakeMozLoop;
var sandbox, dispatcher, roomStore, activeRoomStore, fakeWindow,
fakeMozLoop, fakeContextURL;
beforeEach(function() {
sandbox = sinon.sandbox.create();
@ -48,6 +48,12 @@ describe("loop.roomViews", function () {
mozLoop: {},
activeRoomStore: activeRoomStore
});
fakeContextURL = {
description: "An invalid page",
location: "http://invalid.com",
thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
};
});
afterEach(function() {
@ -103,22 +109,24 @@ describe("loop.roomViews", function () {
view = null;
});
function mountTestComponent() {
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
roomData: {},
show: true,
showContext: false
}, props);
return TestUtils.renderIntoDocument(
React.createElement(
loop.roomViews.DesktopRoomInvitationView, {
dispatcher: dispatcher,
roomStore: roomStore
}));
React.createElement(loop.roomViews.DesktopRoomInvitationView, props));
}
it("should dispatch an EmailRoomUrl action when the email button is " +
"pressed", function() {
view = mountTestComponent();
it("should dispatch an EmailRoomUrl action when the email button is pressed",
function() {
view = mountTestComponent({
roomData: { roomUrl: "http://invalid" }
});
view.setState({roomUrl: "http://invalid"});
var emailBtn = view.getDOMNode().querySelector('.btn-email');
var emailBtn = view.getDOMNode().querySelector(".btn-email");
React.addons.TestUtils.Simulate.click(emailBtn);
@ -131,13 +139,14 @@ describe("loop.roomViews", function () {
var roomNameBox;
beforeEach(function() {
view = mountTestComponent();
view.setState({
roomToken: "fakeToken",
roomName: "fakeName"
view = mountTestComponent({
roomData: {
roomToken: "fakeToken",
roomName: "fakeName"
}
});
roomNameBox = view.getDOMNode().querySelector('.input-room-name');
roomNameBox = view.getDOMNode().querySelector(".input-room-name");
});
it("should dispatch a RenameRoom action when the focus is lost",
@ -175,14 +184,14 @@ describe("loop.roomViews", function () {
describe("Copy Button", function() {
beforeEach(function() {
view = mountTestComponent();
view.setState({roomUrl: "http://invalid"});
view = mountTestComponent({
roomData: { roomUrl: "http://invalid" }
});
});
it("should dispatch a CopyRoomUrl action when the copy button is " +
"pressed", function() {
var copyBtn = view.getDOMNode().querySelector('.btn-copy');
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
@ -192,7 +201,7 @@ describe("loop.roomViews", function () {
});
it("should change the text when the url has been copied", function() {
var copyBtn = view.getDOMNode().querySelector('.btn-copy');
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
@ -215,6 +224,25 @@ describe("loop.roomViews", function () {
expect(view.refs.menu.props.show).to.eql(true);
});
});
describe("Context", function() {
it("should not render the context data when told not to", function() {
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".room-context")).to.eql(null);
});
it("should render context when data is available", function() {
view = mountTestComponent({
showContext: true,
roomData: {
roomContextUrls: [fakeContextURL]
}
});
expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
});
});
});
describe("DesktopRoomConversationView", function() {
@ -468,14 +496,13 @@ describe("loop.roomViews", function () {
view = fakeProvider = null;
});
function mountTestComponent() {
function mountTestComponent(props) {
props = _.extend({
dispatcher: dispatcher,
show: true
}, props);
return TestUtils.renderIntoDocument(
React.createElement(loop.roomViews.SocialShareDropdown, {
dispatcher: dispatcher,
roomStore: roomStore,
show: true
})
);
React.createElement(loop.roomViews.SocialShareDropdown, props));
}
describe("#render", function() {
@ -486,36 +513,31 @@ describe("loop.roomViews", function () {
});
it("should show different contents when the Share XUL button is not available", function() {
activeRoomStore.setStoreState({
view = mountTestComponent({
socialShareProviders: []
});
view = mountTestComponent();
var node = view.getDOMNode();
expect(node.querySelector(".share-panel-header")).to.not.eql(null);
});
it("should show an empty list when no Social Providers are available", function() {
activeRoomStore.setStoreState({
view = mountTestComponent({
socialShareButtonAvailable: true,
socialShareProviders: []
});
view = mountTestComponent();
var node = view.getDOMNode();
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelectorAll(".dropdown-menu-item").length).to.eql(1);
});
it("should show a list of available Social Providers", function() {
activeRoomStore.setStoreState({
view = mountTestComponent({
socialShareButtonAvailable: true,
socialShareProviders: [fakeProvider]
});
view = mountTestComponent();
var node = view.getDOMNode();
expect(node.querySelector(".icon-add-share-service")).to.not.eql(null);
expect(node.querySelector(".dropdown-menu-separator")).to.not.eql(null);
@ -530,12 +552,10 @@ describe("loop.roomViews", function () {
describe("#handleToolbarAddButtonClick", function() {
it("should dispatch an action when the 'add to toolbar' button is clicked", function() {
activeRoomStore.setStoreState({
view = mountTestComponent({
socialShareProviders: []
});
view = mountTestComponent();
var addButton = view.getDOMNode().querySelector(".btn-toolbar-add");
React.addons.TestUtils.Simulate.click(addButton);
@ -547,13 +567,11 @@ describe("loop.roomViews", function () {
describe("#handleAddServiceClick", function() {
it("should dispatch an action when the 'add provider' item is clicked", function() {
activeRoomStore.setStoreState({
view = mountTestComponent({
socialShareProviders: [],
socialShareButtonAvailable: true
});
view = mountTestComponent();
var addItem = view.getDOMNode().querySelector(".dropdown-menu-item:first-child");
React.addons.TestUtils.Simulate.click(addItem);
@ -565,14 +583,12 @@ describe("loop.roomViews", function () {
describe("#handleProviderClick", function() {
it("should dispatch an action when a provider item is clicked", function() {
activeRoomStore.setStoreState({
view = mountTestComponent({
roomUrl: "http://example.com",
socialShareButtonAvailable: true,
socialShareProviders: [fakeProvider]
});
view = mountTestComponent();
var providerItem = view.getDOMNode().querySelector(".dropdown-menu-item:last-child");
React.addons.TestUtils.Simulate.click(providerItem);
@ -580,10 +596,73 @@ describe("loop.roomViews", function () {
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.ShareRoomUrl({
provider: fakeProvider,
roomUrl: activeRoomStore.getStoreState().roomUrl,
roomUrl: "http://example.com",
previews: []
}));
});
});
});
describe("DesktopRoomContextView", function() {
var view;
afterEach(function() {
view = null;
});
function mountTestComponent(props) {
props = _.extend({
show: true
}, props);
return TestUtils.renderIntoDocument(
React.createElement(loop.roomViews.DesktopRoomContextView, props));
}
describe("#render", function() {
it("should show the context information properly when available", function() {
view = mountTestComponent({
roomData: {
roomDescription: "Hello, is it me you're looking for?",
roomContextUrls: [fakeContextURL]
}
});
var node = view.getDOMNode();
expect(node).to.not.eql(null);
expect(node.querySelector(".room-context-thumbnail").src).to.
eql(fakeContextURL.thumbnail);
expect(node.querySelector(".room-context-description").textContent).to.
eql(fakeContextURL.description);
expect(node.querySelector(".room-context-comment").textContent).to.
eql(view.props.roomData.roomDescription);
});
it("should not render optional data", function() {
view = mountTestComponent({
roomData: { roomContextUrls: [fakeContextURL] }
});
expect(view.getDOMNode().querySelector(".room-context-comment")).to.
eql(null);
});
it("should not render the component when 'show' is false", function() {
view = mountTestComponent({
show: false
});
expect(view.getDOMNode()).to.eql(null);
});
it("should close the view when the close button is clicked", function() {
view = mountTestComponent({
roomData: { roomContextUrls: [fakeContextURL] }
});
var closeBtn = view.getDOMNode().querySelector(".room-context-btn-close");
React.addons.TestUtils.Simulate.click(closeBtn);
expect(view.getDOMNode()).to.eql(null);
});
})
});
});

View File

@ -282,6 +282,8 @@ describe("loop.store.ActiveRoomStore", function () {
sinon.assert.calledTwice(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupRoomInfo({
roomContextUrls: undefined,
roomDescription: undefined,
roomToken: fakeToken,
roomName: fakeRoomData.decryptedContext.roomName,
roomOwner: fakeRoomData.roomOwner,
@ -461,6 +463,7 @@ describe("loop.store.ActiveRoomStore", function () {
fetchServerAction.cryptoKey = "fakeKey";
var roomContext = {
description: "Never gonna let you down. Never gonna give you up...",
roomName: "The wonderful Loopy room",
urls: [{
description: "An invalid page",

View File

@ -747,7 +747,9 @@ BrowserGlue.prototype = {
Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
#ifdef NIGHTLY_BUILD
AddonWatcher.init(this._notifySlowAddon);
#endif
},
_checkForOldBuildUpdates: function () {
@ -1006,7 +1008,9 @@ BrowserGlue.prototype = {
#endif
webrtcUI.uninit();
FormValidationHandler.uninit();
#ifdef NIGHTLY_BUILD
AddonWatcher.uninit();
#endif
},
_initServiceDiscovery: function () {

View File

@ -22,6 +22,7 @@ support-files =
doc_inspector_menu.html
doc_inspector_remove-iframe-during-load.html
doc_inspector_search.html
doc_inspector_search-reserved.html
doc_inspector_search-suggestions.html
doc_inspector_select-last-selected-01.html
doc_inspector_select-last-selected-02.html
@ -95,6 +96,7 @@ skip-if = e10s # Test synthesize scrolling events in content. Also, see bug 1035
[browser_inspector_search-03.js]
[browser_inspector_search-04.js]
[browser_inspector_search-05.js]
[browser_inspector_search-reserved.js]
[browser_inspector_select-docshell.js]
[browser_inspector_select-last-selected.js]
[browser_inspector_search-navigation.js]

View File

@ -0,0 +1,134 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Testing searching for ids and classes that contain reserved characters.
const TEST_URL = TEST_URL_ROOT + "doc_inspector_search-reserved.html";
// An array of (key, suggestions) pairs where key is a key to press and
// suggestions is an array of suggestions that should be shown in the popup.
// Suggestion is an object with label of the entry and optional count
// (defaults to 1)
const TEST_DATA = [
{
key: "#",
suggestions: [{label: "#d1\\.d2", count: 1}]
},
{
key: "d",
suggestions: [{label: "#d1\\.d2", count: 1}]
},
{
key: "VK_BACK_SPACE",
suggestions: [{label: "#d1\\.d2", count: 1}]
},
{
key: "VK_BACK_SPACE",
suggestions: []
},
{
key: ".",
suggestions: [{label: ".c1\\.c2", count: 1}]
},
{
key: "c",
suggestions: [{label: ".c1\\.c2", count: 1}]
},
{
key: "VK_BACK_SPACE",
suggestions: [{label: ".c1\\.c2", count: 1}]
},
{
key: "VK_BACK_SPACE",
suggestions: []
},
{
key: "d",
suggestions: [{label: "div", count: 2},
{label: "#d1\\.d2", count: 1}]
},
{
key: "VK_BACK_SPACE",
suggestions: []
},
{
key:"c",
suggestions: [{label: ".c1\\.c2", count: 1}]
},
{
key: "VK_BACK_SPACE",
suggestions: []
},
{
key: "b",
suggestions: [{label: "body", count: 1}]
},
{
key: "o",
suggestions: [{label: "body", count: 1}]
},
{
key: "d",
suggestions: [{label: "body", count: 1}]
},
{
key: "y",
suggestions: []
},
{
key: " ",
suggestions: [{label: "body div", count: 2}]
},
{
key: ".",
suggestions: [{label: "body .c1\\.c2", count: 1}]
},
{
key: "VK_BACK_SPACE",
suggestions: [{label: "body div", count: 2}]
},
{
key: "#",
suggestions: [{label: "body #", count: 1},
{label: "body #d1\\.d2", count: 1}]
}
];
add_task(function* () {
let { inspector } = yield openInspectorForURL(TEST_URL);
let searchBox = inspector.searchBox;
let popup = inspector.searchSuggestions.searchPopup;
yield focusSearchBoxUsingShortcut(inspector.panelWin);
for (let { key, suggestions } of TEST_DATA) {
info("Pressing " + key + " to get " + formatSuggestions(suggestions));
let command = once(searchBox, "command");
EventUtils.synthesizeKey(key, {}, inspector.panelWin);
yield command;
info("Waiting for search query to complete");
yield inspector.searchSuggestions._lastQuery;
info("Query completed. Performing checks for input '" + searchBox.value + "'");
let actualSuggestions = popup.getItems().reverse();
is(popup.isOpen ? actualSuggestions.length: 0, suggestions.length,
"There are expected number of suggestions.");
for (let i = 0; i < suggestions.length; i++) {
is(suggestions[i].label, actualSuggestions[i].label,
"The suggestion at " + i + "th index is correct.");
is(suggestions[i].count || 1, actualSuggestions[i].count,
"The count for suggestion at " + i + "th index is correct.");
}
}
});
function formatSuggestions(suggestions) {
return "[" + suggestions
.map(s => "'" + s.label + "' (" + s.count || 1 + ")")
.join(", ") + "]";
}

View File

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Inspector Search Box Reserved Character Test</title>
</head>
<body>
<div id="d1.d2">Hi, I'm an id that contains a CSS reserved character</div>
<div class="c1.c2">Hi, a class that contains a CSS reserved character</div>
</body>
</html>

View File

@ -1,7 +1,6 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
const TEST_URI = "data:text/html;charset=utf-8,browser_css_color.js";
let {colorUtils} = devtools.require("devtools/css-color");
let origColorUnit;
@ -9,7 +8,6 @@ let origColorUnit;
add_task(function*() {
yield promiseTab("about:blank");
let [host, win, doc] = yield createHost("bottom", TEST_URI);
origColorUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
info("Creating a test canvas element to test colors");
let canvas = createTestCanvas(doc);
@ -49,23 +47,19 @@ function testColorUtils(canvas) {
}
function testToString(color, name, hex, hsl, rgb) {
switchColorUnit(colorUtils.CssColor.COLORUNIT.name);
color.colorUnit = colorUtils.CssColor.COLORUNIT.name;
is(color.toString(), name, "toString() with authored type");
switchColorUnit(colorUtils.CssColor.COLORUNIT.hex);
color.colorUnit = colorUtils.CssColor.COLORUNIT.hex;
is(color.toString(), hex, "toString() with hex type");
switchColorUnit(colorUtils.CssColor.COLORUNIT.hsl);
color.colorUnit = colorUtils.CssColor.COLORUNIT.hsl;
is(color.toString(), hsl, "toString() with hsl type");
switchColorUnit(colorUtils.CssColor.COLORUNIT.rgb);
color.colorUnit = colorUtils.CssColor.COLORUNIT.rgb;
is(color.toString(), rgb, "toString() with rgb type");
}
function switchColorUnit(unit) {
Services.prefs.setCharPref(COLOR_UNIT_PREF, unit);
}
function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
let target;
let ctx = canvas.getContext("2d");
@ -110,7 +104,6 @@ function testColorMatch(name, hex, hsl, rgb, rgba, canvas) {
test(hex, "hex");
test(hsl, "hsl");
test(rgb, "rgb");
switchColorUnit(origColorUnit);
}
function testProcessCSSString() {

View File

@ -28,7 +28,7 @@ function test() {
expectUncaughtException();
if (!DeveloperToolbar.visible) {
DeveloperToolbar.show(true, onOpenToolbar);
DeveloperToolbar.show(true).then(onOpenToolbar);
}
else {
onOpenToolbar();

View File

@ -44,6 +44,9 @@ function promiseTab(aURL) {
}
registerCleanupFunction(function tearDown() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
yield gDevTools.closeToolbox(target);
while (gBrowser.tabs.length > 1) {
gBrowser.removeCurrentTab();
}

View File

@ -1020,6 +1020,11 @@ SwatchBasedEditorTooltip.prototype = {
_onSwatchClick: function(event) {
let swatch = this.swatches.get(event.target);
if (event.shiftKey) {
event.stopPropagation();
return;
}
if (swatch) {
this.activeSwatch = event.target;
this.show();

View File

@ -376,7 +376,7 @@ StyleSheetEditor.prototype = {
lineNumbers: true,
mode: Editor.modes.css,
readOnly: false,
autoCloseBrackets: "{}()[]",
autoCloseBrackets: "{}()",
extraKeys: this._getKeyBindings(),
contextMenu: "sourceEditorContextMenu",
autocomplete: Services.prefs.getBoolPref(AUTOCOMPLETION_PREF),

View File

@ -48,6 +48,7 @@ support-files =
doc_xulpage.xul
[browser_styleeditor_autocomplete.js]
[browser_styleeditor_autocomplete-disabled.js]
[browser_styleeditor_bug_740541_iframes.js]
skip-if = os == "linux" || "mac" # bug 949355
[browser_styleeditor_bug_851132_middle_click.js]

View File

@ -0,0 +1,26 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that autocomplete can be disabled.
const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
// Pref which decides if CSS autocompletion is enabled in Style Editor or not.
const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
add_task(function* () {
Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
let editor = yield ui.editors[0].getSourceEditor();
is(editor.sourceEditor.getOption("autocomplete"), false,
"Autocompletion option does not exist");
ok(!editor.sourceEditor.getAutocompletionPopup(),
"Autocompletion popup does not exist");
});
registerCleanupFunction(() => {
Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
});

View File

@ -1,20 +1,13 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that autocompletion works as expected.
const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
const MAX_SUGGESTIONS = 15;
// Pref which decides if CSS autocompletion is enabled in Style Editor or not.
const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
const {CSSProperties, CSSValues} = getCSSKeywords();
// Test cases to test that autocompletion works correctly when enabled.
@ -87,35 +80,21 @@ let TEST_CASES = [
['Ctrl+Space', {total: 1, current: 0}],
];
let gEditor;
let gPopup;
let index = 0;
add_task(function* () {
let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
let editor = yield ui.editors[0].getSourceEditor();
let sourceEditor = editor.sourceEditor;
let popup = sourceEditor.getAutocompletionPopup();
function test()
{
waitForExplicitFinish();
yield SimpleTest.promiseFocus(panel.panelWindow);
addTabAndOpenStyleEditors(1, testEditorAdded);
content.location = TESTCASE_URI;
}
function testEditorAdded(panel) {
info("Editor added, getting the source editor and starting tests");
panel.UI.editors[0].getSourceEditor().then(editor => {
info("source editor found, starting tests.");
gEditor = editor.sourceEditor;
gPopup = gEditor.getAutocompletionPopup();
waitForFocus(testState, gPanelWindow);
});
}
function testState() {
if (index == TEST_CASES.length) {
testAutocompletionDisabled();
return;
for (let index in TEST_CASES) {
yield testState(index, sourceEditor, popup, panel.panelWindow);
yield checkState(index, sourceEditor, popup);
}
});
function testState(index, sourceEditor, popup, panelWindow) {
let [key, details] = TEST_CASES[index];
let entered;
if (details) {
@ -136,80 +115,54 @@ function testState() {
evt = "popup-hidden";
}
else if (/(left|right|return|home|end)/ig.test(key) ||
(key == "VK_DOWN" && !gPopup.isOpen)) {
(key == "VK_DOWN" && !popup.isOpen)) {
evt = "cursorActivity";
}
else if (key == "VK_TAB" || key == "VK_UP" || key == "VK_DOWN") {
evt = "suggestion-entered";
}
gEditor.once(evt, checkState);
EventUtils.synthesizeKey(key, mods, gPanelWindow);
let ready = sourceEditor.once(evt);
EventUtils.synthesizeKey(key, mods, panelWindow);
return ready;
}
function checkState() {
function checkState(index, sourceEditor, popup) {
let deferred = promise.defer();
executeSoon(() => {
let [key, details] = TEST_CASES[index];
details = details || {};
let {total, current, inserted} = details;
if (total != undefined) {
ok(gPopup.isOpen, "Popup is open for index " + index);
is(total, gPopup.itemCount,
ok(popup.isOpen, "Popup is open for index " + index);
is(total, popup.itemCount,
"Correct total suggestions for index " + index);
is(current, gPopup.selectedIndex,
is(current, popup.selectedIndex,
"Correct index is selected for index " + index);
if (inserted) {
let { preLabel, label, text } = gPopup.getItemAtIndex(current);
let { line, ch } = gEditor.getCursor();
let lineText = gEditor.getText(line);
let { preLabel, label, text } = popup.getItemAtIndex(current);
let { line, ch } = sourceEditor.getCursor();
let lineText = sourceEditor.getText(line);
is(lineText.substring(ch - text.length, ch), text,
"Current suggestion from the popup is inserted into the editor.");
}
}
else {
ok(!gPopup.isOpen, "Popup is closed for index " + index);
ok(!popup.isOpen, "Popup is closed for index " + index);
if (inserted) {
let { preLabel, label, text } = gPopup.getItemAtIndex(current);
let { line, ch } = gEditor.getCursor();
let lineText = gEditor.getText(line);
let { preLabel, label, text } = popup.getItemAtIndex(current);
let { line, ch } = sourceEditor.getCursor();
let lineText = sourceEditor.getText(line);
is(lineText.substring(ch - text.length, ch), text,
"Current suggestion from the popup is inserted into the editor.");
}
}
index++;
testState();
deferred.resolve();
});
}
function testAutocompletionDisabled() {
gBrowser.removeCurrentTab();
index = 0;
info("Starting test to check if autocompletion is disabled correctly.")
Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
addTabAndOpenStyleEditors(1, testEditorAddedDisabled);
content.location = TESTCASE_URI;
}
function testEditorAddedDisabled(panel) {
info("Editor added, getting the source editor and starting tests");
panel.UI.editors[0].getSourceEditor().then(editor => {
is(editor.sourceEditor.getOption("autocomplete"), false,
"Autocompletion option does not exist");
ok(!editor.sourceEditor.getAutocompletionPopup(),
"Autocompletion popup does not exist");
cleanup();
});
}
function cleanup() {
Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
gEditor = null;
gPopup = null;
finish();
return deferred.promise;
}
/**

View File

@ -1,7 +1,10 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
function test()
// Test that sheets inside iframes are shown in the editor.
add_task(function* ()
{
function makeStylesheet(selector) {
@ -69,10 +72,8 @@ function test()
const EXPECTED_STYLE_SHEET_COUNT = 12;
waitForExplicitFinish();
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
// Wait for events until the right number of editors has been opened.
addTabAndOpenStyleEditors(EXPECTED_STYLE_SHEET_COUNT, () => finish());
content.location = TESTCASE_URI;
}
is(ui.editors.length, EXPECTED_STYLE_SHEET_COUNT,
"Got the expected number of style sheets.");
});

View File

@ -1,75 +1,56 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that middle click on style sheet doesn't open styleeditor.xul in a new
// tab (bug 851132).
const TESTCASE_URI = TEST_BASE_HTTP + "four.html";
let gUI;
function test() {
waitForExplicitFinish();
addTabAndOpenStyleEditors(4, runTests);
content.location = TESTCASE_URI;
}
let timeoutID;
function runTests(panel) {
gUI = panel.UI;
add_task(function* () {
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
gBrowser.tabContainer.addEventListener("TabOpen", onTabAdded, false);
gUI.editors[0].getSourceEditor().then(onEditor0Attach);
gUI.editors[1].getSourceEditor().then(onEditor1Attach);
}
function getStylesheetNameLinkFor(aEditor) {
return aEditor.summary.querySelector(".stylesheet-name");
}
function onEditor0Attach(aEditor) {
yield ui.editors[0].getSourceEditor();
info("first editor selected");
waitForFocus(function () {
// left mouse click should focus editor 1
EventUtils.synthesizeMouseAtCenter(
getStylesheetNameLinkFor(gUI.editors[1]),
{button: 0},
gPanelWindow);
}, gPanelWindow);
}
info("Left-clicking on the second editor link.");
yield clickOnStyleSheetLink(ui.editors[1], 0);
function onEditor1Attach(aEditor) {
info("second editor selected");
info("Waiting for the second editor to be selected.");
let editor = yield ui.once("editor-selected");
// Wait for the focus to be set.
executeSoon(function () {
ok(aEditor.sourceEditor.hasFocus(),
"left mouse click has given editor 1 focus");
ok(editor.sourceEditor.hasFocus(),
"Left mouse click gave second editor focus.");
// right mouse click should not open a new tab
EventUtils.synthesizeMouseAtCenter(
getStylesheetNameLinkFor(gUI.editors[2]),
{button: 1},
gPanelWindow);
// middle mouse click should not open a new tab
info("Middle clicking on the third editor link.");
yield clickOnStyleSheetLink(ui.editors[2], 1);
});
setTimeout(finish, 0);
});
/**
* A helper that clicks on style sheet link in the sidebar.
*
* @param {StyleSheetEditor} editor
* The editor of which link should be clicked.
* @param {MouseEvent.button} button
* The button to click the link with.
*/
function* clickOnStyleSheetLink(editor, button) {
let window = editor._window;
let link = editor.summary.querySelector(".stylesheet-name");
info("Waiting for focus.");
yield SimpleTest.promiseFocus(window);
info("Pressing button " + button + " on style sheet name link.");
EventUtils.synthesizeMouseAtCenter(link, { button }, window);
}
function onTabAdded() {
ok(false, "middle mouse click has opened a new tab");
finish();
}
registerCleanupFunction(function () {
gBrowser.tabContainer.removeEventListener("TabOpen", onTabAdded, false);
gUI = null;
});

View File

@ -1,32 +1,17 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that style sheets can be disabled and enabled.
// https rather than chrome to improve coverage
const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
function test()
{
waitForExplicitFinish();
add_task(function* () {
let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
let editor = yield ui.editors[0].getSourceEditor();
let count = 0;
addTabAndOpenStyleEditors(2, function(panel) {
// we test against first stylesheet after all are ready
let UI = panel.UI;
let editor = UI.editors[0];
editor.getSourceEditor().then(runTests.bind(this, UI, editor));
});
content.location = TESTCASE_URI;
}
function runTests(UI, editor)
{
testEnabledToggle(UI, editor);
}
function testEnabledToggle(UI, editor)
{
let summary = editor.summary;
let enabledToggle = summary.querySelector(".stylesheet-enabled");
ok(enabledToggle, "enabled toggle button exists");
@ -37,34 +22,34 @@ function testEnabledToggle(UI, editor)
is(summary.classList.contains("disabled"), false,
"first stylesheet is initially enabled, UI does not have DISABLED class");
let disabledToggleCount = 0;
editor.on("property-change", function(event, property) {
if (property != "disabled") {
return;
}
disabledToggleCount++;
info("Disabling the first stylesheet.");
yield toggleEnabled(editor, enabledToggle, panel.panelWindow);
if (disabledToggleCount == 1) {
is(editor.styleSheet.disabled, true, "first stylesheet is now disabled");
is(summary.classList.contains("disabled"), true,
"first stylesheet is now disabled, UI has DISABLED class");
is(editor.styleSheet.disabled, true, "first stylesheet is now disabled");
is(summary.classList.contains("disabled"), true,
"first stylesheet is now disabled, UI has DISABLED class");
// now toggle it back to enabled
waitForFocus(function () {
EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gPanelWindow);
}, gPanelWindow);
return;
}
info("Enabling the first stylesheet again.");
yield toggleEnabled(editor, enabledToggle, panel.panelWindow);
// disabledToggleCount == 2
is(editor.styleSheet.disabled, false, "first stylesheet is now enabled again");
is(summary.classList.contains("disabled"), false,
"first stylesheet is now enabled again, UI does not have DISABLED class");
is(editor.styleSheet.disabled, false, "first stylesheet is now enabled again");
is(summary.classList.contains("disabled"), false,
"first stylesheet is now enabled again, UI does not have DISABLED class");
});
finish();
});
function* toggleEnabled(editor, enabledToggle, panelWindow) {
let changed = editor.once("property-change");
waitForFocus(function () {
EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, gPanelWindow);
}, gPanelWindow);
info("Waiting for focus.");
yield SimpleTest.promiseFocus(panelWindow);
info("Clicking on the toggle.");
EventUtils.synthesizeMouseAtCenter(enabledToggle, {}, panelWindow);
info("Waiting for stylesheet to be disabled.");
let property = yield changed;
while (property !== "disabled") {
info("Ignoring property-change for '" + property + "'.");
property = yield editor.once("property-change");
}
}

View File

@ -1,6 +1,9 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that 'Save' function works.
const TESTCASE_URI_HTML = TEST_BASE_HTTP + "simple.html";
const TESTCASE_URI_CSS = TEST_BASE_HTTP + "simple.css";
@ -14,50 +17,49 @@ Components.utils.import("resource://gre/modules/NetUtil.jsm", tempScope);
let FileUtils = tempScope.FileUtils;
let NetUtil = tempScope.NetUtil;
function test()
{
waitForExplicitFinish();
add_task(function* () {
let htmlFile = yield copy(TESTCASE_URI_HTML, "simple.html");
let cssFile = yield copy(TESTCASE_URI_CSS, "simple.css");
let uri = Services.io.newFileURI(htmlFile);
let filePath = uri.resolve("");
copy(TESTCASE_URI_HTML, "simple.html", function(htmlFile) {
copy(TESTCASE_URI_CSS, "simple.css", function(cssFile) {
addTabAndOpenStyleEditors(1, function(panel) {
let UI = panel.UI;
let editor = UI.editors[0];
editor.getSourceEditor().then(runTests.bind(this, editor));
});
let { ui } = yield openStyleEditorForURL(filePath);
let uri = Services.io.newFileURI(htmlFile);
let filePath = uri.resolve("");
content.location = filePath;
});
});
}
let editor = ui.editors[0];
yield editor.getSourceEditor();
function runTests(editor)
{
editor.sourceEditor.once("dirty-change", () => {
is(editor.sourceEditor.isClean(), false, "Editor is dirty.");
ok(editor.summary.classList.contains("unsaved"),
"Star icon is present in the corresponding summary.");
});
info("Editing the style sheet.");
let dirty = editor.sourceEditor.once("dirty-change");
let beginCursor = {line: 0, ch: 0};
editor.sourceEditor.replaceText("DIRTY TEXT", beginCursor, beginCursor);
editor.sourceEditor.once("dirty-change", () => {
is(editor.sourceEditor.isClean(), true, "Editor is clean.");
ok(!editor.summary.classList.contains("unsaved"),
"Star icon is not present in the corresponding summary.");
finish();
});
yield dirty;
is(editor.sourceEditor.isClean(), false, "Editor is dirty.");
ok(editor.summary.classList.contains("unsaved"),
"Star icon is present in the corresponding summary.");
info("Saving the changes.");
dirty = editor.sourceEditor.once("dirty-change");
editor.saveToFile(null, function (file) {
ok(file, "file should get saved directly when using a file:// URI");
});
}
function copy(aSrcChromeURL, aDestFileName, aCallback)
yield dirty;
is(editor.sourceEditor.isClean(), true, "Editor is clean.");
ok(!editor.summary.classList.contains("unsaved"),
"Star icon is not present in the corresponding summary.");
});
function copy(aSrcChromeURL, aDestFileName)
{
let deferred = promise.defer();
let destFile = FileUtils.getFile("ProfD", [aDestFileName]);
write(read(aSrcChromeURL), destFile, aCallback);
write(read(aSrcChromeURL), destFile, deferred.resolve);
return deferred.promise;
}
function read(aSrcChromeURL)

View File

@ -1,6 +1,9 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the import button in the UI works.
// http rather than chrome to improve coverage
const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
@ -12,19 +15,22 @@ let FileUtils = tempScope.FileUtils;
const FILENAME = "styleeditor-import-test.css";
const SOURCE = "body{background:red;}";
add_task(function* () {
let { panel, ui } = yield openStyleEditorForURL(TESTCASE_URI);
let gUI;
let added = ui.once("editor-added");
importSheet(ui, panel.panelWindow);
function test()
{
waitForExplicitFinish();
info("Waiting for editor to be added for the imported sheet.");
let editor = yield added;
addTabAndCheckOnStyleEditorAdded(panel => gUI = panel.UI, testEditorAdded);
is(editor.savedFile.leafName, FILENAME,
"imported stylesheet will be saved directly into the same file");
is(editor.friendlyName, FILENAME,
"imported stylesheet has the same name as the filename");
});
content.location = TESTCASE_URI;
}
function testImport()
function importSheet(ui, panelWindow)
{
// create file to import first
let file = FileUtils.getFile("ProfD", [FILENAME]);
@ -37,37 +43,14 @@ function testImport()
FileUtils.closeSafeFileOutputStream(ostream);
// click the import button now that the file to import is ready
gUI._mockImportFile = file;
ui._mockImportFile = file;
waitForFocus(function () {
let document = gPanelWindow.document
let document = panelWindow.document
let importButton = document.querySelector(".style-editor-importButton");
ok(importButton, "import button exists");
EventUtils.synthesizeMouseAtCenter(importButton, {}, gPanelWindow);
}, gPanelWindow);
EventUtils.synthesizeMouseAtCenter(importButton, {}, panelWindow);
}, panelWindow);
});
}
let gAddedCount = 0;
function testEditorAdded(aEditor)
{
if (++gAddedCount == 2) {
// test import after the 2 initial stylesheets have been loaded
gUI.editors[0].getSourceEditor().then(function() {
testImport();
});
}
if (!aEditor.savedFile) {
return;
}
is(aEditor.savedFile.leafName, FILENAME,
"imported stylesheet will be saved directly into the same file");
is(aEditor.friendlyName, FILENAME,
"imported stylesheet has the same name as the filename");
gUI = null;
finish();
}

View File

@ -1,37 +1,25 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that style editor shows sheets loaded with @import rules.
// http rather than chrome to improve coverage
const TESTCASE_URI = TEST_BASE_HTTP + "import.html";
let gUI;
add_task(function* () {
let { ui } = yield openStyleEditorForURL(TESTCASE_URI);
function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditors(3, onEditorAdded);
content.location = TESTCASE_URI;
}
function onEditorAdded(panel)
{
gUI = panel.UI;
is(gUI.editors.length, 3,
is(ui.editors.length, 3,
"there are 3 stylesheets after loading @imports");
is(gUI.editors[0].styleSheet.href, TEST_BASE_HTTP + "simple.css",
is(ui.editors[0].styleSheet.href, TEST_BASE_HTTP + "simple.css",
"stylesheet 1 is simple.css");
is(gUI.editors[1].styleSheet.href, TEST_BASE_HTTP + "import.css",
is(ui.editors[1].styleSheet.href, TEST_BASE_HTTP + "import.css",
"stylesheet 2 is import.css");
is(gUI.editors[2].styleSheet.href, TEST_BASE_HTTP + "import2.css",
is(ui.editors[2].styleSheet.href, TEST_BASE_HTTP + "import2.css",
"stylesheet 3 is import2.css");
gUI = null;
finish();
}
});

View File

@ -1,66 +1,52 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
///////////////////
//
// Whitelisting this test.
// As part of bug 1077403, the leaking uncaught rejection should be fixed.
//
thisTestLeaksUncaughtRejectionsAndShouldBeFixed("Error: Unknown sheet source");
// Test that inline style sheets get correct names if they are saved to disk and
// that those names survice a reload but not navigation to another page.
let gUI;
const FIRST_TEST_PAGE = TEST_BASE_HTTP + "inline-1.html"
const SECOND_TEST_PAGE = TEST_BASE_HTTP + "inline-2.html"
const FIRST_TEST_PAGE = TEST_BASE_HTTP + "inline-1.html";
const SECOND_TEST_PAGE = TEST_BASE_HTTP + "inline-2.html";
const SAVE_PATH = "test.css";
function test()
{
waitForExplicitFinish();
add_task(function* () {
let { ui } = yield openStyleEditorForURL(FIRST_TEST_PAGE);
addTabAndOpenStyleEditors(2, function(panel) {
gUI = panel.UI;
loadCommonFrameScript();
testIndentifierGeneration(ui);
// First test that identifiers are correcly generated. If not other tests
// are likely to fail.
testIndentifierGeneration();
yield saveFirstInlineStyleSheet(ui);
yield testFriendlyNamesAfterSave(ui);
yield reloadPage(ui);
yield testFriendlyNamesAfterSave(ui);
yield navigateToAnotherPage(ui);
yield testFriendlyNamesAfterNavigation(ui);
});
saveFirstInlineStyleSheet()
.then(testFriendlyNamesAfterSave)
.then(reloadPage)
.then(testFriendlyNamesAfterSave)
.then(navigateToAnotherPage)
.then(testFriendlyNamesAfterNavigation)
.then(finishTests);
});
content.location = FIRST_TEST_PAGE;
}
function testIndentifierGeneration() {
function testIndentifierGeneration(ui) {
let fakeStyleSheetFile = {
"href": "http://example.com/test.css",
"nodeHref": "http://example.com/",
"styleSheetIndex": 1
}
};
let fakeInlineStyleSheet = {
"href": null,
"nodeHref": "http://example.com/",
"styleSheetIndex": 2
}
};
is(gUI.getStyleSheetIdentifier(fakeStyleSheetFile), "http://example.com/test.css",
is(ui.getStyleSheetIdentifier(fakeStyleSheetFile), "http://example.com/test.css",
"URI is the identifier of style sheet file.");
is(gUI.getStyleSheetIdentifier(fakeInlineStyleSheet), "inline-2-at-http://example.com/",
is(ui.getStyleSheetIdentifier(fakeInlineStyleSheet), "inline-2-at-http://example.com/",
"Inline style sheets are identified by their page and position at that page.");
}
function saveFirstInlineStyleSheet() {
function saveFirstInlineStyleSheet(ui) {
let deferred = promise.defer();
let editor = gUI.editors[0];
let editor = ui.editors[0];
let destFile = FileUtils.getFile("ProfD", [SAVE_PATH]);
@ -72,9 +58,9 @@ function saveFirstInlineStyleSheet() {
return deferred.promise;
}
function testFriendlyNamesAfterSave() {
let firstEditor = gUI.editors[0];
let secondEditor = gUI.editors[1];
function testFriendlyNamesAfterSave(ui) {
let firstEditor = ui.editors[0];
let secondEditor = ui.editors[1];
// The friendly name of first sheet should've been remembered, the second should
// not be the same (bug 969900).
@ -86,31 +72,21 @@ function testFriendlyNamesAfterSave() {
return promise.resolve(null);
}
function reloadPage() {
function reloadPage(ui) {
info("Reloading page.");
content.location.reload();
return waitForEditors(2);
executeInContent("devtools:test:reload", {}, {}, false /* no response */);
return ui.once("stylesheets-reset");
}
function navigateToAnotherPage() {
function navigateToAnotherPage(ui) {
info("Navigating to another page.");
let deferred = promise.defer();
gBrowser.removeCurrentTab();
gUI = null;
addTabAndOpenStyleEditors(2, function(panel) {
gUI = panel.UI;
deferred.resolve();
});
content.location = SECOND_TEST_PAGE;
return deferred.promise;
executeInContent("devtools:test:navigate", { location: SECOND_TEST_PAGE }, {}, false);
return ui.once("stylesheets-reset");
}
function testFriendlyNamesAfterNavigation() {
let firstEditor = gUI.editors[0];
let secondEditor = gUI.editors[1];
function testFriendlyNamesAfterNavigation(ui) {
let firstEditor = ui.editors[0];
let secondEditor = ui.editors[1];
// Inline style sheets shouldn't have the name of previously saved file as the
// page is different.
@ -121,35 +97,3 @@ function testFriendlyNamesAfterNavigation() {
return promise.resolve(null);
}
function finishTests() {
gUI = null;
finish();
}
/**
* Waits for all editors to be added.
*
* @param {int} aNumberOfEditors
* The number of editors to wait until proceeding.
*
* Returns a promise that's resolved once all editors are added.
*/
function waitForEditors(aNumberOfEditors) {
let deferred = promise.defer();
let count = 0;
info("Waiting for " + aNumberOfEditors + " editors to be added");
gUI.on("editor-added", function editorAdded(event, editor) {
if (++count == aNumberOfEditors) {
info("All editors added. Resolving promise.");
gUI.off("editor-added", editorAdded);
gUI.editors[0].getSourceEditor().then(deferred.resolve);
}
else {
info ("Editor " + count + " of " + aNumberOfEditors + " added.");
}
});
return deferred.promise;
}

View File

@ -2,54 +2,43 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that resizing the source editor container doesn't move the caret.
const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
let gOriginalWidth; // these are set by runTests()
let gOriginalHeight;
add_task(function* () {
let { toolbox, ui } = yield openStyleEditorForURL(TESTCASE_URI);
function test()
{
waitForExplicitFinish();
is(ui.editors.length, 2, "There are 2 style sheets initially");
addTabAndOpenStyleEditors(2, panel => runTests(panel.UI));
info("Changing toolbox host to a window.");
yield toolbox.switchHost(devtools.Toolbox.HostType.WINDOW);
content.location = TESTCASE_URI;
}
let editor = yield ui.editors[0].getSourceEditor();
let originalSourceEditor = editor.sourceEditor;
function runTests(aUI)
{
is(aUI.editors.length, 2,
"there is 2 stylesheets initially");
let hostWindow = toolbox._host._window;
let originalWidth = hostWindow.outerWidth;
let originalHeight = hostWindow.outerHeight;
aUI.editors[0].getSourceEditor().then(aEditor => {
executeSoon(function () {
waitForFocus(function () {
// queue a resize to inverse aspect ratio
// this will trigger a detach and reattach (to workaround bug 254144)
let originalSourceEditor = aEditor.sourceEditor;
let editor = aEditor.sourceEditor;
editor.setCursor(editor.getPosition(4)); // to check the caret is preserved
// to check the caret is preserved
originalSourceEditor.setCursor(originalSourceEditor.getPosition(4));
gOriginalWidth = gPanelWindow.outerWidth;
gOriginalHeight = gPanelWindow.outerHeight;
gPanelWindow.resizeTo(120, 480);
info("Resizing window.");
hostWindow.resizeTo(120, 480);
executeSoon(function () {
is(aEditor.sourceEditor, originalSourceEditor,
"the editor still references the same Editor instance");
let editor = aEditor.sourceEditor;
is(editor.getOffset(editor.getCursor()), 4,
"the caret position has been preserved");
let sourceEditor = ui.editors[0].sourceEditor;
is(sourceEditor, originalSourceEditor,
"the editor still references the same Editor instance");
// queue a resize to original aspect ratio
waitForFocus(function () {
gPanelWindow.resizeTo(gOriginalWidth, gOriginalHeight);
executeSoon(function () {
finish();
});
}, gPanelWindow);
});
}, gPanelWindow);
});
});
}
is(sourceEditor.getOffset(sourceEditor.getCursor()), 4,
"the caret position has been preserved");
info("Restoring window to original size.");
hostWindow.resizeTo(originalWidth, originalHeight);
});
registerCleanupFunction(() => {
// Restore the host type for other tests.
Services.prefs.clearUserPref("devtools.toolbox.host");
});

View File

@ -980,7 +980,6 @@ function PropertyView(aTree, aName)
this.link = "https://developer.mozilla.org/CSS/" + aName;
this.templateMatchedSelectors = aTree.styleDocument.getElementById("templateMatchedSelectors");
this._propertyInfo = new PropertyInfo(aTree, aName);
}
@ -1192,6 +1191,7 @@ PropertyView.prototype = {
this.propertyInfo.value,
{
colorSwatchClass: "computedview-colorswatch",
colorClass: "computedview-color",
urlClass: "theme-link"
// No need to use baseURI here as computed URIs are never relative.
});
@ -1222,9 +1222,10 @@ PropertyView.prototype = {
}
this._matchedSelectorResponse = matched;
CssHtmlTree.processTemplate(this.templateMatchedSelectors,
this.matchedSelectorsContainer, this);
this._buildMatchedSelectors();
this.matchedExpander.setAttribute("open", "");
this.tree.inspector.emit("computed-view-property-expanded");
}).then(null, console.error);
} else {
@ -1240,6 +1241,40 @@ PropertyView.prototype = {
return this._matchedSelectorResponse;
},
_buildMatchedSelectors: function() {
let frag = this.element.ownerDocument.createDocumentFragment();
for (let selector of this.matchedSelectorViews) {
let p = createChild(frag, "p");
let span = createChild(p, "span", {
class: "rule-link"
});
let link = createChild(span, "a", {
target: "_blank",
class: "link theme-link",
title: selector.href,
sourcelocation: selector.source,
tabindex: "0",
textContent: selector.source
});
link.addEventListener("click", selector.openStyleEditor, false);
link.addEventListener("keydown", selector.maybeOpenStyleEditor, false);
let status = createChild(p, "span", {
dir: "ltr",
class: "rule-text theme-fg-color3 " + selector.statusClass,
title: selector.statusText,
textContent: selector.sourceText
});
let valueSpan = createChild(status, "span", {
class: "other-property-value theme-fg-color1"
});
valueSpan.appendChild(selector.outputFragment);
}
this.matchedSelectorsContainer.appendChild(frag);
},
/**
* Provide access to the matched SelectorViews that we are currently
* displaying.
@ -1279,6 +1314,9 @@ PropertyView.prototype = {
*/
onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
{
if (aEvent.shiftKey) {
return;
}
this.matchedExpanded = !this.matchedExpanded;
this.refreshMatchedSelectors();
aEvent.preventDefault();
@ -1328,6 +1366,9 @@ function SelectorView(aTree, aSelectorInfo)
this.selectorInfo = aSelectorInfo;
this._cacheStatusNames();
this.openStyleEditor = this.openStyleEditor.bind(this);
this.maybeOpenStyleEditor = this.maybeOpenStyleEditor.bind(this);
this.updateSourceLink();
}
@ -1418,6 +1459,7 @@ SelectorView.prototype = {
this.selectorInfo.name,
this.selectorInfo.value, {
colorSwatchClass: "computedview-colorswatch",
colorClass: "computedview-color",
urlClass: "theme-link",
baseURI: this.selectorInfo.rule.href
});
@ -1540,5 +1582,32 @@ SelectorView.prototype = {
}
};
/**
* Create a child element with a set of attributes.
*
* @param {Element} aParent
* The parent node.
* @param {string} aTag
* The tag name.
* @param {object} aAttributes
* A set of attributes to set on the node.
*/
function createChild(aParent, aTag, aAttributes={}) {
let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
for (let attr in aAttributes) {
if (aAttributes.hasOwnProperty(attr)) {
if (attr === "textContent") {
elt.textContent = aAttributes[attr];
} else if(attr === "child") {
elt.appendChild(aAttributes[attr]);
} else {
elt.setAttribute(attr, aAttributes[attr]);
}
}
}
aParent.appendChild(elt);
return elt;
}
exports.CssHtmlTree = CssHtmlTree;
exports.PropertyView = PropertyView;

View File

@ -72,38 +72,5 @@
&noPropertiesFound;
</div>
<!--
To visually debug the templates without running firefox, alter the display:none
-->
<div style="display:none;">
<!--
A templateMatchedSelectors sits inside each templateProperties showing the
list of selectors that affect that property. Each needs data like this:
{
matchedSelectorViews: ..., // from cssHtmlTree.propertyViews[name].matchedSelectorViews
}
This is a template so the parent does not need to be a table, except that
using a div as the parent causes the DOM to muck with the tr elements
-->
<div id="templateMatchedSelectors">
<loop foreach="selector in ${matchedSelectorViews}">
<p>
<span class="rule-link">
<a target="_blank" class="link theme-link"
onclick="${selector.openStyleEditor}"
onkeydown="${selector.maybeOpenStyleEditor}"
title="${selector.href}"
sourcelocation="${selector.source}"
tabindex="0">${selector.source}</a>
</span>
<span dir="ltr" class="rule-text ${selector.statusClass} theme-fg-color3" title="${selector.statusText}">
${selector.sourceText}
<span class="other-property-value theme-fg-color1">${selector.outputFragment}</span>
</span>
</p>
</loop>
</div>
</div>
</body>
</html>

View File

@ -27,6 +27,7 @@ support-files =
head.js
[browser_computedview_browser-styles.js]
[browser_computedview_cycle_color.js]
[browser_computedview_getNodeInfo.js]
[browser_computedview_keybindings_01.js]
[browser_computedview_keybindings_02.js]
@ -99,6 +100,7 @@ skip-if = (os == "win" && debug) || e10s # bug 963492: win. bug 1040653: e10s.
[browser_ruleview_multiple_properties_01.js]
[browser_ruleview_multiple_properties_02.js]
[browser_ruleview_original-source-link.js]
[browser_ruleview_cycle-color.js]
[browser_ruleview_override.js]
[browser_ruleview_pseudo-element_01.js]
[browser_ruleview_pseudo-element_02.js]

View File

@ -0,0 +1,68 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Computed view color cycling test.
const PAGE_CONTENT = [
"<style type=\"text/css\">",
".matches {color: #F00;}</style>",
"<span id=\"matches\" class=\"matches\">Some styled text</span>",
"</div>"
].join("\n");
add_task(function*() {
yield addTab("data:text/html;charset=utf-8," +
"Computed view color cycling test.");
content.document.body.innerHTML = PAGE_CONTENT;
info("Opening the computed view");
let {toolbox, inspector, view} = yield openComputedView();
info("Selecting the test node");
yield selectNode("#matches", inspector);
info("Checking the property itself");
let container = getComputedViewPropertyView(view, "color").valueNode;
checkColorCycling(container, inspector);
info("Checking matched selectors");
container = yield getComputedViewMatchedRules(view, "color");
checkColorCycling(container, inspector);
});
function checkColorCycling(container, inspector) {
let swatch = container.querySelector(".computedview-colorswatch");
let valueNode = container.querySelector(".computedview-color");
let win = inspector.sidebar.getWindowForTab("computedview");
// Hex (default)
is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
// HSL
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "hsl(0, 100%, 50%)",
"Color displayed as an HSL value.");
// RGB
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value.");
// Color name
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "red",
"Color displayed as a color name.");
// "Authored" (currently the computed value)
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value.");
// Back to hex
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "#F00",
"Color displayed as hex again.");
}

View File

@ -0,0 +1,60 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test cycling color types in the rule view.
const PAGE_CONTENT = [
"<style type=\"text/css\">",
" body {",
" color: #F00;",
" }",
"</style>",
"Test cycling color types in the rule view!"
].join("\n");
add_task(function*() {
yield addTab("data:text/html;charset=utf-8,Test cycling color types in the " +
"rule view.");
content.document.body.innerHTML = PAGE_CONTENT;
let {toolbox, inspector, view} = yield openRuleView();
let container = getRuleViewProperty(view, "body", "color").valueSpan;
checkColorCycling(container, inspector);
});
function checkColorCycling(container, inspector) {
let swatch = container.querySelector(".ruleview-colorswatch");
let valueNode = container.querySelector(".ruleview-color");
let win = inspector.sidebar.getWindowForTab("ruleview");
// Hex (default)
is(valueNode.textContent, "#F00", "Color displayed as a hex value.");
// HSL
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "hsl(0, 100%, 50%)",
"Color displayed as an HSL value.");
// RGB
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value.");
// Color name
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "red",
"Color displayed as a color name.");
// "Authored" (currently the computed value)
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "rgb(255, 0, 0)",
"Color displayed as an RGB value.");
// Back to hex
EventUtils.synthesizeMouseAtCenter(swatch, {type: "mousedown", shiftKey: true}, win);
is(valueNode.textContent, "#F00",
"Color displayed as hex again.");
}

View File

@ -327,7 +327,9 @@
@RESPATH@/components/toolkit_finalizationwitness.xpt
@RESPATH@/components/toolkit_formautofill.xpt
@RESPATH@/components/toolkit_osfile.xpt
#ifdef NIGHTLY_BUILD
@RESPATH@/components/toolkit_perfmonitoring.xpt
#endif
@RESPATH@/components/toolkit_xulstore.xpt
@RESPATH@/components/toolkitprofile.xpt
#ifdef MOZ_ENABLE_XREMOTE

View File

@ -484,8 +484,9 @@ EventStateManager::PreHandleEvent(nsPresContext* aPresContext,
NS_WARN_IF_FALSE(!aTargetFrame ||
!aTargetFrame->GetContent() ||
aTargetFrame->GetContent() == aTargetContent,
"aTargetContent should be related with aTargetFrame");
aTargetFrame->GetContent() == aTargetContent ||
aTargetFrame->GetContent()->GetParent() == aTargetContent,
"aTargetFrame should be related with aTargetContent");
mCurrentTarget = aTargetFrame;
mCurrentTargetContent = nullptr;

View File

@ -7377,9 +7377,10 @@ PresShell::HandleEvent(nsIFrame* aFrame,
}
}
NS_WARN_IF_FALSE(frame, "Nothing to handle this event!");
if (!frame)
if (!frame) {
NS_WARNING("Nothing to handle this event!");
return NS_OK;
}
nsPresContext* framePresContext = frame->PresContext();
nsPresContext* rootPresContext = framePresContext->GetRootPresContext();
@ -7580,11 +7581,18 @@ PresShell::HandleEvent(nsIFrame* aFrame,
if (aEvent->mClass == ePointerEventClass) {
if (WidgetPointerEvent* pointerEvent = aEvent->AsPointerEvent()) {
// Try to keep frame for following check, because
// frame can be damaged during CheckPointerCaptureState.
nsWeakFrame frameKeeper(frame);
// Before any pointer events, we should check state of pointer capture,
// Thus got/lostpointercapture events emulate asynchronous behavior.
// Handlers of got/lostpointercapture events can change capturing state,
// That's why we should re-check pointer capture state until stable state.
while(CheckPointerCaptureState(pointerEvent->pointerId));
// Prevent application crashes, in case damaged frame.
if (!frameKeeper.IsAlive()) {
frame = nullptr;
}
}
}
@ -7626,7 +7634,11 @@ PresShell::HandleEvent(nsIFrame* aFrame,
delete event;
}
}
return NS_OK;
}
if (!frame) {
NS_WARNING("Nothing to handle this event!");
return NS_OK;
}

View File

@ -0,0 +1,72 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1153130
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1153130</title>
<meta name="author" content="Maksim Lebedev" />
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<style>
#target { background: yellow; padding: 10px; }
</style>
<script type="application/javascript">
var target = undefined;
var test_down = false;
var test_capture = false;
var test_move = false;
var test_success = false;
function TargetHandler(event) {
logger("Target receive event: " + event.type);
if(event.type == "pointerdown") {
test_down = true;
target.setPointerCapture(event.pointerId);
} else if(event.type == "gotpointercapture") {
test_capture = true;
} else if(event.type == "pointermove" && test_capture) {
test_move = true;
}
}
function logger(message) {
console.log(message);
var log = document.getElementById('target');
log.innerHTML = message + "<br>" + log.innerHTML;
}
function prepareTest() {
parent.turnOnPointerEvents(executeTest);
}
function executeTest() {
logger("executeTest");
target = document.getElementById("target");
target.addEventListener("pointerdown", TargetHandler, false);
target.addEventListener("gotpointercapture", TargetHandler, false);
target.addEventListener("pointermove", TargetHandler, false);
var rect = target.getBoundingClientRect();
synthesizePointer(target, rect.width/5, rect.height/5, {type: "pointermove"});
synthesizePointer(target, rect.width/5, rect.height/5, {type: "pointerdown"});
synthesizePointer(target, rect.width/4, rect.height/4, {type: "pointermove"});
synthesizePointer(target, rect.width/3, rect.height/3, {type: "pointermove"});
synthesizePointer(target, rect.width/3, rect.height/3, {type: "pointerup"});
synthesizePointer(target, rect.width/2, rect.height/2, {type: "pointermove"});
test_success = true;
finishTest();
}
function finishTest() {
parent.is(test_down, true, "pointerdown event should be received by target");
parent.is(test_capture, true, "gotpointercapture event should be received by target");
parent.is(test_move, true, "pointermove event should be received by target while pointer capture is active");
parent.is(test_success, true, "Firefox should be live!");
logger("finishTest");
parent.finishTest();
}
</script>
</head>
<body onload="prepareTest()">
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1153130">Mozilla Bug 1153130</a>
<div id="target">div id=target</div>
</body>
</html>

View File

@ -261,3 +261,5 @@ support-files = bug1080361_inner.html
support-files = bug1093686_inner.html
[test_bug1120705.html]
skip-if = buildapp == 'android' || buildapp == 'b2g' || buildapp == 'b2g-debug' || os == 'mac' # android and b2g do not have clickable scrollbars, mac does not have scrollbar down and up buttons
[test_bug1153130.html]
support-files = bug1153130_inner.html

View File

@ -0,0 +1,37 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1153130
-->
<head>
<meta charset="utf-8">
<title>Test for Bug 1153130</title>
<meta name="author" content="Maksim Lebedev" />
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
<script type="text/javascript">
var iframe = undefined;
function prepareTest() {
SimpleTest.waitForExplicitFinish();
iframe = document.getElementById("testFrame");
turnOnPointerEvents(startTest);
}
function turnOnPointerEvents(callback) {
SpecialPowers.pushPrefEnv({
"set": [
["dom.w3c_pointer_events.enabled", true]
]
}, callback);
}
function startTest() {
iframe.src = "bug1153130_inner.html";
}
function finishTest() {
SimpleTest.finish();
}
</script>
</head>
<body onload="prepareTest()">
<iframe id="testFrame" height="700" width="700"></iframe>
</body>
</html>

View File

@ -114,7 +114,6 @@
to. -->
<!ENTITY overlay_share_send_tab_btn_label "Send to another device">
<!ENTITY overlay_share_no_url "No link found in this share">
<!ENTITY overlay_share_retry "Try again">
<!ENTITY overlay_share_select_device "Select device">
<!-- Localization note (overlay_no_synced_devices) : Used when the menu option
to send a tab to a synced device is pressed and no other synced devices

View File

@ -117,7 +117,6 @@
<string name="overlay_share_reading_list_btn_label_already">&overlay_share_reading_list_btn_label_already;</string>
<string name="overlay_share_send_tab_btn_label">&overlay_share_send_tab_btn_label;</string>
<string name="overlay_share_no_url">&overlay_share_no_url;</string>
<string name="overlay_share_retry">&overlay_share_retry;</string>
<string name="overlay_share_select_device">&overlay_share_select_device;</string>
<string name="overlay_no_synced_devices">&overlay_no_synced_devices;</string>
<string name="overlay_share_tab_not_sent">&overlay_share_tab_not_sent;</string>

View File

@ -253,7 +253,9 @@
@BINPATH@/components/toolkit_finalizationwitness.xpt
@BINPATH@/components/toolkit_formautofill.xpt
@BINPATH@/components/toolkit_osfile.xpt
#ifdef NIGHTLY_BUILD
@BINPATH@/components/toolkit_perfmonitoring.xpt
#endif
@BINPATH@/components/toolkit_xulstore.xpt
@BINPATH@/components/toolkitprofile.xpt
#ifdef MOZ_ENABLE_XREMOTE

View File

@ -912,9 +912,9 @@ function testScope(aTester, aTest, expected) {
var self = this;
this.ok = function test_ok(condition, name, diag, stack) {
if (this.__expected == 'fail') {
if (self.__expected == 'fail') {
if (!condition) {
this.__num_failed++;
self.__num_failed++;
condition = true;
}
}

View File

@ -9,6 +9,7 @@
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { AddonManager } = Cu.import("resource://gre/modules/AddonManager.jsm", {});
const { AddonWatcher } = Cu.import("resource://gre/modules/AddonWatcher.jsm", {});
const { PerformanceStats } = Cu.import("resource://gre/modules/PerformanceStats.jsm", {});
/**
@ -84,8 +85,93 @@ let State = {
function update() {
updateLiveData();
updateSlowAddons();
}
/**
* Update the list of slow addons
*/
function updateSlowAddons() {
try {
let dataElt = document.getElementById("data");
let data = AddonWatcher.alerts;
if (data.size == 0) {
// Nothing to display.
return;
}
let alerts = 0;
for (let [addonId, details] of data) {
for (let k of Object.keys(details.alerts)) {
alerts += details.alerts[k];
}
}
if (!alerts) {
// Still nothing to display.
return;
}
let elData = document.getElementById("slowAddonsList");
elData.innerHTML = "";
let elTable = document.createElement("table");
elData.appendChild(elTable);
// Generate header
let elHeader = document.createElement("tr");
elTable.appendChild(elHeader);
for (let name of [
"Alerts",
"Jank level alerts",
"(highest jank)",
"Cross-Process alerts",
"(highest CPOW)"
]) {
let elName = document.createElement("td");
elName.textContent = name;
elHeader.appendChild(elName);
elName.classList.add("header");
}
for (let [addonId, details] of data) {
let elAddon = document.createElement("tr");
// Display the number of occurrences of each alerts
let elTotal = document.createElement("td");
let total = 0;
for (let k of Object.keys(details.alerts)) {
total += details.alerts[k];
}
elTotal.textContent = total;
elAddon.appendChild(elTotal);
for (let filter of ["longestDuration", "totalCPOWTime"]) {
for (let stat of ["alerts", "peaks"]) {
let el = document.createElement("td");
el.textContent = details[stat][filter] || 0;
elAddon.appendChild(el);
}
}
// Display the name of the add-on
let elName = document.createElement("td");
elAddon.appendChild(elName);
AddonManager.getAddonByID(addonId, a => {
elName.textContent = a ? a.name : addonId
});
elTable.appendChild(elAddon);
}
} catch (ex) {
console.error(ex);
}
}
/**
* Update the table of live data.
*/
function updateLiveData() {
try {
let dataElt = document.getElementById("liveData");
dataElt.innerHTML = "";
// Generate table headers
@ -137,7 +223,7 @@ function update() {
let _el = el;
let _item = item;
AddonManager.getAddonByID(item.addonId, a => {
_el.textContent = a?a.name:_item.name
_el.textContent = a ? a.name : _item.name
});
} else {
el.textContent = item.name;

View File

@ -85,7 +85,15 @@
</style>
</head>
<body onload="go()">
<table id="data">
<h1>Performance monitor</h1>
<table id="liveData">
</table>
<h1>Slow add-ons alerts</h1>
<div id="slowAddonsList">
(none)
</div>
</body>
</html>

View File

@ -16,7 +16,7 @@ function frameScript() {
let hasURL = false;
try {
let eltData = content.document.getElementById("data");
let eltData = content.document.getElementById("liveData");
if (!eltData) {
return;
}

View File

@ -21,7 +21,6 @@ LOCAL_INCLUDES += [
'../feeds',
'../find',
'../jsdownloads/src',
'../perfmonitoring',
'../protobuf',
'../startup',
'../statusfilter',
@ -34,4 +33,9 @@ if not CONFIG['MOZ_DISABLE_PARENTAL_CONTROLS']:
'../parentalcontrols',
]
if CONFIG['NIGHTLY_BUILD']:
LOCAL_INCLUDES += [
'../perfmonitoring',
]
FAIL_ON_WARNINGS = True

View File

@ -51,7 +51,13 @@
#include "nsTerminator.h"
#endif
#if defined(NIGHTLY_BUILD)
#define MOZ_HAS_PERFSTATS
#endif // defined(NIGHTLY_BUILD)
#if defined(MOZ_HAS_PERFSTATS)
#include "nsPerformanceStats.h"
#endif // defined (MOZ_HAS_PERFSTATS)
using namespace mozilla;
@ -59,7 +65,9 @@ using namespace mozilla;
NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(nsAppStartup, Init)
#if defined(MOZ_HAS_PERFSTATS)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsPerformanceStatsService)
#endif // defined (MOZ_HAS_PERFSTATS)
#if defined(MOZ_HAS_TERMINATOR)
NS_GENERIC_FACTORY_CONSTRUCTOR(nsTerminator)
@ -119,7 +127,10 @@ NS_GENERIC_FACTORY_CONSTRUCTOR_INIT(NativeFileWatcherService, Init)
NS_GENERIC_FACTORY_SINGLETON_CONSTRUCTOR(AddonPathService, AddonPathService::GetInstance)
NS_DEFINE_NAMED_CID(NS_TOOLKIT_APPSTARTUP_CID);
#if defined(MOZ_HAS_PERFSTATS)
NS_DEFINE_NAMED_CID(NS_TOOLKIT_PERFORMANCESTATSSERVICE_CID);
#endif // defined (MOZ_HAS_PERFSTATS)
#if defined(MOZ_HAS_TERMINATOR)
NS_DEFINE_NAMED_CID(NS_TOOLKIT_TERMINATOR_CID);
#endif
@ -154,7 +165,9 @@ static const Module::CIDEntry kToolkitCIDs[] = {
#if defined(MOZ_HAS_TERMINATOR)
{ &kNS_TOOLKIT_TERMINATOR_CID, false, nullptr, nsTerminatorConstructor },
#endif
#if defined(MOZ_HAS_PERFSTATS)
{ &kNS_TOOLKIT_PERFORMANCESTATSSERVICE_CID, false, nullptr, nsPerformanceStatsServiceConstructor },
#endif // defined (MOZ_HAS_PERFSTATS)
{ &kNS_USERINFO_CID, false, nullptr, nsUserInfoConstructor },
{ &kNS_ALERTSSERVICE_CID, false, nullptr, nsAlertsServiceConstructor },
#if !defined(MOZ_DISABLE_PARENTAL_CONTROLS)
@ -188,7 +201,9 @@ static const Module::ContractIDEntry kToolkitContracts[] = {
#if defined(MOZ_HAS_TERMINATOR)
{ NS_TOOLKIT_TERMINATOR_CONTRACTID, &kNS_TOOLKIT_TERMINATOR_CID },
#endif
#if defined(MOZ_HAS_PERFSTATS)
{ NS_TOOLKIT_PERFORMANCESTATSSERVICE_CONTRACTID, &kNS_TOOLKIT_PERFORMANCESTATSSERVICE_CID },
#endif // defined (MOZ_HAS_PERFSTATS)
{ NS_USERINFO_CONTRACTID, &kNS_USERINFO_CID },
{ NS_ALERTSERVICE_CONTRACTID, &kNS_ALERTSSERVICE_CID },
#if !defined(MOZ_DISABLE_PARENTAL_CONTROLS)

View File

@ -11,7 +11,6 @@ if CONFIG['MOZ_ENABLE_XREMOTE']:
DIRS += [
'aboutcache',
'aboutmemory',
'aboutperformance',
'addoncompat',
'alerts',
'apppicker',
@ -36,7 +35,6 @@ DIRS += [
'parentalcontrols',
'passwordmgr',
'perf',
'perfmonitoring',
'places',
'processsingleton',
'promiseworker',
@ -92,6 +90,12 @@ if CONFIG['MOZ_CAPTIVEDETECT']:
if CONFIG['MOZ_WIDGET_TOOLKIT'] != "gonk" and CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
DIRS += ['terminator']
if CONFIG['NIGHTLY_BUILD']: # Bug 1136927 - Performance Monitoring is not ready for prime-time yet
DIRS += [
'aboutperformance',
'perfmonitoring',
]
DIRS += ['build']
EXTRA_COMPONENTS += [

View File

@ -23,8 +23,16 @@ XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
const FILTERS = ["longestDuration", "totalCPOWTime"];
let AddonWatcher = {
_previousPerformanceIndicators: {},
/**
* Stats, designed to be consumed by clients of AddonWatcher.
*
*/
_stats: new Map(),
_timer: Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer),
_callback: null,
/**
@ -161,24 +169,36 @@ let AddonWatcher = {
add(addonId, diff.totalCPOWTime / 1000);
}
// Report mibehaviors to the user.
let reason = null;
// Store misbehaviors for about:performance and other clients
for (let k of ["longestDuration", "totalCPOWTime"]) {
if (limits[k] > 0 && diff[k] > limits[k]) {
reason = k;
let stats = this._stats.get(addonId);
if (!stats) {
stats = {
peaks: {},
alerts: {},
};
this._stats.set(addonId, stats);
}
// Report misbehaviors to the user.
for (let filter of FILTERS) {
dump(`Checking addon ${addonId} with filter ${filter}\n`);
let peak = stats.peaks[filter] || 0;
stats.peaks[filter] = Math.max(diff[filter], peak);
if (limits[filter] <= 0 || diff[filter] <= limits[filter]) {
continue;
}
}
if (!reason) {
continue;
}
stats.alerts[filter] = (stats.alerts[filter] || 0) + 1;
try {
this._callback(addonId, reason);
} catch (ex) {
Cu.reportError("Error in AddonWatcher._checkAddons callback " + ex);
Cu.reportError(ex.stack);
try {
this._callback(addonId, filter);
} catch (ex) {
Cu.reportError("Error in AddonWatcher._checkAddons callback " + ex);
Cu.reportError(ex.stack);
}
}
}
} catch (ex) {
@ -200,5 +220,24 @@ let AddonWatcher = {
} catch (ex) {
Preferences.set("browser.addon-watch.ignore", JSON.stringify([addonid]));
}
}
},
/**
* The list of alerts for this session.
*
* @type {Map<String, Object>} A map associating addonId to
* objects with fields
* - {Object} peaks The highest values encountered for each filter.
* - {number} longestDuration
* - {number} totalCPOWTime
* - {Object} alerts The number of alerts for each filter.
* - {number} longestDuration
* - {number} totalCPOWTime
*/
get alerts() {
let result = new Map();
for (let [k, v] of this._stats) {
result.set(k, Cu.cloneInto(v, this));
}
return result;
},
};

View File

@ -58,7 +58,9 @@ let burn_rubber = Task.async(function*({histogramName, topic, expectedReason, pr
info("Preparing add-on watcher");
let wait = new Promise(resolve => AddonWatcher.init((id, reason) => {
Assert.equal(id, ADDON_ID, "The add-on watcher has detected the misbehaving addon");
resolve(reason);
if (reason == expectedReason) {
resolve(reason);
}
}));
let done = false;
wait = wait.then(result => {

View File

@ -32,9 +32,32 @@ function frameScript() {
});
}
function Assert_leq(a, b, msg) {
Assert.ok(a <= b, `${msg}: ${a} <= ${b}`);
}
// A variant of `Assert` that doesn't spam the logs
// in case of success.
let SilentAssert = {
equal: function(a, b, msg) {
if (a == b) {
return;
}
Assert.equal(a, b, msg);
},
notEqual: function(a, b, msg) {
if (a != b) {
return;
}
Assert.notEqual(a, b, msg);
},
ok: function(a, msg) {
if (a) {
return;
}
Assert.ok(a, msg);
},
leq: function(a, b, msg) {
this.ok(a <= b, `${msg}: ${a} <= ${b}`);
}
};
function monotinicity_tester(source, testName) {
// In the background, check invariants:
@ -54,22 +77,22 @@ function monotinicity_tester(source, testName) {
return;
}
for (let k of ["name", "addonId", "isSystem"]) {
Assert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed.`);
SilentAssert.equal(prev[k], next[k], `Sanity check (${testName}): ${k} hasn't changed.`);
}
for (let k of ["totalUserTime", "totalSystemTime", "totalCPOWTime", "ticks"]) {
Assert.equal(typeof next[k], "number", `Sanity check (${testName}): ${k} is a number.`);
Assert_leq(prev[k], next[k], `Sanity check (${testName}): ${k} is monotonic.`);
Assert_leq(0, next[k], `Sanity check (${testName}): ${k} is >= 0.`)
SilentAssert.equal(typeof next[k], "number", `Sanity check (${testName}): ${k} is a number.`);
SilentAssert.leq(prev[k], next[k], `Sanity check (${testName}): ${k} is monotonic.`);
SilentAssert.leq(0, next[k], `Sanity check (${testName}): ${k} is >= 0.`)
}
Assert.equal(prev.durations.length, next.durations.length);
SilentAssert.equal(prev.durations.length, next.durations.length);
for (let i = 0; i < next.durations.length; ++i) {
Assert.ok(typeof next.durations[i] == "number" && next.durations[i] >= 0,
SilentAssert.ok(typeof next.durations[i] == "number" && next.durations[i] >= 0,
`Sanity check (${testName}): durations[${i}] is a non-negative number.`);
Assert_leq(prev.durations[i], next.durations[i],
SilentAssert.leq(prev.durations[i], next.durations[i],
`Sanity check (${testName}): durations[${i}] is monotonic.`)
}
for (let i = 0; i < next.durations.length - 1; ++i) {
Assert_leq(next.durations[i + 1], next.durations[i],
SilentAssert.leq(next.durations[i + 1], next.durations[i],
`Sanity check (${testName}): durations[${i}] >= durations[${i + 1}].`)
}
};
@ -86,9 +109,9 @@ function monotinicity_tester(source, testName) {
// Sanity check on the process data.
sanityCheck(previous.processData, snapshot.processData);
Assert.equal(snapshot.processData.isSystem, true);
Assert.equal(snapshot.processData.name, "<process>");
Assert.equal(snapshot.processData.addonId, "");
SilentAssert.equal(snapshot.processData.isSystem, true);
SilentAssert.equal(snapshot.processData.name, "<process>");
SilentAssert.equal(snapshot.processData.addonId, "");
previous.procesData = snapshot.processData;
// Sanity check on components data.
@ -102,17 +125,13 @@ function monotinicity_tester(source, testName) {
previous.componentsMap.set(key, item);
for (let k of ["totalUserTime", "totalSystemTime", "totalCPOWTime"]) {
Assert_leq(item[k], snapshot.processData[k],
SilentAssert.leq(item[k], snapshot.processData[k],
`Sanity check (${testName}): component has a lower ${k} than process`);
}
}
// Check that we do not have duplicate components.
info(`Before deduplication, we had the following components: ${keys.sort().join(", ")}`);
info(`After deduplication, we have the following components: ${[...set.keys()].sort().join(", ")}`);
info(`Deactivating deduplication check (Bug 1150045)`);
if (false) {
Assert.equal(set.size, snapshot.componentsData.length);
SilentAssert.equal(set.size, snapshot.componentsData.length);
}
});
let interval = window.setInterval(frameCheck, 300);

View File

@ -461,8 +461,7 @@ BookmarkImporter.prototype = {
this._importPromises.push(kwPromise);
}
if (aData.tags) {
// TODO (bug 967196) the tagging service should trim by itself.
let tags = aData.tags.split(",").map(tag => tag.trim());
let tags = aData.tags.split(",");
if (tags.length)
PlacesUtils.tagging.tagURI(NetUtil.newURI(aData.uri), tags);
}

View File

@ -10,6 +10,8 @@ const Cr = Components.results;
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
const TOPIC_SHUTDOWN = "places-shutdown";
@ -98,12 +100,14 @@ TaggingService.prototype = {
*
* @param aTags
* Array of tags. Entries can be tag names or concrete item id.
* @param trim [optional]
* Whether to trim passed-in named tags. Defaults to false.
* @return Array of tag objects like { id: number, name: string }.
*
* @throws Cr.NS_ERROR_INVALID_ARG if any element of the input array is not
* a valid tag.
*/
_convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags)
_convertInputMixedTagsArray: function TS__convertInputMixedTagsArray(aTags, trim=false)
{
return aTags.map(function (val)
{
@ -117,7 +121,7 @@ TaggingService.prototype = {
}
else if (typeof(val) == "string" && val.length > 0 && val.length <= Ci.nsITaggingService.MAX_TAG_LENGTH) {
// This is a tag name.
tag.name = val;
tag.name = trim ? val.trim() : val;
// We can't know the id at this point, since a previous tag could
// have created it.
tag.__defineGetter__("id", function () this._self._getItemIdForTag(this.name));
@ -137,7 +141,7 @@ TaggingService.prototype = {
}
// This also does some input validation.
let tags = this._convertInputMixedTagsArray(aTags);
let tags = this._convertInputMixedTagsArray(aTags, true);
let taggingService = this;
PlacesUtils.bookmarks.runInBatchMode({
@ -215,6 +219,12 @@ TaggingService.prototype = {
// This also does some input validation.
let tags = this._convertInputMixedTagsArray(aTags);
let isAnyTagNotTrimmed = tags.some(tag => /^\s|\s$/.test(tag.name));
if (isAnyTagNotTrimmed) {
Deprecated.warning("At least one tag passed to untagURI was not trimmed",
"https://bugzilla.mozilla.org/show_bug.cgi?id=967196");
}
let taggingService = this;
PlacesUtils.bookmarks.runInBatchMode({
runBatched: function (aUserData)
@ -239,6 +249,11 @@ TaggingService.prototype = {
if (!aTagName || aTagName.length == 0)
throw Cr.NS_ERROR_INVALID_ARG;
if (/^\s|\s$/.test(aTagName)) {
Deprecated.warning("Tag passed to getURIsForTag was not trimmed",
"https://bugzilla.mozilla.org/show_bug.cgi?id=967196");
}
let uris = [];
let tagId = this._getItemIdForTag(aTagName);
if (tagId == -1)

View File

@ -175,4 +175,16 @@ function run_test() {
// cleanup
tagRoot.containerOpen = false;
// Tagging service should trim tags (Bug967196)
let exampleURI = uri("http://www.example.com/");
PlacesUtils.tagging.tagURI(exampleURI, [ " test " ]);
let exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI);
do_check_eq(exampleTags.length, 1);
do_check_eq(exampleTags[0], "test");
PlacesUtils.tagging.untagURI(exampleURI, [ "test" ]);
exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI);
do_check_eq(exampleTags.length, 0);
}

View File

@ -32,7 +32,7 @@ const MAX_PING_FILE_AGE = 14 * 24 * 60 * 60 * 1000; // 2 weeks
const OVERDUE_PING_FILE_AGE = 7 * 24 * 60 * 60 * 1000; // 1 week
// Maximum number of pings to save.
const MAX_LRU_PINGS = 17;
const MAX_LRU_PINGS = 50;
// The number of outstanding saved pings that we have issued loading
// requests for.

View File

@ -1,166 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/ThirdPartyCookieProbe.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
let TOPIC_ACCEPTED = "third-party-cookie-accepted";
let TOPIC_REJECTED = "third-party-cookie-rejected";
let FLUSH_MILLISECONDS = 1000 * 60 * 60 * 24 / 2; /*Half a day, for testing purposes*/
const NUMBER_OF_REJECTS = 30;
const NUMBER_OF_ACCEPTS = 17;
const NUMBER_OF_REPEATS = 5;
let gCookieService;
let gThirdPartyCookieProbe;
let gHistograms = {
clear: function() {
this.sitesAccepted.clear();
this.requestsAccepted.clear();
this.sitesRejected.clear();
this.requestsRejected.clear();
}
};
function run_test() {
do_print("Initializing environment");
do_get_profile();
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
gCookieService = Cc["@mozilla.org/cookieService;1"].getService(Ci.nsICookieService);
do_print("Initializing ThirdPartyCookieProbe.jsm");
gThirdPartyCookieProbe = new ThirdPartyCookieProbe();
gThirdPartyCookieProbe.init();
do_print("Acquiring histograms");
gHistograms.sitesAccepted =
Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_SITES_ACCEPTED");
gHistograms.sitesRejected =
Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_SITES_BLOCKED"),
gHistograms.requestsAccepted =
Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_ATTEMPTS_ACCEPTED");
gHistograms.requestsRejected =
Services.telemetry.getHistogramById("COOKIES_3RDPARTY_NUM_ATTEMPTS_BLOCKED"),
run_next_test();
}
/**
* Utility function: try to set a cookie with the given document uri and referrer uri.
*
* @param obj An object with the following fields
* - {string} request The uri of the request setting the cookie.
* - {string} referrer The uri of the referrer for this request.
*/
function tryToSetCookie(obj) {
let requestURI = Services.io.newURI(obj.request, null, null);
let referrerURI = Services.io.newURI(obj.referrer, null, null);
let requestChannel = Services.io.newChannelFromURI2(requestURI,
null, // aLoadingNode
Services.scriptSecurityManager.getSystemPrincipal(),
null, // aTriggeringPrincipal
Ci.nsILoadInfo.SEC_NORMAL,
Ci.nsIContentPolicy.TYPE_OTHER);
gCookieService.setCookieString(referrerURI, null, "Is there a cookie in my jar?", requestChannel);
}
function wait(ms) {
let deferred = Promise.defer();
do_timeout(ms, () => deferred.resolve());
return deferred.promise;
}
function oneTest(tld, flushUptime, check) {
gHistograms.clear();
do_print("Testing with tld " + tld);
do_print("Adding rejected entries");
Services.prefs.setIntPref("network.cookie.cookieBehavior",
1 /*reject third-party cookies*/);
for (let i = 0; i < NUMBER_OF_REJECTS; ++i) {
for (let j = 0; j < NUMBER_OF_REPEATS; ++j) {
for (let prefix of ["http://", "https://"]) {
// Histogram sitesRejected should only count
// NUMBER_OF_REJECTS entries.
// Histogram requestsRejected should count
// NUMBER_OF_REJECTS * NUMBER_OF_REPEATS * 2
tryToSetCookie({
request: prefix + "echelon" + tld,
referrer: prefix + "domain" + i + tld
});
}
}
}
do_print("Adding accepted entries");
Services.prefs.setIntPref("network.cookie.cookieBehavior",
0 /*accept third-party cookies*/);
for (let i = 0; i < NUMBER_OF_ACCEPTS; ++i) {
for (let j = 0; j < NUMBER_OF_REPEATS; ++j) {
for (let prefix of ["http://", "https://"]) {
// Histogram sitesAccepted should only count
// NUMBER_OF_ACCEPTS entries.
// Histogram requestsAccepted should count
// NUMBER_OF_ACCEPTS * NUMBER_OF_REPEATS * 2
tryToSetCookie({
request: prefix + "prism" + tld,
referrer: prefix + "domain" + i + tld
});
}
}
}
do_print("Checking that the histograms have not changed before ping()");
do_check_eq(gHistograms.sitesAccepted.snapshot().sum, 0);
do_check_eq(gHistograms.sitesRejected.snapshot().sum, 0);
do_check_eq(gHistograms.requestsAccepted.snapshot().sum, 0);
do_check_eq(gHistograms.requestsRejected.snapshot().sum, 0);
do_print("Checking that the resulting histograms are correct");
if (flushUptime != null) {
let now = Date.now();
let before = now - flushUptime;
gThirdPartyCookieProbe._latestFlush = before;
gThirdPartyCookieProbe.flush(now);
} else {
gThirdPartyCookieProbe.flush();
}
check();
}
add_task(function() {
// To ensure that we work correctly with eTLD, test with several suffixes
for (let tld of [".com", ".com.ar", ".co.uk", ".gouv.fr"]) {
oneTest(tld, FLUSH_MILLISECONDS, function() {
do_check_eq(gHistograms.sitesAccepted.snapshot().sum, NUMBER_OF_ACCEPTS * 2);
do_check_eq(gHistograms.sitesRejected.snapshot().sum, NUMBER_OF_REJECTS * 2);
do_check_eq(gHistograms.requestsAccepted.snapshot().sum, NUMBER_OF_ACCEPTS * NUMBER_OF_REPEATS * 2 * 2);
do_check_eq(gHistograms.requestsRejected.snapshot().sum, NUMBER_OF_REJECTS * NUMBER_OF_REPEATS * 2 * 2);
});
}
// Check that things still work with default uptime management
for (let tld of [".com", ".com.ar", ".co.uk", ".gouv.fr"]) {
yield wait(1000); // Ensure that uptime is at least one second
oneTest(tld, null, function() {
do_check_true(gHistograms.sitesAccepted.snapshot().sum > 0);
do_check_true(gHistograms.sitesRejected.snapshot().sum > 0);
do_check_true(gHistograms.requestsAccepted.snapshot().sum > 0);
do_check_true(gHistograms.requestsRejected.snapshot().sum > 0);
});
}
});
add_task(function() {
gThirdPartyCookieProbe.dispose();
});

View File

@ -38,13 +38,10 @@ skip-if = android_version == "18"
[test_TelemetryPingBuildID.js]
# Bug 1144395: crash on Android 4.3
skip-if = android_version == "18"
[test_ThirdPartyCookieProbe.js]
skip-if = true # Bug 1149284
[test_TelemetrySendOldPings.js]
skip-if = debug == true || os == "android" # Disabled due to intermittent orange on Android
[test_TelemetrySession.js]
# Bug 1144395: crash on Android 4.3
#skip-if = android_version == "18"
skip-if = true # Bug 1149284
skip-if = android_version == "18"
[test_ThreadHangStats.js]
run-sequentially = Bug 1046307, test can fail intermittently when CPU load is high

View File

@ -95,8 +95,22 @@ CssColor.COLORUNIT = {
};
CssColor.prototype = {
_colorUnit: null,
authored: null,
get colorUnit() {
if (this._colorUnit === null) {
let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
this._colorUnit = CssColor.COLORUNIT[defaultUnit];
}
return this._colorUnit;
},
set colorUnit(unit) {
this._colorUnit = unit;
},
get hasAlpha() {
if (!this.valid) {
return false;
@ -269,15 +283,31 @@ CssColor.prototype = {
return this;
},
nextColorUnit: function() {
// Reorder the formats array to have the current format at the
// front so we can cycle through.
let formats = ["authored", "hex", "hsl", "rgb", "name"];
let putOnEnd = formats.splice(0, formats.indexOf(this.colorUnit));
formats = formats.concat(putOnEnd);
let currentDisplayedColor = this[formats[0]];
for (let format of formats) {
if (this[format].toLowerCase() !== currentDisplayedColor.toLowerCase()) {
this.colorUnit = CssColor.COLORUNIT[format];
break;
}
}
return this.toString();
},
/**
* Return a string representing a color of type defined in COLOR_UNIT_PREF.
*/
toString: function() {
let color;
let defaultUnit = Services.prefs.getCharPref(COLOR_UNIT_PREF);
let unit = CssColor.COLORUNIT[defaultUnit];
switch(unit) {
switch(this.colorUnit) {
case CssColor.COLORUNIT.authored:
color = this.authored;
break;

View File

@ -77,6 +77,8 @@ loader.lazyGetter(this, "REGEX_ALL_CSS_PROPERTIES", function () {
*/
function OutputParser() {
this.parsed = [];
this.colorSwatches = new WeakMap();
this._onSwatchMouseDown = this._onSwatchMouseDown.bind(this);
}
exports.OutputParser = OutputParser;
@ -396,12 +398,14 @@ OutputParser.prototype = {
class: options.colorSwatchClass,
style: "background-color:" + color
});
this.colorSwatches.set(swatch, colorObj);
swatch.addEventListener("mousedown", this._onSwatchMouseDown, false);
container.appendChild(swatch);
}
if (options.defaultColorType) {
color = colorObj.toString();
container.dataset["color"] = color;
container.dataset.color = color;
}
let value = this._createNode("span", {
@ -435,6 +439,21 @@ OutputParser.prototype = {
this.parsed.push(container);
},
_onSwatchMouseDown: function(event) {
// Prevent text selection in the case of shift-click or double-click.
event.preventDefault();
if (!event.shiftKey) {
return;
}
let swatch = event.target;
let color = this.colorSwatches.get(swatch);
let val = color.nextColorUnit();
swatch.nextElementSibling.textContent = val;
},
/**
* Append a URL to the output.
*

View File

@ -21,10 +21,14 @@ let { TabActor } = require("devtools/server/actors/webbrowser");
* The conection to the client.
* @param chromeGlobal
* The content script global holding |content| and |docShell| properties for a tab.
* @param prefix
* the prefix used in protocol to create IDs for each actor.
* Used as ID identifying this particular TabActor from the parent process.
*/
function ContentActor(connection, chromeGlobal)
function ContentActor(connection, chromeGlobal, prefix)
{
this._chromeGlobal = chromeGlobal;
this._prefix = prefix;
TabActor.call(this, connection, chromeGlobal);
this.traits.reconfigure = false;
this._sendForm = this._sendForm.bind(this);
@ -49,32 +53,11 @@ Object.defineProperty(ContentActor.prototype, "title", {
});
ContentActor.prototype.exit = function() {
this._chromeGlobal.removeMessageListener("debug:form", this._sendForm);
this._sendForm = null;
TabActor.prototype.exit.call(this);
};
// Override form just to rename this._tabActorPool to this._tabActorPool2
// in order to prevent it to be cleaned on detach.
// We have to keep tab actors alive as we keep the ContentActor
// alive after detach and reuse it for multiple debug sessions.
ContentActor.prototype.form = function () {
let response = {
"actor": this.actorID,
"title": this.title,
"url": this.url
};
// Walk over tab actors added by extensions and add them to a new ActorPool.
let actorPool = new ActorPool(this.conn);
this._createExtraActors(DebuggerServer.tabActorFactories, actorPool);
if (!actorPool.isEmpty()) {
this._tabActorPool2 = actorPool;
this.conn.addActorPool(this._tabActorPool2);
if (this._sendForm) {
this._chromeGlobal.removeMessageListener("debug:form", this._sendForm);
this._sendForm = null;
}
this._appendExtraActors(response);
return response;
return TabActor.prototype.exit.call(this);
};
/**

View File

@ -111,7 +111,7 @@ const DirectorRegistry = exports.DirectorRegistry = {
let gTrackedMessageManager = new Set();
exports.setupParentProcess = function setupParentProcess({mm, childID}) {
exports.setupParentProcess = function setupParentProcess({mm, prefix}) {
// prevents multiple subscriptions on the same messagemanager
if (gTrackedMessageManager.has(mm)) {
return;
@ -121,7 +121,7 @@ exports.setupParentProcess = function setupParentProcess({mm, childID}) {
// listen for director-script requests from the child process
mm.addMessageListener("debug:director-registry-request", handleChildRequest);
DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected);
DebuggerServer.once("disconnected-from-child:" + prefix, handleMessageManagerDisconnected);
/* parent process helpers */

View File

@ -1917,7 +1917,7 @@ var WalkerActor = protocol.ActorClass({
sugs.classes.delete(HIDDEN_CLASS);
for (let [className, count] of sugs.classes) {
if (className.startsWith(completing)) {
result.push(["." + className, count, selectorState]);
result.push(["." + CSS.escape(className), count, selectorState]);
}
}
break;
@ -1934,7 +1934,7 @@ var WalkerActor = protocol.ActorClass({
}
for (let [id, count] of sugs.ids) {
if (id.startsWith(completing)) {
result.push(["#" + id, count, selectorState]);
result.push(["#" + CSS.escape(id), count, selectorState]);
}
}
break;

View File

@ -196,6 +196,11 @@ RootActor.prototype = {
this._parameters.onShutdown();
}
this._extraActors = null;
this.conn = null;
this._tabActorPool = null;
this._globalActorPool = null;
this._parameters = null;
this._chromeActor = null;
},
/* The 'listTabs' request and the 'tabListChanged' notification. */

View File

@ -213,9 +213,9 @@ function WebappsActor(aConnection) {
Cu.import("resource://gre/modules/AppsUtils.jsm");
Cu.import("resource://gre/modules/FileUtils.jsm");
// Keep reference of already created app actors.
// key: app frame message manager, value: ContentActor's grip() value
this._appActorsMap = new Map();
// Keep reference of already connected app processes.
// values: app frame message manager
this._connectedApps = new Set();
this.conn = aConnection;
this._uploads = [];
@ -960,24 +960,33 @@ WebappsActor.prototype = {
// Only create a new actor, if we haven't already
// instanciated one for this connection.
let map = this._appActorsMap;
let set = this._connectedApps;
let mm = appFrame.QueryInterface(Ci.nsIFrameLoaderOwner)
.frameLoader
.messageManager;
let actor = map.get(mm);
if (!actor) {
if (!set.has(mm)) {
let onConnect = actor => {
map.set(mm, actor);
set.add(mm);
return { actor: actor };
};
let onDisconnect = mm => {
map.delete(mm);
set.delete(mm);
};
return DebuggerServer.connectToChild(this.conn, appFrame, onDisconnect)
.then(onConnect);
}
return { actor: actor };
// We have to update the form as it may have changed
// if we detached the TabActor
let deferred = promise.defer();
let onFormUpdate = msg => {
mm.removeMessageListener("debug:form", onFormUpdate);
deferred.resolve({ actor: msg.json });
};
mm.addMessageListener("debug:form", onFormUpdate);
mm.sendAsyncMessage("debug:form");
return deferred.promise;
},
watchApps: function () {

View File

@ -876,10 +876,7 @@ TabActor.prototype = {
* Called when the actor is removed from the connection.
*/
disconnect: function BTA_disconnect() {
this._detach();
this._extraActors = null;
this._styleSheetActors.clear();
this._exited = true;
this.exit();
},
/**
@ -901,6 +898,14 @@ TabActor.prototype = {
type: "tabDetached" });
}
Object.defineProperty(this, "docShell", {
value: null,
configurable: true
});
this._extraActors = null;
this._styleSheetActors.clear();
this._exited = true;
},
@ -1221,11 +1226,6 @@ TabActor.prototype = {
this._tabActorPool = null;
}
Object.defineProperty(this, "docShell", {
value: null,
configurable: true
});
this._attached = false;
return true;
},
@ -1822,7 +1822,10 @@ function RemoteBrowserTabActor(aConnection, aBrowser)
RemoteBrowserTabActor.prototype = {
connect: function() {
let connect = DebuggerServer.connectToChild(this._conn, this._browser);
let onDestroy = () => {
this._form = null;
};
let connect = DebuggerServer.connectToChild(this._conn, this._browser, onDestroy);
return connect.then(form => {
this._form = form;
return this;
@ -1835,15 +1838,21 @@ RemoteBrowserTabActor.prototype = {
},
update: function() {
let deferred = promise.defer();
let onFormUpdate = msg => {
this._mm.removeMessageListener("debug:form", onFormUpdate);
this._form = msg.json;
deferred.resolve(this);
};
this._mm.addMessageListener("debug:form", onFormUpdate);
this._mm.sendAsyncMessage("debug:form");
return deferred.promise;
// If the child happens to be crashed/close/detach, it won't have _form set,
// so only request form update if some code is still listening on the other side.
if (this._form) {
let deferred = promise.defer();
let onFormUpdate = msg => {
this._mm.removeMessageListener("debug:form", onFormUpdate);
this._form = msg.json;
deferred.resolve(this);
};
this._mm.addMessageListener("debug:form", onFormUpdate);
this._mm.sendAsyncMessage("debug:form");
return deferred.promise;
} else {
return this.connect();
}
},
form: function() {

View File

@ -17,10 +17,6 @@ let chromeGlobal = this;
const { dumpn } = DevToolsUtils;
const { DebuggerServer, ActorPool } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {});
if (!DebuggerServer.childID) {
DebuggerServer.childID = 1;
}
if (!DebuggerServer.initialized) {
DebuggerServer.init();
@ -44,17 +40,16 @@ let chromeGlobal = this;
let mm = msg.target;
let prefix = msg.data.prefix;
let id = DebuggerServer.childID++;
let conn = DebuggerServer.connectToParent(prefix, mm);
connections.set(id, conn);
connections.set(prefix, conn);
let actor = new DebuggerServer.ContentActor(conn, chromeGlobal);
let actor = new DebuggerServer.ContentActor(conn, chromeGlobal, prefix);
let actorPool = new ActorPool(conn);
actorPool.addActor(actor);
conn.addActorPool(actorPool);
sendAsyncMessage("debug:actor", {actor: actor.form(), childID: id});
sendAsyncMessage("debug:actor", {actor: actor.form(), prefix: prefix});
});
addMessageListener("debug:connect", onConnect);
@ -95,11 +90,11 @@ let chromeGlobal = this;
// Call DebuggerServerConnection.close to destroy all child actors
// (It should end up calling DebuggerServerConnection.onClosed
// that would actually cleanup all actor pools)
let childID = msg.data.childID;
let conn = connections.get(childID);
let prefix = msg.data.prefix;
let conn = connections.get(prefix);
if (conn) {
conn.close();
connections.delete(childID);
connections.delete(prefix);
}
});
addMessageListener("debug:disconnect", onDisconnect);

View File

@ -76,7 +76,7 @@ connected to the child process as parameter, e.g. in the **director-registry**:
let gTrackedMessageManager = new Set();
exports.setupParentProcess = function setupParentProcess({ mm, childID }) {
exports.setupParentProcess = function setupParentProcess({ mm, prefix }) {
if (gTrackedMessageManager.has(mm)) { return; }
gTrackedMessageManager.add(mm);
@ -84,7 +84,7 @@ exports.setupParentProcess = function setupParentProcess({ mm, childID }) {
mm.addMessageListener("debug:director-registry-request", handleChildRequest);
// time to unsubscribe from the disconnected message manager
DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected);
DebuggerServer.once("disconnected-from-child:" + prefix, handleMessageManagerDisconnected);
function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) {
...
@ -109,8 +109,8 @@ In the child process:
In the parent process:
- The DebuggerServer receives the DebuggerServer.setupInParent request
- it tries to load the required module
- it tries to call the **mod[setupParent]** method with the frame message manager and the childID
in the json parameter **{ mm, childID }**
- it tries to call the **mod[setupParent]** method with the frame message manager and the prefix
in the json parameter **{ mm, prefix }**
- the module setupParent helper use the mm to subscribe the messagemanager events
- the module setupParent helper use the DebuggerServer object to subscribe *once* the
**"disconnected-from-child:CHILDID"** event (needed to unsubscribe the messagemanager events)
**"disconnected-from-child:PREFIX"** event (needed to unsubscribe the messagemanager events)

View File

@ -809,13 +809,15 @@ var DebuggerServer = {
* The debugger server connection to use.
* @param nsIDOMElement aFrame
* The browser element that holds the child process.
* @param function [aOnDisconnect]
* Optional function to invoke when the child is disconnected.
* @param function [aOnDestroy]
* Optional function to invoke when the child process closes
* or the connection shuts down. (Need to forget about the
* related TabActor)
* @return object
* A promise object that is resolved once the connection is
* established.
*/
connectToChild: function(aConnection, aFrame, aOnDisconnect) {
connectToChild: function(aConnection, aFrame, aOnDestroy) {
let deferred = defer();
let mm = aFrame.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader
@ -824,7 +826,6 @@ var DebuggerServer = {
let actor, childTransport;
let prefix = aConnection.allocID("child");
let childID = null;
let netMonitor = null;
// provides hook to actor modules that need to exchange messages
@ -841,7 +842,7 @@ var DebuggerServer = {
return false;
}
m[setupParent]({ mm: mm, childID: childID });
m[setupParent]({ mm: mm, prefix: prefix });
return true;
} catch(e) {
@ -855,10 +856,11 @@ var DebuggerServer = {
mm.addMessageListener("debug:setup-in-parent", onSetupInParent);
let onActorCreated = DevToolsUtils.makeInfallible(function (msg) {
if (msg.json.prefix != prefix) {
return;
}
mm.removeMessageListener("debug:actor", onActorCreated);
childID = msg.json.childID;
// Pipe Debugger message from/to parent/child via the message manager
childTransport = new ChildDebuggerTransport(mm, prefix);
childTransport.hooks = {
@ -882,70 +884,65 @@ var DebuggerServer = {
}).bind(this);
mm.addMessageListener("debug:actor", onActorCreated);
let onMessageManagerClose = DevToolsUtils.makeInfallible(function (subject, topic, data) {
if (subject == mm) {
Services.obs.removeObserver(onMessageManagerClose, topic);
let destroy = DevToolsUtils.makeInfallible(function () {
// provides hook to actor modules that need to exchange messages
// between e10s parent and child processes
DebuggerServer.emit("disconnected-from-child:" + prefix, { mm: mm, prefix: prefix });
// provides hook to actor modules that need to exchange messages
// between e10s parent and child processes
this.emit("disconnected-from-child:" + childID, { mm: mm, childID: childID });
mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
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.
mm.sendAsyncMessage("debug:disconnect", { childID: childID });
} else {
// Otherwise, the app has been closed before the actor
// had a chance to be created, so we are not able to create
// the actor.
deferred.resolve(null);
}
if (actor) {
// The ContentActor within the child process doesn't necessary
// have to time to uninitialize itself when the app is closed/killed.
// So ensure telling the client that the related actor is detached.
aConnection.send({ from: actor.actor, type: "tabDetached" });
actor = null;
}
if (netMonitor) {
netMonitor.destroy();
netMonitor = null;
}
if (aOnDisconnect) {
aOnDisconnect(mm);
}
}
}).bind(this);
Services.obs.addObserver(onMessageManagerClose,
"message-manager-close", false);
events.once(aConnection, "closed", () => {
if (childTransport) {
// When the client disconnects, we have to unplug the dedicated
// ChildDebuggerTransport...
// 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.
mm.sendAsyncMessage("debug:disconnect", { childID: childID });
if (netMonitor) {
netMonitor.destroy();
netMonitor = null;
}
mm.sendAsyncMessage("debug:disconnect", { prefix: prefix });
} else {
// Otherwise, the app has been closed before the actor
// had a chance to be created, so we are not able to create
// the actor.
deferred.resolve(null);
}
if (actor) {
// The ContentActor within the child process doesn't necessary
// have time to uninitialize itself when the app is closed/killed.
// So ensure telling the client that the related actor is detached.
aConnection.send({ from: actor.actor, type: "tabDetached" });
actor = null;
}
if (netMonitor) {
netMonitor.destroy();
netMonitor = null;
}
if (aOnDestroy) {
aOnDestroy(mm);
}
// Cleanup all listeners
Services.obs.removeObserver(onMessageManagerClose, "message-manager-close");
mm.removeMessageListener("debug:setup-in-parent", onSetupInParent);
if (!actor) {
mm.removeMessageListener("debug:actor", onActorCreated);
}
events.off(aConnection, "closed", destroy);
});
// Listen for app process exit
let onMessageManagerClose = function (subject, topic, data) {
if (subject == mm) {
destroy();
}
};
Services.obs.addObserver(onMessageManagerClose,
"message-manager-close", false);
// Listen for connection close to cleanup things
// when user unplug the device or we lose the connection somehow.
events.on(aConnection, "closed", destroy);
mm.sendAsyncMessage("debug:connect", { prefix: prefix });
return deferred.promise;
@ -1558,6 +1555,8 @@ DebuggerServerConnection.prototype = {
this._extraPools.map(function(p) { p.cleanup(); });
this._extraPools = null;
this.rootActor = null;
this._transport = null;
DebuggerServer._connectionClosed(this);
},

View File

@ -90,7 +90,7 @@ function objEquiv(a, b) {
return false;
}
// An identical 'prototype' property.
if (a.prototype !== b.prototype) {
if ((a.prototype || undefined) != (b.prototype || undefined)) {
return false;
}
// Object.keys may be broken through screwy arguments passing. Converting to

View File

@ -623,6 +623,7 @@ function logTestInfo(aText, aCaller) {
*/
function debugDump(aText, aCaller) {
if (DEBUG_AUS_TEST) {
logTestInfo(aText, aCaller);
let caller = aCaller ? aCaller : Components.stack.caller;
logTestInfo(aText, caller);
}
}

View File

@ -1000,7 +1000,14 @@ function doTestFinish() {
if (gPassed === undefined) {
gPassed = true;
}
do_test_finished();
if (DEBUG_AUS_TEST) {
// This prevents do_print errors from being printed by the xpcshell test
// harness due to nsUpdateService.js logging to the console when the
// app.update.log preference is true.
Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false);
gAUS.observe(null, "nsPref:changed", PREF_APP_UPDATE_LOG);
}
do_execute_soon(do_test_finished);
}
/**
@ -1601,8 +1608,9 @@ function runUpdate(aExpectedExitValue, aExpectedStatus, aCallback) {
let updateLog = getUpdatesPatchDir();
updateLog.append(FILE_UPDATE_LOG);
// xpcshell tests won't display the entire contents so log each line.
let contents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
let aryLogContents = contents.split("\n");
let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
updateLogContents = replaceLogPaths(updateLogContents);
let aryLogContents = updateLogContents.split("\n");
logTestInfo("contents of " + updateLog.path + ":");
aryLogContents.forEach(function RU_LC_FE(aLine) {
logTestInfo(aLine);
@ -2181,8 +2189,9 @@ function runUpdateUsingService(aInitialStatus, aExpectedStatus, aCheckSvcLog) {
let updateLog = getUpdatesPatchDir();
updateLog.append(FILE_UPDATE_LOG);
// xpcshell tests won't display the entire contents so log each line.
let contents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
let aryLogContents = contents.split("\n");
let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
updateLogContents = replaceLogPaths(updateLogContents);
let aryLogContents = updateLogContents.split("\n");
logTestInfo("contents of " + updateLog.path + ":");
aryLogContents.forEach(function RUUS_TC_LC_FE(aLine) {
logTestInfo(aLine);
@ -2449,6 +2458,41 @@ function createUpdaterINI(aIsExeAsync) {
writeFile(updaterIni, updaterIniContents);
}
/**
* Helper function that replaces the common part of paths in the update log's
* contents with <test_dir_path> for paths to the the test directory and
* <update_dir_path> for paths to the update directory. This is needed since
* Assert.equal will truncate what it prints to the xpcshell log file.
*
* @param aLogContents
* The update log file's contents.
* @return the log contents with the paths replaced.
*/
function replaceLogPaths(aLogContents) {
let logContents = aLogContents;
// Remove the majority of the path up to the test directory. This is needed
// since Assert.equal won't print long strings to the test logs.
let testDirPath = do_get_file(gTestID, false).path;
if (IS_WIN) {
// Replace \\ with \\\\ so the regexp works.
testDirPath = testDirPath.replace(/\\/g, "\\\\");
}
logContents = logContents.replace(new RegExp(testDirPath, "g"),
"<test_dir_path>/" + gTestID);
let updatesDirPath = getMockUpdRootD().path;
if (IS_WIN) {
// Replace \\ with \\\\ so the regexp works.
updatesDirPath = updatesDirPath.replace(/\\/g, "\\\\");
}
logContents = logContents.replace(new RegExp(updatesDirPath, "g"),
"<update_dir_path>/" + gTestID);
if (IS_WIN) {
// Replace \ with /
logContents = logContents.replace(/\\/g, "/");
}
return logContents;
}
/**
* Helper function for updater binary tests for verifying the contents of the
* update log after a successful update.
@ -2464,12 +2508,13 @@ function checkUpdateLogContents(aCompareLogFile, aExcludeDistributionDir) {
// Sorting on Linux is different so skip checking the logs for now.
return;
}
let updateLog = getUpdatesPatchDir();
updateLog.append(FILE_UPDATE_LOG);
let updateLogContents = readFileBytes(updateLog);
// The channel-prefs.js is defined in gTestFilesCommon which will always be
// located to the end of gTestFiles.
// located to the end of gTestFiles when it is present.
if (gTestFiles.length > 1 &&
gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" &&
!gTestFiles[gTestFiles.length - 1].originalContents) {
@ -2515,9 +2560,7 @@ function checkUpdateLogContents(aCompareLogFile, aExcludeDistributionDir) {
updateLogContents = updateLogContents.replace(/\n+/g, "\n");
// Remove leading and trailing newlines
updateLogContents = updateLogContents.replace(/^\n|\n$/g, "");
// The update log when running the service tests sometimes starts with data
// from the previous launch of the updater.
updateLogContents = updateLogContents.replace(/^calling QuitProgressUI\n[^\n]*\nUPDATE TYPE/g, "UPDATE TYPE");
updateLogContents = replaceLogPaths(updateLogContents);
let compareLogContents = "";
if (aCompareLogFile) {
@ -2583,8 +2626,9 @@ function checkUpdateLogContains(aCheckString) {
let updateLog = getUpdatesPatchDir();
updateLog.append(FILE_UPDATE_LOG);
let updateLogContents = readFileBytes(updateLog);
updateLogContents = replaceLogPaths(updateLogContents);
Assert.notEqual(updateLogContents.indexOf(aCheckString), -1,
"the update log contents" + MSG_SHOULD_EQUAL + ", value: " +
"the update log contents should contain value: " +
aCheckString);
}
@ -2848,6 +2892,8 @@ function checkCallbackAppLog() {
gTimeoutRuns++;
if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
logTestInfo("callback log contents are not correct");
// This file doesn't contain full paths so there is no need to call
// replaceLogPaths.
let aryLog = logContents.split("\n");
let aryCompare = expectedLogContents.split("\n");
// Pushing an empty string to both arrays makes it so either array's length

View File

@ -224,7 +224,6 @@ function finishCheckUpdateFinished() {
checkAppBundleModTime();
checkFilesAfterUpdateSuccess(getApplyDirFile, false, false);
checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
checkCallbackAppLog();
standardInit();

View File

@ -202,7 +202,6 @@ function finishCheckUpdateApplied() {
gSwitchApp = true;
checkUpdateLogContents();
gSwitchApp = false;
checkCallbackAppLog();
standardInit();

View File

@ -106,7 +106,6 @@ function finishCheckUpdateFinished() {
checkAppBundleModTime();
checkFilesAfterUpdateSuccess(getApplyDirFile, false, false);
checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
checkCallbackAppLog();
standardInit();

View File

@ -44,6 +44,7 @@ skip-if = os != 'win'
skip-if = os != 'win'
[marFileLockedStageFailureComplete_win.js]
skip-if = os != 'win'
run-sequentially = Bug 1156446
[marFileLockedStageFailurePartial_win.js]
skip-if = os != 'win'
[marFileLockedFallbackStageFailureComplete_win.js]

View File

@ -228,7 +228,6 @@ function finishCheckUpdateFinished() {
checkAppBundleModTime();
checkFilesAfterUpdateSuccess(getApplyDirFile, false, false);
checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
checkCallbackAppLog();
standardInit();

View File

@ -206,7 +206,6 @@ function finishCheckUpdateApplied() {
gSwitchApp = true;
checkUpdateLogContents();
gSwitchApp = false;
checkCallbackAppLog();
standardInit();

View File

@ -110,7 +110,6 @@ function finishCheckUpdateFinished() {
checkAppBundleModTime();
checkFilesAfterUpdateSuccess(getApplyDirFile, false, false);
checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
checkCallbackAppLog();
standardInit();