This commit is contained in:
Wes Kocher 2015-01-05 17:08:49 -08:00
Родитель ddbca4135c d085aaee23
Коммит e62807e2fa
49 изменённых файлов: 4597 добавлений и 2278 удалений

Просмотреть файл

@ -16,521 +16,11 @@ loop.conversation = (function(mozL10n) {
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var IncomingConversationView = loop.conversationViews.IncomingConversationView;
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
getDefaultProps: function() {
return {
showMenu: false,
video: true
};
},
clickHandler: function(e) {
var target = e.target;
if (!target.classList.contains('btn-chevron')) {
this._hideDeclineMenu();
}
},
_handleAccept: function(callType) {
return function() {
this.props.model.set("selectedCallType", callType);
this.props.model.trigger("accept");
}.bind(this);
},
_handleDecline: function() {
this.props.model.trigger("decline");
},
_handleDeclineBlock: function(e) {
this.props.model.trigger("declineAndBlock");
/* Prevent event propagation
* stop the click from reaching parent element */
return false;
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept("audio-video"),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept("audio"),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (!this.props.video) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
/* jshint ignore:start */
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
});
return (
React.DOM.div({className: "call-window"},
CallIdentifierView({video: this.props.video,
peerIdentifier: this.props.model.getCallIdentifier(),
urlCreationDate: this.props.model.get("urlCreationDate"),
showIcons: true}),
React.DOM.div({className: "btn-group call-action-group"},
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
React.DOM.div({className: "btn-chevron-menu-group"},
React.DOM.div({className: "btn-group-chevron"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: "btn btn-decline",
onClick: this._handleDecline},
mozL10n.get("incoming_call_cancel_button")
),
React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu})
),
React.DOM.ul({className: dropdownMenuClassesDecline},
React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock},
mozL10n.get("incoming_call_cancel_and_block_button")
)
)
)
),
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
AcceptCallButton({mode: this._answerModeProps()}),
React.DOM.div({className: "fx-embedded-call-button-spacer"})
)
)
);
/* jshint ignore:end */
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton',
propTypes: {
mode: React.PropTypes.object.isRequired,
},
render: function() {
var mode = this.props.mode;
return (
/* jshint ignore:start */
React.DOM.div({className: "btn-chevron-menu-group"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: "btn btn-accept",
onClick: mode.primary.handler,
title: mozL10n.get(mode.primary.tooltip)},
React.DOM.span({className: "fx-embedded-answer-btn-text"},
mozL10n.get("incoming_call_accept_button")
),
React.DOM.span({className: mode.primary.className})
),
React.DOM.div({className: mode.secondary.className,
onClick: mode.secondary.handler,
title: mozL10n.get(mode.secondary.tooltip)}
)
)
)
/* jshint ignore:end */
);
}
});
/**
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
mixins: [sharedMixins.AudioMixin],
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("generic_failure_title");
return (
React.DOM.div({className: "call-window"},
React.DOM.h2(null, mozL10n.get("generic_failure_title")),
React.DOM.div({className: "btn-group call-action-group"},
React.DOM.button({className: "btn btn-cancel",
onClick: this.props.cancelCall},
mozL10n.get("cancel_button")
)
)
)
);
}
});
/**
* This view manages the incoming conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {
return {
callFailed: false, // XXX this should be removed when bug 1047410 lands.
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("accept", this.accept, this);
this.props.conversation.on("decline", this.decline, this);
this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
this.props.conversation.on("call:accepted", this.accepted, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this.endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
this.setupIncomingCall();
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
render: function() {
switch (this.state.callStatus) {
case "start": {
document.title = mozL10n.get("incoming_call_title2");
// XXX Don't render anything initially, though this should probably
// be some sort of pending view, whilst we connect the websocket.
return null;
}
case "incoming": {
document.title = mozL10n.get("incoming_call_title2");
return (
IncomingCallView({
model: this.props.conversation,
video: this.props.conversation.hasVideoStream("incoming")}
)
);
}
case "connected": {
document.title = this.props.conversation.getCallIdentifier();
var callType = this.props.conversation.get("selectedCallType");
return (
sharedViews.ConversationView({
initiate: true,
sdk: this.props.sdk,
model: this.props.conversation,
video: {enabled: callType !== "audio"}}
)
);
}
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return GenericFailureView({
cancelCall: this.closeWindow.bind(this)}
);
}
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
sharedViews.FeedbackView({
feedbackStore: this.props.feedbackStore,
onAfterFeedbackReceived: this.closeWindow.bind(this)}
)
);
}
case "close": {
this.closeWindow();
return (React.DOM.div(null));
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error(error);
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.setState({callFailed: false, callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Incoming call route.
*/
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
/**
* Starts the actual conversation
*/
accepted: function() {
this.setState({callStatus: "connected"});
},
/**
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function(progressStatus) {
this.setState({
callStatus: progressStatus === "terminated" ? "close" : "incoming"
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
* If we add more cases here, then we should refactor this function.
*
* @param {Object} progressData The progress data from the websocket.
* @param {String} previousState The previous state from the websocket.
*/
_handleWebSocketProgress: function(progressData, previousState) {
// We only care about the terminated state at the moment.
if (progressData.state !== "terminated")
return;
// XXX This would be nicer in the _abortIncomingCall function, but we need to stop
// it here for now due to server-side issues that are being fixed in bug 1088351.
// This is before the abort call to ensure that it happens before the window is
// closed.
navigator.mozLoop.stopAlerting();
// If we hit any of the termination reasons, and the user hasn't accepted
// then it seems reasonable to close the window/abort the incoming call.
//
// If the user has accepted the call, and something's happened, display
// the call failed view.
//
// https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
if (previousState === "init" || previousState === "alerting") {
this._abortIncomingCall();
} else {
this.setState({callFailed: true, callStatus: "end"});
}
},
/**
* Silently aborts an incoming call - stops the alerting, and
* closes the websocket.
*/
_abortIncomingCall: function() {
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._websocket.accept();
this.props.conversation.accepted();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error("Failed initiating the call session.");
},
});
var GenericFailureView = loop.conversationViews.GenericFailureView;
/**
* Master controller view for handling if incoming or outgoing calls are
@ -711,9 +201,6 @@ loop.conversation = (function(mozL10n) {
return {
AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
GenericFailureView: GenericFailureView,
init: init
};
})(document.mozL10n);

Просмотреть файл

@ -16,521 +16,11 @@ loop.conversation = (function(mozL10n) {
var sharedModels = loop.shared.models;
var sharedActions = loop.shared.actions;
var IncomingConversationView = loop.conversationViews.IncomingConversationView;
var OutgoingConversationView = loop.conversationViews.OutgoingConversationView;
var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var IncomingCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
getDefaultProps: function() {
return {
showMenu: false,
video: true
};
},
clickHandler: function(e) {
var target = e.target;
if (!target.classList.contains('btn-chevron')) {
this._hideDeclineMenu();
}
},
_handleAccept: function(callType) {
return function() {
this.props.model.set("selectedCallType", callType);
this.props.model.trigger("accept");
}.bind(this);
},
_handleDecline: function() {
this.props.model.trigger("decline");
},
_handleDeclineBlock: function(e) {
this.props.model.trigger("declineAndBlock");
/* Prevent event propagation
* stop the click from reaching parent element */
return false;
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept("audio-video"),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept("audio"),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (!this.props.video) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
/* jshint ignore:start */
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
});
return (
<div className="call-window">
<CallIdentifierView video={this.props.video}
peerIdentifier={this.props.model.getCallIdentifier()}
urlCreationDate={this.props.model.get("urlCreationDate")}
showIcons={true} />
<div className="btn-group call-action-group">
<div className="fx-embedded-call-button-spacer"></div>
<div className="btn-chevron-menu-group">
<div className="btn-group-chevron">
<div className="btn-group">
<button className="btn btn-decline"
onClick={this._handleDecline}>
{mozL10n.get("incoming_call_cancel_button")}
</button>
<div className="btn-chevron" onClick={this.toggleDropdownMenu} />
</div>
<ul className={dropdownMenuClassesDecline}>
<li className="btn-block" onClick={this._handleDeclineBlock}>
{mozL10n.get("incoming_call_cancel_and_block_button")}
</li>
</ul>
</div>
</div>
<div className="fx-embedded-call-button-spacer"></div>
<AcceptCallButton mode={this._answerModeProps()} />
<div className="fx-embedded-call-button-spacer"></div>
</div>
</div>
);
/* jshint ignore:end */
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({
propTypes: {
mode: React.PropTypes.object.isRequired,
},
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"
onClick={mode.primary.handler}
title={mozL10n.get(mode.primary.tooltip)}>
<span className="fx-embedded-answer-btn-text">
{mozL10n.get("incoming_call_accept_button")}
</span>
<span className={mode.primary.className}></span>
</button>
<div className={mode.secondary.className}
onClick={mode.secondary.handler}
title={mozL10n.get(mode.secondary.tooltip)}>
</div>
</div>
</div>
/* jshint ignore:end */
);
}
});
/**
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var GenericFailureView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("generic_failure_title");
return (
<div className="call-window">
<h2>{mozL10n.get("generic_failure_title")}</h2>
<div className="btn-group call-action-group">
<button className="btn btn-cancel"
onClick={this.props.cancelCall}>
{mozL10n.get("cancel_button")}
</button>
</div>
</div>
);
}
});
/**
* This view manages the incoming conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {
return {
callFailed: false, // XXX this should be removed when bug 1047410 lands.
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("accept", this.accept, this);
this.props.conversation.on("decline", this.decline, this);
this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
this.props.conversation.on("call:accepted", this.accepted, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this.endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
this.setupIncomingCall();
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
render: function() {
switch (this.state.callStatus) {
case "start": {
document.title = mozL10n.get("incoming_call_title2");
// XXX Don't render anything initially, though this should probably
// be some sort of pending view, whilst we connect the websocket.
return null;
}
case "incoming": {
document.title = mozL10n.get("incoming_call_title2");
return (
<IncomingCallView
model={this.props.conversation}
video={this.props.conversation.hasVideoStream("incoming")}
/>
);
}
case "connected": {
document.title = this.props.conversation.getCallIdentifier();
var callType = this.props.conversation.get("selectedCallType");
return (
<sharedViews.ConversationView
initiate={true}
sdk={this.props.sdk}
model={this.props.conversation}
video={{enabled: callType !== "audio"}}
/>
);
}
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return <GenericFailureView
cancelCall={this.closeWindow.bind(this)}
/>;
}
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
<sharedViews.FeedbackView
feedbackStore={this.props.feedbackStore}
onAfterFeedbackReceived={this.closeWindow.bind(this)}
/>
);
}
case "close": {
this.closeWindow();
return (<div/>);
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error(error);
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.setState({callFailed: false, callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Incoming call route.
*/
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
/**
* Starts the actual conversation
*/
accepted: function() {
this.setState({callStatus: "connected"});
},
/**
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function(progressStatus) {
this.setState({
callStatus: progressStatus === "terminated" ? "close" : "incoming"
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
* If we add more cases here, then we should refactor this function.
*
* @param {Object} progressData The progress data from the websocket.
* @param {String} previousState The previous state from the websocket.
*/
_handleWebSocketProgress: function(progressData, previousState) {
// We only care about the terminated state at the moment.
if (progressData.state !== "terminated")
return;
// XXX This would be nicer in the _abortIncomingCall function, but we need to stop
// it here for now due to server-side issues that are being fixed in bug 1088351.
// This is before the abort call to ensure that it happens before the window is
// closed.
navigator.mozLoop.stopAlerting();
// If we hit any of the termination reasons, and the user hasn't accepted
// then it seems reasonable to close the window/abort the incoming call.
//
// If the user has accepted the call, and something's happened, display
// the call failed view.
//
// https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
if (previousState === "init" || previousState === "alerting") {
this._abortIncomingCall();
} else {
this.setState({callFailed: true, callStatus: "end"});
}
},
/**
* Silently aborts an incoming call - stops the alerting, and
* closes the websocket.
*/
_abortIncomingCall: function() {
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._websocket.accept();
this.props.conversation.accepted();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error("Failed initiating the call session.");
},
});
var GenericFailureView = loop.conversationViews.GenericFailureView;
/**
* Master controller view for handling if incoming or outgoing calls are
@ -711,9 +201,6 @@ loop.conversation = (function(mozL10n) {
return {
AppControllerView: AppControllerView,
IncomingConversationView: IncomingConversationView,
IncomingCallView: IncomingCallView,
GenericFailureView: GenericFailureView,
init: init
};
})(document.mozL10n);

Просмотреть файл

@ -15,6 +15,7 @@ loop.conversationViews = (function(mozL10n) {
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
@ -129,6 +130,518 @@ loop.conversationViews = (function(mozL10n) {
}
});
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
getDefaultProps: function() {
return {
showMenu: false,
video: true
};
},
clickHandler: function(e) {
var target = e.target;
if (!target.classList.contains('btn-chevron')) {
this._hideDeclineMenu();
}
},
_handleAccept: function(callType) {
return function() {
this.props.model.set("selectedCallType", callType);
this.props.model.trigger("accept");
}.bind(this);
},
_handleDecline: function() {
this.props.model.trigger("decline");
},
_handleDeclineBlock: function(e) {
this.props.model.trigger("declineAndBlock");
/* Prevent event propagation
* stop the click from reaching parent element */
return false;
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept("audio-video"),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept("audio"),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (!this.props.video) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
/* jshint ignore:start */
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
});
return (
React.DOM.div({className: "call-window"},
CallIdentifierView({video: this.props.video,
peerIdentifier: this.props.model.getCallIdentifier(),
urlCreationDate: this.props.model.get("urlCreationDate"),
showIcons: true}),
React.DOM.div({className: "btn-group call-action-group"},
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
React.DOM.div({className: "btn-chevron-menu-group"},
React.DOM.div({className: "btn-group-chevron"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: "btn btn-decline",
onClick: this._handleDecline},
mozL10n.get("incoming_call_cancel_button")
),
React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu})
),
React.DOM.ul({className: dropdownMenuClassesDecline},
React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock},
mozL10n.get("incoming_call_cancel_and_block_button")
)
)
)
),
React.DOM.div({className: "fx-embedded-call-button-spacer"}),
AcceptCallButton({mode: this._answerModeProps()}),
React.DOM.div({className: "fx-embedded-call-button-spacer"})
)
)
);
/* jshint ignore:end */
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton',
propTypes: {
mode: React.PropTypes.object.isRequired,
},
render: function() {
var mode = this.props.mode;
return (
/* jshint ignore:start */
React.DOM.div({className: "btn-chevron-menu-group"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: "btn btn-accept",
onClick: mode.primary.handler,
title: mozL10n.get(mode.primary.tooltip)},
React.DOM.span({className: "fx-embedded-answer-btn-text"},
mozL10n.get("incoming_call_accept_button")
),
React.DOM.span({className: mode.primary.className})
),
React.DOM.div({className: mode.secondary.className,
onClick: mode.secondary.handler,
title: mozL10n.get(mode.secondary.tooltip)}
)
)
)
/* jshint ignore:end */
);
}
});
/**
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var GenericFailureView = React.createClass({displayName: 'GenericFailureView',
mixins: [sharedMixins.AudioMixin],
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("generic_failure_title");
return (
React.DOM.div({className: "call-window"},
React.DOM.h2(null, mozL10n.get("generic_failure_title")),
React.DOM.div({className: "btn-group call-action-group"},
React.DOM.button({className: "btn btn-cancel",
onClick: this.props.cancelCall},
mozL10n.get("cancel_button")
)
)
)
);
}
});
/**
* This view manages the incoming conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({displayName: 'IncomingConversationView',
mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {
return {
callFailed: false, // XXX this should be removed when bug 1047410 lands.
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("accept", this.accept, this);
this.props.conversation.on("decline", this.decline, this);
this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
this.props.conversation.on("call:accepted", this.accepted, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this.endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
this.setupIncomingCall();
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
render: function() {
switch (this.state.callStatus) {
case "start": {
document.title = mozL10n.get("incoming_call_title2");
// XXX Don't render anything initially, though this should probably
// be some sort of pending view, whilst we connect the websocket.
return null;
}
case "incoming": {
document.title = mozL10n.get("incoming_call_title2");
return (
IncomingCallView({
model: this.props.conversation,
video: this.props.conversation.hasVideoStream("incoming")}
)
);
}
case "connected": {
document.title = this.props.conversation.getCallIdentifier();
var callType = this.props.conversation.get("selectedCallType");
return (
sharedViews.ConversationView({
initiate: true,
sdk: this.props.sdk,
model: this.props.conversation,
video: {enabled: callType !== "audio"}}
)
);
}
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return GenericFailureView({
cancelCall: this.closeWindow.bind(this)}
);
}
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
sharedViews.FeedbackView({
feedbackStore: this.props.feedbackStore,
onAfterFeedbackReceived: this.closeWindow.bind(this)}
)
);
}
case "close": {
this.closeWindow();
return (React.DOM.div(null));
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error(error);
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.setState({callFailed: false, callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Incoming call route.
*/
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
/**
* Starts the actual conversation
*/
accepted: function() {
this.setState({callStatus: "connected"});
},
/**
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function(progressStatus) {
this.setState({
callStatus: progressStatus === "terminated" ? "close" : "incoming"
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
* If we add more cases here, then we should refactor this function.
*
* @param {Object} progressData The progress data from the websocket.
* @param {String} previousState The previous state from the websocket.
*/
_handleWebSocketProgress: function(progressData, previousState) {
// We only care about the terminated state at the moment.
if (progressData.state !== "terminated")
return;
// XXX This would be nicer in the _abortIncomingCall function, but we need to stop
// it here for now due to server-side issues that are being fixed in bug 1088351.
// This is before the abort call to ensure that it happens before the window is
// closed.
navigator.mozLoop.stopAlerting();
// If we hit any of the termination reasons, and the user hasn't accepted
// then it seems reasonable to close the window/abort the incoming call.
//
// If the user has accepted the call, and something's happened, display
// the call failed view.
//
// https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
if (previousState === "init" || previousState === "alerting") {
this._abortIncomingCall();
} else {
this.setState({callFailed: true, callStatus: "end"});
}
},
/**
* Silently aborts an incoming call - stops the alerting, and
* closes the websocket.
*/
_abortIncomingCall: function() {
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._websocket.accept();
this.props.conversation.accepted();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error("Failed initiating the call session.");
},
});
/**
* View for pending conversations. Displays a cancel button and appropriate
* pending/ringing strings.
@ -535,6 +1048,9 @@ loop.conversationViews = (function(mozL10n) {
CallIdentifierView: CallIdentifierView,
ConversationDetailView: ConversationDetailView,
CallFailedView: CallFailedView,
GenericFailureView: GenericFailureView,
IncomingCallView: IncomingCallView,
IncomingConversationView: IncomingConversationView,
OngoingConversationView: OngoingConversationView,
OutgoingConversationView: OutgoingConversationView
};

Просмотреть файл

@ -15,6 +15,7 @@ loop.conversationViews = (function(mozL10n) {
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
@ -129,6 +130,518 @@ loop.conversationViews = (function(mozL10n) {
}
});
// Matches strings of the form "<nonspaces>@<nonspaces>" or "+<digits>"
var EMAIL_OR_PHONE_RE = /^(:?\S+@\S+|\+\d+)$/;
var IncomingCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.AudioMixin],
propTypes: {
model: React.PropTypes.object.isRequired,
video: React.PropTypes.bool.isRequired
},
getDefaultProps: function() {
return {
showMenu: false,
video: true
};
},
clickHandler: function(e) {
var target = e.target;
if (!target.classList.contains('btn-chevron')) {
this._hideDeclineMenu();
}
},
_handleAccept: function(callType) {
return function() {
this.props.model.set("selectedCallType", callType);
this.props.model.trigger("accept");
}.bind(this);
},
_handleDecline: function() {
this.props.model.trigger("decline");
},
_handleDeclineBlock: function(e) {
this.props.model.trigger("declineAndBlock");
/* Prevent event propagation
* stop the click from reaching parent element */
return false;
},
/*
* Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video
* answer button primarily, an audio call will flip them.
**/
_answerModeProps: function() {
var videoButton = {
handler: this._handleAccept("audio-video"),
className: "fx-embedded-btn-icon-video",
tooltip: "incoming_call_accept_audio_video_tooltip"
};
var audioButton = {
handler: this._handleAccept("audio"),
className: "fx-embedded-btn-audio-small",
tooltip: "incoming_call_accept_audio_only_tooltip"
};
var props = {};
props.primary = videoButton;
props.secondary = audioButton;
// When video is not enabled on this call, we swap the buttons around.
if (!this.props.video) {
audioButton.className = "fx-embedded-btn-icon-audio";
videoButton.className = "fx-embedded-btn-video-small";
props.primary = audioButton;
props.secondary = videoButton;
}
return props;
},
render: function() {
/* jshint ignore:start */
var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true,
"conversation-window-dropdown": true,
"visually-hidden": !this.state.showMenu
});
return (
<div className="call-window">
<CallIdentifierView video={this.props.video}
peerIdentifier={this.props.model.getCallIdentifier()}
urlCreationDate={this.props.model.get("urlCreationDate")}
showIcons={true} />
<div className="btn-group call-action-group">
<div className="fx-embedded-call-button-spacer"></div>
<div className="btn-chevron-menu-group">
<div className="btn-group-chevron">
<div className="btn-group">
<button className="btn btn-decline"
onClick={this._handleDecline}>
{mozL10n.get("incoming_call_cancel_button")}
</button>
<div className="btn-chevron" onClick={this.toggleDropdownMenu} />
</div>
<ul className={dropdownMenuClassesDecline}>
<li className="btn-block" onClick={this._handleDeclineBlock}>
{mozL10n.get("incoming_call_cancel_and_block_button")}
</li>
</ul>
</div>
</div>
<div className="fx-embedded-call-button-spacer"></div>
<AcceptCallButton mode={this._answerModeProps()} />
<div className="fx-embedded-call-button-spacer"></div>
</div>
</div>
);
/* jshint ignore:end */
}
});
/**
* Incoming call view accept button, renders different primary actions
* (answer with video / with audio only) based on the props received
**/
var AcceptCallButton = React.createClass({
propTypes: {
mode: React.PropTypes.object.isRequired,
},
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"
onClick={mode.primary.handler}
title={mozL10n.get(mode.primary.tooltip)}>
<span className="fx-embedded-answer-btn-text">
{mozL10n.get("incoming_call_accept_button")}
</span>
<span className={mode.primary.className}></span>
</button>
<div className={mode.secondary.className}
onClick={mode.secondary.handler}
title={mozL10n.get(mode.secondary.tooltip)}>
</div>
</div>
</div>
/* jshint ignore:end */
);
}
});
/**
* Something went wrong view. Displayed when there's a big problem.
*
* XXX Based on CallFailedView, but built specially until we flux-ify the
* incoming call views (bug 1088672).
*/
var GenericFailureView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: {
cancelCall: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("failure");
},
render: function() {
document.title = mozL10n.get("generic_failure_title");
return (
<div className="call-window">
<h2>{mozL10n.get("generic_failure_title")}</h2>
<div className="btn-group call-action-group">
<button className="btn btn-cancel"
onClick={this.props.cancelCall}>
{mozL10n.get("cancel_button")}
</button>
</div>
</div>
);
}
});
/**
* This view manages the incoming conversation views - from
* call initiation through to the actual conversation and call end.
*
* At the moment, it does more than that, these parts need refactoring out.
*/
var IncomingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin, sharedMixins.WindowCloseMixin],
propTypes: {
client: React.PropTypes.instanceOf(loop.Client).isRequired,
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
conversationAppStore: React.PropTypes.instanceOf(
loop.store.ConversationAppStore).isRequired,
feedbackStore:
React.PropTypes.instanceOf(loop.store.FeedbackStore).isRequired
},
getInitialState: function() {
return {
callFailed: false, // XXX this should be removed when bug 1047410 lands.
callStatus: "start"
};
},
componentDidMount: function() {
this.props.conversation.on("accept", this.accept, this);
this.props.conversation.on("decline", this.decline, this);
this.props.conversation.on("declineAndBlock", this.declineAndBlock, this);
this.props.conversation.on("call:accepted", this.accepted, this);
this.props.conversation.on("change:publishedStream", this._checkConnected, this);
this.props.conversation.on("change:subscribedStream", this._checkConnected, this);
this.props.conversation.on("session:ended", this.endCall, this);
this.props.conversation.on("session:peer-hungup", this._onPeerHungup, this);
this.props.conversation.on("session:network-disconnected", this._onNetworkDisconnected, this);
this.props.conversation.on("session:connection-error", this._notifyError, this);
this.setupIncomingCall();
},
componentDidUnmount: function() {
this.props.conversation.off(null, null, this);
},
render: function() {
switch (this.state.callStatus) {
case "start": {
document.title = mozL10n.get("incoming_call_title2");
// XXX Don't render anything initially, though this should probably
// be some sort of pending view, whilst we connect the websocket.
return null;
}
case "incoming": {
document.title = mozL10n.get("incoming_call_title2");
return (
<IncomingCallView
model={this.props.conversation}
video={this.props.conversation.hasVideoStream("incoming")}
/>
);
}
case "connected": {
document.title = this.props.conversation.getCallIdentifier();
var callType = this.props.conversation.get("selectedCallType");
return (
<sharedViews.ConversationView
initiate={true}
sdk={this.props.sdk}
model={this.props.conversation}
video={{enabled: callType !== "audio"}}
/>
);
}
case "end": {
// XXX To be handled with the "failed" view state when bug 1047410 lands
if (this.state.callFailed) {
return <GenericFailureView
cancelCall={this.closeWindow.bind(this)}
/>;
}
document.title = mozL10n.get("conversation_has_ended");
this.play("terminated");
return (
<sharedViews.FeedbackView
feedbackStore={this.props.feedbackStore}
onAfterFeedbackReceived={this.closeWindow.bind(this)}
/>
);
}
case "close": {
this.closeWindow();
return (<div/>);
}
}
},
/**
* Notify the user that the connection was not possible
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error(error);
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Peer hung up. Notifies the user and ends the call.
*
* Event properties:
* - {String} connectionId: OT session id
*/
_onPeerHungup: function() {
this.setState({callFailed: false, callStatus: "end"});
},
/**
* Network disconnected. Notifies the user and ends the call.
*/
_onNetworkDisconnected: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
this.setState({callFailed: true, callStatus: "end"});
},
/**
* Incoming call route.
*/
setupIncomingCall: function() {
navigator.mozLoop.startAlerting();
// XXX This is a hack until we rework for the flux model in bug 1088672.
var callData = this.props.conversationAppStore.getStoreState().windowData;
this.props.conversation.setIncomingSessionData(callData);
this._setupWebSocket();
},
/**
* Starts the actual conversation
*/
accepted: function() {
this.setState({callStatus: "connected"});
},
/**
* Moves the call to the end state
*/
endCall: function() {
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this.setState({callStatus: "end"});
},
/**
* Used to set up the web socket connection and navigate to the
* call view if appropriate.
*/
_setupWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.props.conversation.get("progressURL"),
websocketToken: this.props.conversation.get("websocketToken"),
callId: this.props.conversation.get("callId"),
});
this._websocket.promiseConnect().then(function(progressStatus) {
this.setState({
callStatus: progressStatus === "terminated" ? "close" : "incoming"
});
}.bind(this), function() {
this._handleSessionError();
return;
}.bind(this));
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Checks if the streams have been connected, and notifies the
* websocket that the media is now connected.
*/
_checkConnected: function() {
// Check we've had both local and remote streams connected before
// sending the media up message.
if (this.props.conversation.streamsConnected()) {
this._websocket.mediaUp();
}
},
/**
* Used to receive websocket progress and to determine how to handle
* it if appropraite.
* If we add more cases here, then we should refactor this function.
*
* @param {Object} progressData The progress data from the websocket.
* @param {String} previousState The previous state from the websocket.
*/
_handleWebSocketProgress: function(progressData, previousState) {
// We only care about the terminated state at the moment.
if (progressData.state !== "terminated")
return;
// XXX This would be nicer in the _abortIncomingCall function, but we need to stop
// it here for now due to server-side issues that are being fixed in bug 1088351.
// This is before the abort call to ensure that it happens before the window is
// closed.
navigator.mozLoop.stopAlerting();
// If we hit any of the termination reasons, and the user hasn't accepted
// then it seems reasonable to close the window/abort the incoming call.
//
// If the user has accepted the call, and something's happened, display
// the call failed view.
//
// https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
if (previousState === "init" || previousState === "alerting") {
this._abortIncomingCall();
} else {
this.setState({callFailed: true, callStatus: "end"});
}
},
/**
* Silently aborts an incoming call - stops the alerting, and
* closes the websocket.
*/
_abortIncomingCall: function() {
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Accepts an incoming call.
*/
accept: function() {
navigator.mozLoop.stopAlerting();
this._websocket.accept();
this.props.conversation.accepted();
},
/**
* Declines a call and handles closing of the window.
*/
_declineCall: function() {
this._websocket.decline();
navigator.mozLoop.calls.clearCallInProgress(
this.props.conversation.get("windowId"));
this._websocket.close();
// Having a timeout here lets the logging for the websocket complete and be
// displayed on the console if both are on.
setTimeout(this.closeWindow, 0);
},
/**
* Declines an incoming call.
*/
decline: function() {
navigator.mozLoop.stopAlerting();
this._declineCall();
},
/**
* Decline and block an incoming call
* @note:
* - loopToken is the callUrl identifier. It gets set in the panel
* after a callUrl is received
*/
declineAndBlock: function() {
navigator.mozLoop.stopAlerting();
var token = this.props.conversation.get("callToken");
var callerId = this.props.conversation.get("callerId");
// If this is a direct call, we'll need to block the caller directly.
if (callerId && EMAIL_OR_PHONE_RE.test(callerId)) {
navigator.mozLoop.calls.blockDirectCaller(callerId, function(err) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1103150).
console.log(err.fileName + ":" + err.lineNumber + ": " + err.message);
});
} else {
this.props.client.deleteCallUrl(token,
this.props.conversation.get("sessionType"),
function(error) {
// XXX The conversation window will be closed when this cb is triggered
// figure out if there is a better way to report the error to the user
// (bug 1048909).
console.log(error);
});
}
this._declineCall();
},
/**
* Handles a error starting the session
*/
_handleSessionError: function() {
// XXX Not the ideal response, but bug 1047410 will be replacing
// this by better "call failed" UI.
console.error("Failed initiating the call session.");
},
});
/**
* View for pending conversations. Displays a cancel button and appropriate
* pending/ringing strings.
@ -535,6 +1048,9 @@ loop.conversationViews = (function(mozL10n) {
CallIdentifierView: CallIdentifierView,
ConversationDetailView: ConversationDetailView,
CallFailedView: CallFailedView,
GenericFailureView: GenericFailureView,
IncomingCallView: IncomingCallView,
IncomingConversationView: IncomingConversationView,
OngoingConversationView: OngoingConversationView,
OutgoingConversationView: OutgoingConversationView
};

Просмотреть файл

@ -295,7 +295,7 @@ loop.roomViews = (function(mozL10n) {
case ROOM_STATES.FULL: {
// Note: While rooms are set to hold a maximum of 2 participants, the
// FULL case should never happen on desktop.
return loop.conversation.GenericFailureView({
return loop.conversationViews.GenericFailureView({
cancelCall: this.closeWindow}
);
}

Просмотреть файл

@ -295,7 +295,7 @@ loop.roomViews = (function(mozL10n) {
case ROOM_STATES.FULL: {
// Note: While rooms are set to hold a maximum of 2 participants, the
// FULL case should never happen on desktop.
return <loop.conversation.GenericFailureView
return <loop.conversationViews.GenericFailureView
cancelCall={this.closeWindow}
/>;
}

Просмотреть файл

@ -7,13 +7,28 @@ describe("loop.conversationViews", function () {
"use strict";
var sharedUtils = loop.shared.utils;
var sharedView = loop.shared.views;
var sandbox, oldTitle, view, dispatcher, contact, fakeAudioXHR;
var fakeMozLoop, fakeWindow;
var CALL_STATES = loop.store.CALL_STATES;
// XXX refactor to Just Work with "sandbox.stubComponent" or else
// just pass in the sandbox and put somewhere generally usable
function stubComponent(obj, component, mockTagName){
var reactClass = React.createClass({
render: function() {
var mockTagName = mockTagName || "div";
return React.DOM[mockTagName](null, this.props.children);
}
});
return sandbox.stub(obj, component, reactClass);
}
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
oldTitle = document.title;
sandbox.stub(document.mozL10n, "get", function(x) {
@ -45,7 +60,11 @@ describe("loop.conversationViews", function () {
};
fakeMozLoop = navigator.mozLoop = {
getLoopPref: sinon.stub().returns("http://fakeurl"),
// Dummy function, stubbed below.
getLoopPref: function() {},
calls: {
clearCallInProgress: sinon.stub()
},
composeEmail: sinon.spy(),
get appVersionInfo() {
return {
@ -57,10 +76,19 @@ describe("loop.conversationViews", function () {
getAudioBlob: sinon.spy(function(name, callback) {
callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
}),
startAlerting: sinon.stub(),
stopAlerting: sinon.stub(),
userProfile: {
email: "bob@invalid.tld"
}
};
sinon.stub(fakeMozLoop, "getLoopPref", function(pref) {
if (pref === "fake") {
return"http://fakeurl";
}
return false;
});
fakeWindow = {
navigator: { mozLoop: fakeMozLoop },
@ -578,4 +606,721 @@ describe("loop.conversationViews", function () {
loop.conversationViews.CallFailedView);
});
});
describe("IncomingConversationView", function() {
var conversationAppStore, conversation, client, icView, oldTitle,
feedbackStore;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversationViews.IncomingConversationView({
client: client,
conversation: conversation,
sdk: {},
conversationAppStore: conversationAppStore,
feedbackStore: feedbackStore
}));
}
beforeEach(function() {
oldTitle = document.title;
client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {}
});
conversation.set({windowId: 42});
var dispatcher = new loop.Dispatcher();
conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
sandbox.stub(conversation, "setOutgoingSessionData");
});
afterEach(function() {
icView = undefined;
document.title = oldTitle;
});
describe("start", function() {
it("should set the title to incoming_call_title2", function() {
conversationAppStore.setStoreState({
windowData: {
progressURL: "fake",
websocketToken: "fake",
callId: 42
}
});
icView = mountTestComponent();
expect(document.title).eql("incoming_call_title2");
});
});
describe("componentDidMount", function() {
var fakeSessionData, promise, resolveWebSocketConnect;
var rejectWebSocketConnect;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: "7b"
};
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
stubComponent(loop.conversationViews, "IncomingCallView");
stubComponent(sharedView, "ConversationView");
});
it("should start alerting", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
});
describe("Session Data setup", function() {
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function () {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
return promise;
},
on: sinon.stub()
});
});
it("should store the session data", function() {
sandbox.stub(conversation, "setIncomingSessionData");
icView = mountTestComponent();
sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData);
});
it("should setup the websocket connection", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
websocketToken: "7b"
});
});
});
describe("WebSocket Handling", function() {
beforeEach(function() {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
});
it("should set the state to incoming on success", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("incoming");
promise.then(function () {
expect(icView.state.callStatus).eql("incoming");
done();
});
});
it("should set the state to close on success if the progress " +
"state is terminated", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("terminated");
promise.then(function () {
expect(icView.state.callStatus).eql("close");
done();
});
});
// XXX implement me as part of bug 1047410
// see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
it.skip("should should switch view state to failed", function(done) {
icView = mountTestComponent();
rejectWebSocketConnect();
promise.then(function() {}, function() {
done();
});
});
});
describe("WebSocket Events", function() {
describe("Call cancelled or timed out before acceptance", function() {
beforeEach(function() {
// Mounting the test component automatically calls the required
// setup functions
icView = mountTestComponent();
promise = new Promise(function(resolve, reject) {
resolve();
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
});
describe("progress - terminated (previousState = alerting)", function() {
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "timeout"
}, "alerting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
it("should close the websocket", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "closed"
}, "alerting");
sinon.assert.calledOnce(icView._websocket.close);
done();
});
});
it("should close the window", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "answered-elsewhere"
}, "alerting");
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
done();
});
});
});
describe("progress - terminated (previousState not init" +
" nor alerting)",
function() {
it("should set the state to end", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "media-fail"
}, "connecting");
expect(icView.state.callStatus).eql("end");
done();
});
});
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "media-fail"
}, "connecting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
});
});
});
describe("#accept", function() {
beforeEach(function() {
icView = mountTestComponent();
conversation.setIncomingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
sandbox.stub(icView._websocket, "accept");
sandbox.stub(icView.props.conversation, "accepted");
});
it("should initiate the conversation", function() {
icView.accept();
sinon.assert.calledOnce(icView.props.conversation.accepted);
});
it("should notify the websocket of the user acceptance", function() {
icView.accept();
sinon.assert.calledOnce(icView._websocket.accept);
});
it("should stop alerting", function() {
icView.accept();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
});
describe("#decline", function() {
beforeEach(function() {
icView = mountTestComponent();
icView._websocket = {
decline: sinon.stub(),
close: sinon.stub()
};
conversation.set({
windowId: "8699"
});
conversation.setIncomingSessionData({
websocketToken: 123
});
});
it("should close the window", function() {
icView.decline();
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
});
it("should stop alerting", function() {
icView.decline();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
it("should release callData", function() {
icView.decline();
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "8699");
});
});
describe("#blocked", function() {
var mozLoop, deleteCallUrlStub;
beforeEach(function() {
icView = mountTestComponent();
icView._websocket = {
decline: sinon.spy(),
close: sinon.stub()
};
mozLoop = {
LOOP_SESSION_TYPE: {
GUEST: 1,
FXA: 2
}
};
deleteCallUrlStub = sandbox.stub(loop.Client.prototype,
"deleteCallUrl");
});
it("should call mozLoop.stopAlerting", function() {
icView.declineAndBlock();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
it("should call delete call", function() {
sandbox.stub(conversation, "get").withArgs("callToken")
.returns("fakeToken")
.withArgs("sessionType")
.returns(mozLoop.LOOP_SESSION_TYPE.FXA);
icView.declineAndBlock();
sinon.assert.calledOnce(deleteCallUrlStub);
sinon.assert.calledWithExactly(deleteCallUrlStub,
"fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func);
});
it("should get callToken from conversation model", function() {
sandbox.stub(conversation, "get");
icView.declineAndBlock();
sinon.assert.called(conversation.get);
sinon.assert.calledWithExactly(conversation.get, "callToken");
sinon.assert.calledWithExactly(conversation.get, "windowId");
});
it("should trigger error handling in case of error", function() {
// XXX just logging to console for now
var log = sandbox.stub(console, "log");
var fakeError = {
error: true
};
deleteCallUrlStub.callsArgWith(2, fakeError);
icView.declineAndBlock();
sinon.assert.calledOnce(log);
sinon.assert.calledWithExactly(log, fakeError);
});
it("should close the window", function() {
icView.declineAndBlock();
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
});
});
});
describe("Events", function() {
var fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
return new Promise(function() {});
},
on: sandbox.spy()
});
icView = mountTestComponent();
conversation.set("loopToken", "fakeToken");
stubComponent(sharedView, "ConversationView");
});
describe("call:accepted", function() {
it("should display the ConversationView",
function() {
conversation.accepted();
TestUtils.findRenderedComponentWithType(icView,
sharedView.ConversationView);
});
it("should set the title to the call identifier", function() {
sandbox.stub(conversation, "getCallIdentifier").returns("fakeId");
conversation.accepted();
expect(document.title).eql("fakeId");
});
});
describe("session:ended", function() {
it("should display the feedback view when the call session ends",
function() {
conversation.trigger("session:ended");
TestUtils.findRenderedComponentWithType(icView,
sharedView.FeedbackView);
});
});
describe("session:peer-hungup", function() {
it("should display the feedback view when the peer hangs up",
function() {
conversation.trigger("session:peer-hungup");
TestUtils.findRenderedComponentWithType(icView,
sharedView.FeedbackView);
});
});
describe("session:network-disconnected", function() {
it("should navigate to call failed when network disconnects",
function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(icView,
loop.conversationViews.GenericFailureView);
});
it("should update the conversation window toolbar title",
function() {
conversation.trigger("session:network-disconnected");
expect(document.title).eql("generic_failure_title");
});
});
describe("Published and Subscribed Streams", function() {
beforeEach(function() {
icView._websocket = {
mediaUp: sinon.spy()
};
});
describe("publishStream", function() {
it("should not notify the websocket if only one stream is up",
function() {
conversation.set("publishedStream", true);
sinon.assert.notCalled(icView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
"are connected", function() {
conversation.set("subscribedStream", true);
conversation.set("publishedStream", true);
sinon.assert.calledOnce(icView._websocket.mediaUp);
});
});
describe("subscribedStream", function() {
it("should not notify the websocket if only one stream is up",
function() {
conversation.set("subscribedStream", true);
sinon.assert.notCalled(icView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
"are connected", function() {
conversation.set("publishedStream", true);
conversation.set("subscribedStream", true);
sinon.assert.calledOnce(icView._websocket.mediaUp);
});
});
});
});
});
describe("IncomingCallView", function() {
var view, model, fakeAudio;
beforeEach(function() {
var Model = Backbone.Model.extend({
getCallIdentifier: function() {return "fakeId";}
});
model = new Model();
sandbox.spy(model, "trigger");
sandbox.stub(model, "set");
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
view = TestUtils.renderIntoDocument(
loop.conversationViews.IncomingCallView({
model: model,
video: true
}));
});
describe("default answer mode", function() {
it("should display video as primary answer mode", function() {
view = TestUtils.renderIntoDocument(
loop.conversationViews.IncomingCallView({
model: model,
video: true
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-video');
expect(primaryBtn).not.to.eql(null);
});
it("should display audio as primary answer mode", function() {
view = TestUtils.renderIntoDocument(
loop.conversationViews.IncomingCallView({
model: model,
video: false
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-audio');
expect(primaryBtn).not.to.eql(null);
});
it("should accept call with video", function() {
view = TestUtils.renderIntoDocument(
loop.conversationViews.IncomingCallView({
model: model,
video: true
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-video');
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with audio", function() {
view = TestUtils.renderIntoDocument(
loop.conversationViews.IncomingCallView({
model: model,
video: false
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-audio');
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with video when clicking on secondary btn",
function() {
view = TestUtils.renderIntoDocument(
loop.conversationViews.IncomingCallView({
model: model,
video: false
}));
var secondaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-video-small');
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with audio when clicking on secondary btn",
function() {
view = TestUtils.renderIntoDocument(
loop.conversationViews.IncomingCallView({
model: model,
video: true
}));
var secondaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-audio-small');
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
});
describe("click event on .btn-accept", function() {
it("should trigger an 'accept' conversation model event", function () {
var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
model.trigger.withArgs("accept");
TestUtils.Simulate.click(buttonAccept);
/* Setting a model property triggers 2 events */
sinon.assert.calledOnce(model.trigger.withArgs("accept"));
});
it("should set selectedCallType to audio-video", function () {
var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
TestUtils.Simulate.click(buttonAccept);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType",
"audio-video");
});
});
describe("click event on .btn-decline", function() {
it("should trigger an 'decline' conversation model event", function() {
var buttonDecline = view.getDOMNode().querySelector(".btn-decline");
TestUtils.Simulate.click(buttonDecline);
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWith(model.trigger, "decline");
});
});
describe("click event on .btn-block", function() {
it("should trigger a 'block' conversation model event", function() {
var buttonBlock = view.getDOMNode().querySelector(".btn-block");
TestUtils.Simulate.click(buttonBlock);
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWith(model.trigger, "declineAndBlock");
});
});
});
describe("GenericFailureView", function() {
var view, fakeAudio;
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
navigator.mozLoop.doNotDisturb = false;
sandbox.stub(window, "Audio").returns(fakeAudio);
view = TestUtils.renderIntoDocument(
loop.conversationViews.GenericFailureView({
cancelCall: function() {}
})
);
});
it("should play a failure sound, once", function() {
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"failure", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false);
});
});
});

Просмотреть файл

@ -10,26 +10,11 @@ describe("loop.conversation", function() {
"use strict";
var sharedModels = loop.shared.models,
sharedView = loop.shared.views,
fakeWindow,
sandbox;
// XXX refactor to Just Work with "sandbox.stubComponent" or else
// just pass in the sandbox and put somewhere generally usable
function stubComponent(obj, component, mockTagName){
var reactClass = React.createClass({
render: function() {
var mockTagName = mockTagName || "div";
return React.DOM[mockTagName](null, this.props.children);
}
});
return sandbox.stub(obj, component, reactClass);
}
beforeEach(function() {
sandbox = sinon.sandbox.create();
sandbox.useFakeTimers();
navigator.mozLoop = {
doNotDisturb: true,
@ -47,9 +32,6 @@ describe("loop.conversation", function() {
return "http://fake";
},
calls: {
clearCallInProgress: sinon.stub()
},
LOOP_SESSION_TYPE: {
GUEST: 1,
FXA: 2
@ -220,7 +202,7 @@ describe("loop.conversation", function() {
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversation.IncomingConversationView);
loop.conversationViews.IncomingConversationView);
});
it("should display the RoomView for rooms", function() {
@ -238,717 +220,7 @@ describe("loop.conversation", function() {
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversation.GenericFailureView);
loop.conversationViews.GenericFailureView);
});
});
describe("IncomingConversationView", function() {
var conversationAppStore, conversation, client, icView, oldTitle,
feedbackStore;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversation.IncomingConversationView({
client: client,
conversation: conversation,
sdk: {},
conversationAppStore: conversationAppStore,
feedbackStore: feedbackStore
}));
}
beforeEach(function() {
oldTitle = document.title;
client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {}
});
conversation.set({windowId: 42});
var dispatcher = new loop.Dispatcher();
conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
sandbox.stub(conversation, "setOutgoingSessionData");
});
afterEach(function() {
icView = undefined;
document.title = oldTitle;
});
describe("start", function() {
it("should set the title to incoming_call_title2", function() {
conversationAppStore.setStoreState({
windowData: {
progressURL: "fake",
websocketToken: "fake",
callId: 42
}
});
icView = mountTestComponent();
expect(document.title).eql("incoming_call_title2");
});
});
describe("componentDidMount", function() {
var fakeSessionData, promise, resolveWebSocketConnect;
var rejectWebSocketConnect;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: "7b"
};
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
stubComponent(loop.conversation, "IncomingCallView");
stubComponent(sharedView, "ConversationView");
});
it("should start alerting", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(navigator.mozLoop.startAlerting);
});
describe("Session Data setup", function() {
beforeEach(function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function () {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
return promise;
},
on: sinon.stub()
});
});
it("should store the session data", function() {
sandbox.stub(conversation, "setIncomingSessionData");
icView = mountTestComponent();
sinon.assert.calledOnce(conversation.setIncomingSessionData);
sinon.assert.calledWithExactly(conversation.setIncomingSessionData,
fakeSessionData);
});
it("should setup the websocket connection", function() {
icView = mountTestComponent();
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
callId: "Hello",
url: "http://progress.example.com",
websocketToken: "7b"
});
});
});
describe("WebSocket Handling", function() {
beforeEach(function() {
promise = new Promise(function(resolve, reject) {
resolveWebSocketConnect = resolve;
rejectWebSocketConnect = reject;
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
});
it("should set the state to incoming on success", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("incoming");
promise.then(function () {
expect(icView.state.callStatus).eql("incoming");
done();
});
});
it("should set the state to close on success if the progress " +
"state is terminated", function(done) {
icView = mountTestComponent();
resolveWebSocketConnect("terminated");
promise.then(function () {
expect(icView.state.callStatus).eql("close");
done();
});
});
// XXX implement me as part of bug 1047410
// see https://hg.mozilla.org/integration/fx-team/rev/5d2c69ebb321#l18.259
it.skip("should should switch view state to failed", function(done) {
icView = mountTestComponent();
rejectWebSocketConnect();
promise.then(function() {}, function() {
done();
});
});
});
describe("WebSocket Events", function() {
describe("Call cancelled or timed out before acceptance", function() {
beforeEach(function() {
// Mounting the test component automatically calls the required
// setup functions
icView = mountTestComponent();
promise = new Promise(function(resolve, reject) {
resolve();
});
sandbox.stub(loop.CallConnectionWebSocket.prototype, "promiseConnect").returns(promise);
sandbox.stub(loop.CallConnectionWebSocket.prototype, "close");
});
describe("progress - terminated (previousState = alerting)", function() {
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "timeout"
}, "alerting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
it("should close the websocket", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "closed"
}, "alerting");
sinon.assert.calledOnce(icView._websocket.close);
done();
});
});
it("should close the window", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "answered-elsewhere"
}, "alerting");
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
done();
});
});
});
describe("progress - terminated (previousState not init" +
" nor alerting)",
function() {
it("should set the state to end", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "media-fail"
}, "connecting");
expect(icView.state.callStatus).eql("end");
done();
});
});
it("should stop alerting", function(done) {
promise.then(function() {
icView._websocket.trigger("progress", {
state: "terminated",
reason: "media-fail"
}, "connecting");
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
done();
});
});
});
});
});
describe("#accept", function() {
beforeEach(function() {
icView = mountTestComponent();
conversation.setIncomingSessionData({
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey",
callType: "callType",
callId: "Hello",
progressURL: "http://progress.example.com",
websocketToken: 123
});
sandbox.stub(icView._websocket, "accept");
sandbox.stub(icView.props.conversation, "accepted");
});
it("should initiate the conversation", function() {
icView.accept();
sinon.assert.calledOnce(icView.props.conversation.accepted);
});
it("should notify the websocket of the user acceptance", function() {
icView.accept();
sinon.assert.calledOnce(icView._websocket.accept);
});
it("should stop alerting", function() {
icView.accept();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
});
describe("#decline", function() {
beforeEach(function() {
icView = mountTestComponent();
icView._websocket = {
decline: sinon.stub(),
close: sinon.stub()
};
conversation.set({
windowId: "8699"
});
conversation.setIncomingSessionData({
websocketToken: 123
});
});
it("should close the window", function() {
icView.decline();
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
});
it("should stop alerting", function() {
icView.decline();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
it("should release callData", function() {
icView.decline();
sinon.assert.calledOnce(navigator.mozLoop.calls.clearCallInProgress);
sinon.assert.calledWithExactly(
navigator.mozLoop.calls.clearCallInProgress, "8699");
});
});
describe("#blocked", function() {
var mozLoop, deleteCallUrlStub;
beforeEach(function() {
icView = mountTestComponent();
icView._websocket = {
decline: sinon.spy(),
close: sinon.stub()
};
mozLoop = {
LOOP_SESSION_TYPE: {
GUEST: 1,
FXA: 2
}
};
deleteCallUrlStub = sandbox.stub(loop.Client.prototype,
"deleteCallUrl");
});
it("should call mozLoop.stopAlerting", function() {
icView.declineAndBlock();
sinon.assert.calledOnce(navigator.mozLoop.stopAlerting);
});
it("should call delete call", function() {
sandbox.stub(conversation, "get").withArgs("callToken")
.returns("fakeToken")
.withArgs("sessionType")
.returns(mozLoop.LOOP_SESSION_TYPE.FXA);
icView.declineAndBlock();
sinon.assert.calledOnce(deleteCallUrlStub);
sinon.assert.calledWithExactly(deleteCallUrlStub,
"fakeToken", mozLoop.LOOP_SESSION_TYPE.FXA, sinon.match.func);
});
it("should get callToken from conversation model", function() {
sandbox.stub(conversation, "get");
icView.declineAndBlock();
sinon.assert.called(conversation.get);
sinon.assert.calledWithExactly(conversation.get, "callToken");
sinon.assert.calledWithExactly(conversation.get, "windowId");
});
it("should trigger error handling in case of error", function() {
// XXX just logging to console for now
var log = sandbox.stub(console, "log");
var fakeError = {
error: true
};
deleteCallUrlStub.callsArgWith(2, fakeError);
icView.declineAndBlock();
sinon.assert.calledOnce(log);
sinon.assert.calledWithExactly(log, fakeError);
});
it("should close the window", function() {
icView.declineAndBlock();
sandbox.clock.tick(1);
sinon.assert.calledOnce(fakeWindow.close);
});
});
});
describe("Events", function() {
var fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
apiKey: "apiKey"
};
conversationAppStore.setStoreState({
windowData: fakeSessionData
});
sandbox.stub(conversation, "setIncomingSessionData");
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() {
return new Promise(function() {});
},
on: sandbox.spy()
});
icView = mountTestComponent();
conversation.set("loopToken", "fakeToken");
stubComponent(sharedView, "ConversationView");
});
describe("call:accepted", function() {
it("should display the ConversationView",
function() {
conversation.accepted();
TestUtils.findRenderedComponentWithType(icView,
sharedView.ConversationView);
});
it("should set the title to the call identifier", function() {
sandbox.stub(conversation, "getCallIdentifier").returns("fakeId");
conversation.accepted();
expect(document.title).eql("fakeId");
});
});
describe("session:ended", function() {
it("should display the feedback view when the call session ends",
function() {
conversation.trigger("session:ended");
TestUtils.findRenderedComponentWithType(icView,
sharedView.FeedbackView);
});
});
describe("session:peer-hungup", function() {
it("should display the feedback view when the peer hangs up",
function() {
conversation.trigger("session:peer-hungup");
TestUtils.findRenderedComponentWithType(icView,
sharedView.FeedbackView);
});
});
describe("session:network-disconnected", function() {
it("should navigate to call failed when network disconnects",
function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(icView,
loop.conversation.GenericFailureView);
});
it("should update the conversation window toolbar title",
function() {
conversation.trigger("session:network-disconnected");
expect(document.title).eql("generic_failure_title");
});
});
describe("Published and Subscribed Streams", function() {
beforeEach(function() {
icView._websocket = {
mediaUp: sinon.spy()
};
});
describe("publishStream", function() {
it("should not notify the websocket if only one stream is up",
function() {
conversation.set("publishedStream", true);
sinon.assert.notCalled(icView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
"are connected", function() {
conversation.set("subscribedStream", true);
conversation.set("publishedStream", true);
sinon.assert.calledOnce(icView._websocket.mediaUp);
});
});
describe("subscribedStream", function() {
it("should not notify the websocket if only one stream is up",
function() {
conversation.set("subscribedStream", true);
sinon.assert.notCalled(icView._websocket.mediaUp);
});
it("should notify the websocket that media is up if both streams" +
"are connected", function() {
conversation.set("publishedStream", true);
conversation.set("subscribedStream", true);
sinon.assert.calledOnce(icView._websocket.mediaUp);
});
});
});
});
});
describe("IncomingCallView", function() {
var view, model, fakeAudio;
beforeEach(function() {
var Model = Backbone.Model.extend({
getCallIdentifier: function() {return "fakeId";}
});
model = new Model();
sandbox.spy(model, "trigger");
sandbox.stub(model, "set");
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: true
}));
});
describe("default answer mode", function() {
it("should display video as primary answer mode", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: true
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-video');
expect(primaryBtn).not.to.eql(null);
});
it("should display audio as primary answer mode", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: false
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-audio');
expect(primaryBtn).not.to.eql(null);
});
it("should accept call with video", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: true
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-video');
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with audio", function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: false
}));
var primaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-icon-audio');
React.addons.TestUtils.Simulate.click(primaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with video when clicking on secondary btn",
function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: false
}));
var secondaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-video-small');
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
it("should accept call with audio when clicking on secondary btn",
function() {
view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
model: model,
video: true
}));
var secondaryBtn = view.getDOMNode()
.querySelector('.fx-embedded-btn-audio-small');
React.addons.TestUtils.Simulate.click(secondaryBtn);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWithExactly(model.trigger, "accept");
});
});
describe("click event on .btn-accept", function() {
it("should trigger an 'accept' conversation model event", function () {
var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
model.trigger.withArgs("accept");
TestUtils.Simulate.click(buttonAccept);
/* Setting a model property triggers 2 events */
sinon.assert.calledOnce(model.trigger.withArgs("accept"));
});
it("should set selectedCallType to audio-video", function () {
var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
TestUtils.Simulate.click(buttonAccept);
sinon.assert.calledOnce(model.set);
sinon.assert.calledWithExactly(model.set, "selectedCallType",
"audio-video");
});
});
describe("click event on .btn-decline", function() {
it("should trigger an 'decline' conversation model event", function() {
var buttonDecline = view.getDOMNode().querySelector(".btn-decline");
TestUtils.Simulate.click(buttonDecline);
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWith(model.trigger, "decline");
});
});
describe("click event on .btn-block", function() {
it("should trigger a 'block' conversation model event", function() {
var buttonBlock = view.getDOMNode().querySelector(".btn-block");
TestUtils.Simulate.click(buttonBlock);
sinon.assert.calledOnce(model.trigger);
sinon.assert.calledWith(model.trigger, "declineAndBlock");
});
});
});
describe("GenericFailureView", function() {
var view, fakeAudio;
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
navigator.mozLoop.doNotDisturb = false;
sandbox.stub(window, "Audio").returns(fakeAudio);
view = TestUtils.renderIntoDocument(
loop.conversation.GenericFailureView({
cancelCall: function() {}
})
);
});
it("should play a failure sound, once", function() {
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
"failure", sinon.match.func);
sinon.assert.calledOnce(fakeAudio.play);
expect(fakeAudio.loop).to.equal(false);
});
});
});

Просмотреть файл

@ -321,7 +321,7 @@ describe("loop.roomViews", function () {
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversation.GenericFailureView);
loop.conversationViews.GenericFailureView);
});
it("should render the GenericFailureView if the roomState is `FULL`",
@ -331,7 +331,7 @@ describe("loop.roomViews", function () {
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversation.GenericFailureView);
loop.conversationViews.GenericFailureView);
});
it("should render the DesktopRoomInvitationView if roomState is `JOINED`",

Просмотреть файл

@ -18,7 +18,7 @@
// 1.1 Panel
var PanelView = loop.panel.PanelView;
// 1.2. Conversation Window
var IncomingCallView = loop.conversation.IncomingCallView;
var IncomingCallView = loop.conversationViews.IncomingCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var CallFailedView = loop.conversationViews.CallFailedView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;

Просмотреть файл

@ -18,7 +18,7 @@
// 1.1 Panel
var PanelView = loop.panel.PanelView;
// 1.2. Conversation Window
var IncomingCallView = loop.conversation.IncomingCallView;
var IncomingCallView = loop.conversationViews.IncomingCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var CallFailedView = loop.conversationViews.CallFailedView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;

Просмотреть файл

@ -85,7 +85,7 @@ FontInspector.prototype = {
/**
* Retrieve all the font info for the selected node and display it.
*/
update: Task.async(function*() {
update: Task.async(function*(showAllFonts) {
let node = this.inspector.selection.nodeFront;
if (!node ||
@ -104,10 +104,16 @@ FontInspector.prototype = {
includePreviews: true,
previewFillStyle: fillStyle
}
let fonts = yield this.pageStyle.getUsedFontFaces(node, options)
let fonts = [];
if (showAllFonts){
fonts = yield this.pageStyle.getAllUsedFontFaces(options)
.then(null, console.error);
if (!fonts) {
}
else{
fonts = yield this.pageStyle.getUsedFontFaces(node, options)
.then(null, console.error);
}
if (!fonts || !fonts.length) {
return;
}
@ -169,21 +175,10 @@ FontInspector.prototype = {
},
/**
* Select the <body> to show all the fonts included in the document.
* Show all fonts for the document (including iframes)
*/
showAll: function FI_showAll() {
if (!this.isActive() ||
!this.inspector.selection.isConnected() ||
!this.inspector.selection.isElementNode()) {
return;
}
// Select the body node to show all fonts
let walker = this.inspector.walker;
walker.getRootNode().then(root => walker.querySelector(root, "body")).then(body => {
this.inspector.selection.setNodeFront(body, "fontinspector");
});
this.update(true);
},
}

Просмотреть файл

@ -2,6 +2,7 @@
subsuite = devtools
support-files =
browser_fontinspector.html
test_iframe.html
ostrich-black.ttf
ostrich-regular.ttf
head.js

Просмотреть файл

@ -45,6 +45,7 @@
<body>
BODY
<div>DIV</div>
<iframe src="test_iframe.html"></iframe>
<div class="normal-text">NORMAL DIV</div>
<div class="bold-text">BOLD DIV</div>
<div class="black-text">800 DIV</div>

Просмотреть файл

@ -107,7 +107,8 @@ function* testShowAllFonts(inspector) {
viewDoc.querySelector("#showall").click();
yield updated;
is(inspector.selection.nodeFront.nodeName, "BODY", "Show all fonts selected the body node");
// shouldn't change the node selection
is(inspector.selection.nodeFront.nodeName, "DIV", "Show all fonts selected");
let sections = viewDoc.querySelectorAll("#all-fonts > section");
is(sections.length, 5, "And font-inspector still shows 5 fonts for body");
is(sections.length, 6, "Font inspector shows 6 fonts (1 from iframe)");
}

Просмотреть файл

@ -0,0 +1,11 @@
<!DOCTYPE html>
<style>
div{
font-family: "Times New Roman";
}
</style>
<body>
<div>Hello world</div>
</body>

Просмотреть файл

@ -1,4 +1,4 @@
This is the pdf.js project output, https://github.com/mozilla/pdf.js
Current extension version is: 1.0.978
Current extension version is: 1.0.1040

Просмотреть файл

@ -1,3 +1,5 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -12,8 +14,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* jshint esnext:true */
/* globals Components, Services, XPCOMUtils, PdfjsChromeUtils, PdfRedirector,
PdfjsContentUtils, DEFAULT_PREFERENCES, PdfStreamConverter */
var EXPORTED_SYMBOLS = ["PdfJs"];
'use strict';
var EXPORTED_SYMBOLS = ['PdfJs'];
const Cc = Components.classes;
const Ci = Components.interfaces;
@ -25,11 +32,12 @@ const PREF_PREFIX = 'pdfjs';
const PREF_DISABLED = PREF_PREFIX + '.disabled';
const PREF_MIGRATION_VERSION = PREF_PREFIX + '.migrationVersion';
const PREF_PREVIOUS_ACTION = PREF_PREFIX + '.previousHandler.preferredAction';
const PREF_PREVIOUS_ASK = PREF_PREFIX + '.previousHandler.alwaysAskBeforeHandling';
const PREF_PREVIOUS_ASK = PREF_PREFIX +
'.previousHandler.alwaysAskBeforeHandling';
const PREF_DISABLED_PLUGIN_TYPES = 'plugin.disable_full_page_plugin_for_types';
const TOPIC_PDFJS_HANDLER_CHANGED = 'pdfjs:handlerChanged';
const TOPIC_PLUGINS_LIST_UPDATED = "plugins-list-updated";
const TOPIC_PLUGIN_INFO_UPDATED = "plugin-info-updated";
const TOPIC_PLUGINS_LIST_UPDATED = 'plugins-list-updated';
const TOPIC_PLUGIN_INFO_UPDATED = 'plugin-info-updated';
const PDF_CONTENT_TYPE = 'application/pdf';
Cu.import('resource://gre/modules/XPCOMUtils.jsm');
@ -42,10 +50,10 @@ XPCOMUtils.defineLazyServiceGetter(Svc, 'mime',
XPCOMUtils.defineLazyServiceGetter(Svc, 'pluginHost',
'@mozilla.org/plugin/host;1',
'nsIPluginHost');
XPCOMUtils.defineLazyModuleGetter(this, "PdfjsChromeUtils",
"resource://pdf.js/PdfjsChromeUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PdfjsContentUtils",
"resource://pdf.js/PdfjsContentUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsChromeUtils',
'resource://pdf.js/PdfjsChromeUtils.jsm');
XPCOMUtils.defineLazyModuleGetter(this, 'PdfjsContentUtils',
'resource://pdf.js/PdfjsContentUtils.jsm');
function getBoolPref(aPref, aDefaultValue) {
try {
@ -64,7 +72,7 @@ function getIntPref(aPref, aDefaultValue) {
}
function isDefaultHandler() {
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
if (Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_CONTENT) {
return PdfjsContentUtils.isDefaultHandlerApp();
}
return PdfjsChromeUtils.isDefaultHandlerApp();
@ -134,8 +142,10 @@ let PdfJs = {
_initialized: false,
init: function init(remote) {
if (Services.appinfo.processType != Services.appinfo.PROCESS_TYPE_DEFAULT) {
throw new Error("PdfJs.init should only get called in the parent process.");
if (Services.appinfo.processType !==
Services.appinfo.PROCESS_TYPE_DEFAULT) {
throw new Error('PdfJs.init should only get called ' +
'in the parent process.');
}
PdfjsChromeUtils.init();
if (!remote) {
@ -239,9 +249,9 @@ let PdfJs = {
prefs.setCharPref(PREF_DISABLED_PLUGIN_TYPES, types.join(','));
// Update the category manager in case the plugins are already loaded.
let categoryManager = Cc["@mozilla.org/categorymanager;1"];
let categoryManager = Cc['@mozilla.org/categorymanager;1'];
categoryManager.getService(Ci.nsICategoryManager).
deleteCategoryEntry("Gecko-Content-Viewers",
deleteCategoryEntry('Gecko-Content-Viewers',
PDF_CONTENT_TYPE,
false);
},
@ -249,8 +259,9 @@ let PdfJs = {
// nsIObserver
observe: function observe(aSubject, aTopic, aData) {
this.updateRegistration();
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT) {
let jsm = "resource://pdf.js/PdfjsChromeUtils.jsm";
if (Services.appinfo.processType ===
Services.appinfo.PROCESS_TYPE_DEFAULT) {
let jsm = 'resource://pdf.js/PdfjsChromeUtils.jsm';
let PdfjsChromeUtils = Components.utils.import(jsm, {}).PdfjsChromeUtils;
PdfjsChromeUtils.notifyChildOfSettingsChange();
}
@ -282,9 +293,9 @@ let PdfJs = {
}
// Check if there is an enabled pdf plugin.
// Note: this check is performed last because getPluginTags() triggers costly
// plugin list initialization (bug 881575)
let tags = Cc["@mozilla.org/plugin/host;1"].
// Note: this check is performed last because getPluginTags() triggers
// costly plugin list initialization (bug 881575)
let tags = Cc['@mozilla.org/plugin/host;1'].
getService(Ci.nsIPluginHost).
getPluginTags();
let enabledPluginFound = tags.some(function(tag) {
@ -302,9 +313,9 @@ let PdfJs = {
},
_ensureRegistered: function _ensureRegistered() {
if (this._registered)
if (this._registered) {
return;
}
this._pdfStreamConverterFactory = new Factory();
Cu.import('resource://pdf.js/PdfStreamConverter.jsm');
this._pdfStreamConverterFactory.register(PdfStreamConverter);
@ -320,9 +331,9 @@ let PdfJs = {
},
_ensureUnregistered: function _ensureUnregistered() {
if (!this._registered)
if (!this._registered) {
return;
}
this._pdfStreamConverterFactory.unregister();
Cu.unload('resource://pdf.js/PdfStreamConverter.jsm');
delete this._pdfStreamConverterFactory;

Просмотреть файл

@ -14,7 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* jshint esnext:true */
/* jshint esnext:true, maxlen: 100 */
/* globals Components, Services */
'use strict';
@ -25,47 +26,47 @@ Cu.import('resource://gre/modules/Services.jsm');
this.PdfJsTelemetry = {
onViewerIsUsed: function () {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_USED");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_USED');
histogram.add(true);
},
onFallback: function () {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FALLBACK_SHOWN");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FALLBACK_SHOWN');
histogram.add(true);
},
onDocumentSize: function (size) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_SIZE_KB");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_SIZE_KB');
histogram.add(size / 1024);
},
onDocumentVersion: function (versionId) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_VERSION");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_VERSION');
histogram.add(versionId);
},
onDocumentGenerator: function (generatorId) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_DOCUMENT_GENERATOR");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_DOCUMENT_GENERATOR');
histogram.add(generatorId);
},
onEmbed: function (isObject) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_EMBED");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_EMBED');
histogram.add(isObject);
},
onFontType: function (fontTypeId) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FONT_TYPES");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FONT_TYPES');
histogram.add(fontTypeId);
},
onForm: function (isAcroform) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_FORM");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_FORM');
histogram.add(isAcroform);
},
onPrint: function () {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_PRINT");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_PRINT');
histogram.add(true);
},
onStreamType: function (streamTypeId) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_STREAM_TYPES");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_STREAM_TYPES');
histogram.add(streamTypeId);
},
onTimeToView: function (ms) {
let histogram = Services.telemetry.getHistogramById("PDF_VIEWER_TIME_TO_VIEW_MS");
let histogram = Services.telemetry.getHistogramById('PDF_VIEWER_TIME_TO_VIEW_MS');
histogram.add(ms);
}
};

Просмотреть файл

@ -16,7 +16,7 @@
*/
/* jshint esnext:true */
/* globals Components, Services, XPCOMUtils, NetUtil, PrivateBrowsingUtils,
dump, NetworkManager, PdfJsTelemetry */
dump, NetworkManager, PdfJsTelemetry, PdfjsContentUtils */
'use strict';
@ -143,20 +143,23 @@ function getLocalizedStrings(path) {
property = key.substring(i + 1);
key = key.substring(0, i);
}
if (!(key in map))
if (!(key in map)) {
map[key] = {};
}
map[key][property] = string.value;
}
return map;
}
function getLocalizedString(strings, id, property) {
property = property || 'textContent';
if (id in strings)
if (id in strings) {
return strings[id][property];
}
return id;
}
function makeContentReadable(obj, window) {
/* jshint -W027 */
return Cu.cloneInto(obj, window);
}
@ -242,7 +245,7 @@ ChromeActions.prototype = {
// the original url.
var originalUri = NetUtil.newURI(data.originalUrl);
var filename = data.filename;
if (typeof filename !== 'string' ||
if (typeof filename !== 'string' ||
(!/\.pdf$/i.test(filename) && !data.isAttachment)) {
filename = 'document.pdf';
}
@ -261,8 +264,9 @@ ChromeActions.prototype = {
}
NetUtil.asyncFetch(netChannel, function(aInputStream, aResult) {
if (!Components.isSuccessCode(aResult)) {
if (sendResponse)
if (sendResponse) {
sendResponse(true);
}
return;
}
// Create a nsIInputStreamChannel so we can set the url on the channel
@ -296,11 +300,13 @@ ChromeActions.prototype = {
this.extListener.onStartRequest(aRequest, aContext);
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
if (this.extListener)
if (this.extListener) {
this.extListener.onStopRequest(aRequest, aContext, aStatusCode);
}
// Notify the content code we're done downloading.
if (sendResponse)
if (sendResponse) {
sendResponse(false);
}
},
onDataAvailable: function(aRequest, aContext, aInputStream, aOffset,
aCount) {
@ -318,9 +324,9 @@ ChromeActions.prototype = {
getStrings: function(data) {
try {
// Lazy initialization of localizedStrings
if (!('localizedStrings' in this))
if (!('localizedStrings' in this)) {
this.localizedStrings = getLocalizedStrings('viewer.properties');
}
var result = this.localizedStrings[data];
return JSON.stringify(result || null);
} catch (e) {
@ -373,10 +379,10 @@ ChromeActions.prototype = {
if (!documentStats || typeof documentStats !== 'object') {
break;
}
var streamTypes = documentStats.streamTypes;
var i, streamTypes = documentStats.streamTypes;
if (Array.isArray(streamTypes)) {
var STREAM_TYPE_ID_LIMIT = 20;
for (var i = 0; i < STREAM_TYPE_ID_LIMIT; i++) {
for (i = 0; i < STREAM_TYPE_ID_LIMIT; i++) {
if (streamTypes[i] &&
!this.telemetryState.streamTypesUsed[i]) {
PdfJsTelemetry.onStreamType(i);
@ -387,7 +393,7 @@ ChromeActions.prototype = {
var fontTypes = documentStats.fontTypes;
if (Array.isArray(fontTypes)) {
var FONT_TYPE_ID_LIMIT = 20;
for (var i = 0; i < FONT_TYPE_ID_LIMIT; i++) {
for (i = 0; i < FONT_TYPE_ID_LIMIT; i++) {
if (fontTypes[i] &&
!this.telemetryState.fontTypesUsed[i]) {
PdfJsTelemetry.onFontType(i);
@ -420,8 +426,9 @@ ChromeActions.prototype = {
getLocalizedString(strings, 'open_with_different_viewer', 'accessKey'));
},
updateFindControlState: function(data) {
if (!this.supportsIntegratedFind())
if (!this.supportsIntegratedFind()) {
return;
}
// Verify what we're sending to the findbar.
var result = data.result;
var findPrevious = data.findPrevious;
@ -706,11 +713,11 @@ RequestListener.prototype.receive = function(event) {
log('Unknown action: ' + action);
return;
}
var response;
if (sync) {
var response = actions[action].call(this.actions, data);
response = actions[action].call(this.actions, data);
event.detail.response = response;
} else {
var response;
if (!event.detail.responseExpected) {
doc.documentElement.removeChild(message);
response = null;
@ -718,7 +725,8 @@ RequestListener.prototype.receive = function(event) {
response = function sendResponse(response) {
try {
var listener = doc.createEvent('CustomEvent');
let detail = makeContentReadable({response: response}, doc.defaultView);
let detail = makeContentReadable({response: response},
doc.defaultView);
listener.initCustomEvent('pdf.js.response', true, false, detail);
return message.dispatchEvent(listener);
} catch (e) {
@ -987,10 +995,11 @@ PdfStreamConverter.prototype = {
return;
}
if (Components.isSuccessCode(aStatusCode))
if (Components.isSuccessCode(aStatusCode)) {
this.dataListener.finish();
else
} else {
this.dataListener.error(aStatusCode);
}
delete this.dataListener;
delete this.binaryStream;
}

Просмотреть файл

@ -14,7 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*globals DEFAULT_PREFERENCES */
/* jshint esnext:true */
/* globals Components, Services, XPCOMUtils, DEFAULT_PREFERENCES */
'use strict';
@ -67,39 +68,42 @@ let PdfjsChromeUtils = {
init: function () {
if (!this._ppmm) {
// global parent process message manager (PPMM)
this._ppmm = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIMessageBroadcaster);
this._ppmm.addMessageListener("PDFJS:Parent:clearUserPref", this);
this._ppmm.addMessageListener("PDFJS:Parent:setIntPref", this);
this._ppmm.addMessageListener("PDFJS:Parent:setBoolPref", this);
this._ppmm.addMessageListener("PDFJS:Parent:setCharPref", this);
this._ppmm.addMessageListener("PDFJS:Parent:setStringPref", this);
this._ppmm.addMessageListener("PDFJS:Parent:isDefaultHandlerApp", this);
this._ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1'].
getService(Ci.nsIMessageBroadcaster);
this._ppmm.addMessageListener('PDFJS:Parent:clearUserPref', this);
this._ppmm.addMessageListener('PDFJS:Parent:setIntPref', this);
this._ppmm.addMessageListener('PDFJS:Parent:setBoolPref', this);
this._ppmm.addMessageListener('PDFJS:Parent:setCharPref', this);
this._ppmm.addMessageListener('PDFJS:Parent:setStringPref', this);
this._ppmm.addMessageListener('PDFJS:Parent:isDefaultHandlerApp', this);
// global dom message manager (MMg)
this._mmg = Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageListenerManager);
this._mmg.addMessageListener("PDFJS:Parent:getChromeWindow", this);
this._mmg.addMessageListener("PDFJS:Parent:getFindBar", this);
this._mmg.addMessageListener("PDFJS:Parent:displayWarning", this);
this._mmg = Cc['@mozilla.org/globalmessagemanager;1'].
getService(Ci.nsIMessageListenerManager);
this._mmg.addMessageListener('PDFJS:Parent:getChromeWindow', this);
this._mmg.addMessageListener('PDFJS:Parent:getFindBar', this);
this._mmg.addMessageListener('PDFJS:Parent:displayWarning', this);
// observer to handle shutdown
Services.obs.addObserver(this, "quit-application", false);
Services.obs.addObserver(this, 'quit-application', false);
}
},
uninit: function () {
if (this._ppmm) {
this._ppmm.removeMessageListener("PDFJS:Parent:clearUserPref", this);
this._ppmm.removeMessageListener("PDFJS:Parent:setIntPref", this);
this._ppmm.removeMessageListener("PDFJS:Parent:setBoolPref", this);
this._ppmm.removeMessageListener("PDFJS:Parent:setCharPref", this);
this._ppmm.removeMessageListener("PDFJS:Parent:setStringPref", this);
this._ppmm.removeMessageListener("PDFJS:Parent:isDefaultHandlerApp", this);
this._ppmm.removeMessageListener('PDFJS:Parent:clearUserPref', this);
this._ppmm.removeMessageListener('PDFJS:Parent:setIntPref', this);
this._ppmm.removeMessageListener('PDFJS:Parent:setBoolPref', this);
this._ppmm.removeMessageListener('PDFJS:Parent:setCharPref', this);
this._ppmm.removeMessageListener('PDFJS:Parent:setStringPref', this);
this._ppmm.removeMessageListener('PDFJS:Parent:isDefaultHandlerApp',
this);
this._mmg.removeMessageListener("PDFJS:Parent:getChromeWindow", this);
this._mmg.removeMessageListener("PDFJS:Parent:getFindBar", this);
this._mmg.removeMessageListener("PDFJS:Parent:displayWarning", this);
this._mmg.removeMessageListener('PDFJS:Parent:getChromeWindow', this);
this._mmg.removeMessageListener('PDFJS:Parent:getFindBar', this);
this._mmg.removeMessageListener('PDFJS:Parent:displayWarning', this);
Services.obs.removeObserver(this, "quit-application", false);
Services.obs.removeObserver(this, 'quit-application', false);
this._mmg = null;
this._ppmm = null;
@ -113,14 +117,14 @@ let PdfjsChromeUtils = {
* the module's registration.
*/
notifyChildOfSettingsChange: function () {
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_DEFAULT &&
this._ppmm) {
if (Services.appinfo.processType ===
Services.appinfo.PROCESS_TYPE_DEFAULT && this._ppmm) {
// XXX kinda bad, we want to get the parent process mm associated
// with the content process. _ppmm is currently the global process
// manager, which means this is going to fire to every child process
// we have open. Unfortunately I can't find a way to get at that
// process specific mm from js.
this._ppmm.broadcastAsyncMessage("PDFJS:Child:refreshSettings", {});
this._ppmm.broadcastAsyncMessage('PDFJS:Child:refreshSettings', {});
}
},
@ -129,38 +133,38 @@ let PdfjsChromeUtils = {
*/
observe: function(aSubject, aTopic, aData) {
if (aTopic == "quit-application") {
if (aTopic === 'quit-application') {
this.uninit();
}
},
receiveMessage: function (aMsg) {
switch (aMsg.name) {
case "PDFJS:Parent:clearUserPref":
case 'PDFJS:Parent:clearUserPref':
this._clearUserPref(aMsg.data.name);
break;
case "PDFJS:Parent:setIntPref":
case 'PDFJS:Parent:setIntPref':
this._setIntPref(aMsg.data.name, aMsg.data.value);
break;
case "PDFJS:Parent:setBoolPref":
case 'PDFJS:Parent:setBoolPref':
this._setBoolPref(aMsg.data.name, aMsg.data.value);
break;
case "PDFJS:Parent:setCharPref":
case 'PDFJS:Parent:setCharPref':
this._setCharPref(aMsg.data.name, aMsg.data.value);
break;
case "PDFJS:Parent:setStringPref":
case 'PDFJS:Parent:setStringPref':
this._setStringPref(aMsg.data.name, aMsg.data.value);
break;
case "PDFJS:Parent:isDefaultHandlerApp":
case 'PDFJS:Parent:isDefaultHandlerApp':
return this.isDefaultHandlerApp();
case "PDFJS:Parent:displayWarning":
case 'PDFJS:Parent:displayWarning':
this._displayWarning(aMsg);
break;
// CPOW getters
case "PDFJS:Parent:getChromeWindow":
case 'PDFJS:Parent:getChromeWindow':
return this._getChromeWindow(aMsg);
case "PDFJS:Parent:getFindBar":
case 'PDFJS:Parent:getFindBar':
return this._getFindBar(aMsg);
}
},
@ -193,8 +197,8 @@ let PdfjsChromeUtils = {
let unPrefixedName = aPrefName.split(PREF_PREFIX + '.');
if (unPrefixedName[0] !== '' ||
this._allowedPrefNames.indexOf(unPrefixedName[1]) === -1) {
let msg = "'" + aPrefName + "' ";
msg += "can't be accessed from content. See PdfjsChromeUtils."
let msg = '"' + aPrefName + '" ' +
'can\'t be accessed from content. See PdfjsChromeUtils.';
throw new Error(msg);
}
},
@ -234,8 +238,8 @@ let PdfjsChromeUtils = {
*/
isDefaultHandlerApp: function () {
var handlerInfo = Svc.mime.getFromTypeAndExtension(PDF_CONTENT_TYPE, 'pdf');
return !handlerInfo.alwaysAskBeforeHandling &&
handlerInfo.preferredAction == Ci.nsIHandlerInfo.handleInternally;
return (!handlerInfo.alwaysAskBeforeHandling &&
handlerInfo.preferredAction === Ci.nsIHandlerInfo.handleInternally);
},
/*
@ -290,13 +294,13 @@ function PdfjsFindbarWrapper(aBrowser) {
let tab;
tab = tabbrowser.getTabForBrowser(aBrowser);
this._findbar = tabbrowser.getFindBar(tab);
};
}
PdfjsFindbarWrapper.prototype = {
__exposedProps__: {
addEventListener: "r",
removeEventListener: "r",
updateControlState: "r",
addEventListener: 'r',
removeEventListener: 'r',
updateControlState: 'r',
},
_findbar: null,
@ -305,7 +309,8 @@ PdfjsFindbarWrapper.prototype = {
},
addEventListener: function (aType, aListener, aUseCapture, aWantsUntrusted) {
this._findbar.addEventListener(aType, aListener, aUseCapture, aWantsUntrusted);
this._findbar.addEventListener(aType, aListener, aUseCapture,
aWantsUntrusted);
},
removeEventListener: function (aType, aListener, aUseCapture) {
@ -315,11 +320,11 @@ PdfjsFindbarWrapper.prototype = {
function PdfjsWindowWrapper(aBrowser) {
this._window = aBrowser.ownerDocument.defaultView;
};
}
PdfjsWindowWrapper.prototype = {
__exposedProps__: {
valueOf: "r",
valueOf: 'r',
},
_window: null,
@ -328,4 +333,3 @@ PdfjsWindowWrapper.prototype = {
}
};

Просмотреть файл

@ -1,3 +1,5 @@
/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* Copyright 2012 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -12,6 +14,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* jshint esnext:true */
/* globals Components, Services, XPCOMUtils */
'use strict';
@ -33,23 +37,25 @@ let PdfjsContentUtils = {
*/
get isRemote() {
return Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT;
return (Services.appinfo.processType ===
Services.appinfo.PROCESS_TYPE_CONTENT);
},
init: function () {
// child *process* mm, or when loaded into the parent for in-content
// support the psuedo child process mm 'child PPMM'.
if (!this._mm) {
this._mm = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender);
this._mm.addMessageListener("PDFJS:Child:refreshSettings", this);
Services.obs.addObserver(this, "quit-application", false);
this._mm = Cc['@mozilla.org/childprocessmessagemanager;1'].
getService(Ci.nsISyncMessageSender);
this._mm.addMessageListener('PDFJS:Child:refreshSettings', this);
Services.obs.addObserver(this, 'quit-application', false);
}
},
uninit: function () {
if (this._mm) {
this._mm.removeMessageListener("PDFJS:Child:refreshSettings", this);
Services.obs.removeObserver(this, "quit-application");
this._mm.removeMessageListener('PDFJS:Child:refreshSettings', this);
Services.obs.removeObserver(this, 'quit-application');
}
this._mm = null;
},
@ -61,34 +67,34 @@ let PdfjsContentUtils = {
*/
clearUserPref: function (aPrefName) {
this._mm.sendSyncMessage("PDFJS:Parent:clearUserPref", {
this._mm.sendSyncMessage('PDFJS:Parent:clearUserPref', {
name: aPrefName
});
},
setIntPref: function (aPrefName, aPrefValue) {
this._mm.sendSyncMessage("PDFJS:Parent:setIntPref", {
this._mm.sendSyncMessage('PDFJS:Parent:setIntPref', {
name: aPrefName,
value: aPrefValue
});
},
setBoolPref: function (aPrefName, aPrefValue) {
this._mm.sendSyncMessage("PDFJS:Parent:setBoolPref", {
this._mm.sendSyncMessage('PDFJS:Parent:setBoolPref', {
name: aPrefName,
value: aPrefValue
});
},
setCharPref: function (aPrefName, aPrefValue) {
this._mm.sendSyncMessage("PDFJS:Parent:setCharPref", {
this._mm.sendSyncMessage('PDFJS:Parent:setCharPref', {
name: aPrefName,
value: aPrefValue
});
},
setStringPref: function (aPrefName, aPrefValue) {
this._mm.sendSyncMessage("PDFJS:Parent:setStringPref", {
this._mm.sendSyncMessage('PDFJS:Parent:setStringPref', {
name: aPrefName,
value: aPrefValue
});
@ -99,7 +105,7 @@ let PdfjsContentUtils = {
* handler app settings only available in the parent process.
*/
isDefaultHandlerApp: function () {
return this._mm.sendSyncMessage("PDFJS:Parent:isDefaultHandlerApp")[0];
return this._mm.sendSyncMessage('PDFJS:Parent:isDefaultHandlerApp')[0];
},
/*
@ -112,7 +118,7 @@ let PdfjsContentUtils = {
.getInterface(Ci.nsIDocShell)
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIContentFrameMessageManager);
winmm.sendAsyncMessage("PDFJS:Parent:displayWarning", {
winmm.sendAsyncMessage('PDFJS:Parent:displayWarning', {
message: aMessage,
label: aLabel,
accessKey: accessKey
@ -126,17 +132,18 @@ let PdfjsContentUtils = {
*/
observe: function(aSubject, aTopic, aData) {
if (aTopic == "quit-application") {
if (aTopic === 'quit-application') {
this.uninit();
}
},
receiveMessage: function (aMsg) {
switch (aMsg.name) {
case "PDFJS:Child:refreshSettings":
case 'PDFJS:Child:refreshSettings':
// Only react to this if we are remote.
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
let jsm = "resource://pdf.js/PdfJs.jsm";
if (Services.appinfo.processType ===
Services.appinfo.PROCESS_TYPE_CONTENT) {
let jsm = 'resource://pdf.js/PdfJs.jsm';
let pdfjs = Components.utils.import(jsm, {}).PdfJs;
pdfjs.updateRegistration();
}
@ -159,12 +166,14 @@ let PdfjsContentUtils = {
// send over a small container for the object we want.
let suitcase = {
_window: null,
setChromeWindow: function (aObj) { this._window = aObj; }
}
if (!winmm.sendSyncMessage("PDFJS:Parent:getChromeWindow", {},
setChromeWindow: function (aObj) {
this._window = aObj;
}
};
if (!winmm.sendSyncMessage('PDFJS:Parent:getChromeWindow', {},
{ suitcase: suitcase })[0]) {
Cu.reportError("A request for a CPOW wrapped chrome window " +
"failed for unknown reasons.");
Cu.reportError('A request for a CPOW wrapped chrome window ' +
'failed for unknown reasons.');
return null;
}
return suitcase._window;
@ -179,12 +188,14 @@ let PdfjsContentUtils = {
.getInterface(Ci.nsIContentFrameMessageManager);
let suitcase = {
_findbar: null,
setFindBar: function (aObj) { this._findbar = aObj; }
}
if (!winmm.sendSyncMessage("PDFJS:Parent:getFindBar", {},
setFindBar: function (aObj) {
this._findbar = aObj;
}
};
if (!winmm.sendSyncMessage('PDFJS:Parent:getFindBar', {},
{ suitcase: suitcase })[0]) {
Cu.reportError("A request for a CPOW wrapped findbar " +
"failed for unknown reasons.");
Cu.reportError('A request for a CPOW wrapped findbar ' +
'failed for unknown reasons.');
return null;
}
return suitcase._findbar;

Просмотреть файл

@ -22,8 +22,8 @@ if (typeof PDFJS === 'undefined') {
(typeof window !== 'undefined' ? window : this).PDFJS = {};
}
PDFJS.version = '1.0.978';
PDFJS.build = '20bf84a';
PDFJS.version = '1.0.1040';
PDFJS.build = '997096f';
(function pdfjsWrapper() {
// Use strict in our context only - users might not want it
@ -2803,7 +2803,13 @@ var MIN_FONT_SIZE = 16;
var MAX_FONT_SIZE = 100;
var MAX_GROUP_SIZE = 4096;
// Heuristic value used when enforcing minimum line widths.
var MIN_WIDTH_FACTOR = 0.65;
var COMPILE_TYPE3_GLYPHS = true;
var MAX_SIZE_TO_COMPILE = 1000;
var FULL_CHUNK_HEIGHT = 16;
function createScratchCanvas(width, height) {
var canvas = document.createElement('canvas');
@ -2937,7 +2943,7 @@ var CachedCanvases = (function CachedCanvasesClosure() {
getCanvas: function CachedCanvases_getCanvas(id, width, height,
trackTransform) {
var canvasEntry;
if (id in cache) {
if (cache[id] !== undefined) {
canvasEntry = cache[id];
canvasEntry.canvas.width = width;
canvasEntry.canvas.height = height;
@ -3151,6 +3157,7 @@ var CanvasExtraState = (function CanvasExtraStateClosure() {
// Default fore and background colors
this.fillColor = '#000000';
this.strokeColor = '#000000';
this.patternFill = false;
// Note: fill alpha applies to all non-stroking operations
this.fillAlpha = 1;
this.strokeAlpha = 1;
@ -3203,6 +3210,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
if (canvasCtx) {
addContextCurrentTransform(canvasCtx);
}
this.cachedGetSinglePixelWidth = null;
}
function putBinaryImageData(ctx, imgData) {
@ -3223,13 +3231,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
// that's ok; any such pixels are ignored.
var height = imgData.height, width = imgData.width;
var fullChunkHeight = 16;
var fracChunks = height / fullChunkHeight;
var fullChunks = Math.floor(fracChunks);
var totalChunks = Math.ceil(fracChunks);
var partialChunkHeight = height - fullChunks * fullChunkHeight;
var partialChunkHeight = height % FULL_CHUNK_HEIGHT;
var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
var chunkImgData = ctx.createImageData(width, fullChunkHeight);
var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
var srcPos = 0, destPos;
var src = imgData.data;
var dest = chunkImgData.data;
@ -3249,7 +3255,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
0xFF000000 : 0x000000FF;
for (i = 0; i < totalChunks; i++) {
thisChunkHeight =
(i < fullChunks) ? fullChunkHeight : partialChunkHeight;
(i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight;
destPos = 0;
for (j = 0; j < thisChunkHeight; j++) {
var srcDiff = srcLength - srcPos;
@ -3284,19 +3290,19 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
dest32[destPos++] = 0;
}
ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
}
} else if (imgData.kind === ImageKind.RGBA_32BPP) {
// RGBA, 32-bits per pixel.
j = 0;
elemsInThisChunk = width * fullChunkHeight * 4;
elemsInThisChunk = width * FULL_CHUNK_HEIGHT * 4;
for (i = 0; i < fullChunks; i++) {
dest.set(src.subarray(srcPos, srcPos + elemsInThisChunk));
srcPos += elemsInThisChunk;
ctx.putImageData(chunkImgData, 0, j);
j += fullChunkHeight;
j += FULL_CHUNK_HEIGHT;
}
if (i < totalChunks) {
elemsInThisChunk = width * partialChunkHeight * 4;
@ -3306,11 +3312,11 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
} else if (imgData.kind === ImageKind.RGB_24BPP) {
// RGB, 24-bits per pixel.
thisChunkHeight = fullChunkHeight;
thisChunkHeight = FULL_CHUNK_HEIGHT;
elemsInThisChunk = width * thisChunkHeight;
for (i = 0; i < totalChunks; i++) {
if (i >= fullChunks) {
thisChunkHeight =partialChunkHeight;
thisChunkHeight = partialChunkHeight;
elemsInThisChunk = width * thisChunkHeight;
}
@ -3321,7 +3327,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
dest[destPos++] = src[srcPos++];
dest[destPos++] = 255;
}
ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
}
} else {
error('bad image kind: ' + imgData.kind);
@ -3330,20 +3336,18 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
function putBinaryImageMask(ctx, imgData) {
var height = imgData.height, width = imgData.width;
var fullChunkHeight = 16;
var fracChunks = height / fullChunkHeight;
var fullChunks = Math.floor(fracChunks);
var totalChunks = Math.ceil(fracChunks);
var partialChunkHeight = height - fullChunks * fullChunkHeight;
var partialChunkHeight = height % FULL_CHUNK_HEIGHT;
var fullChunks = (height - partialChunkHeight) / FULL_CHUNK_HEIGHT;
var totalChunks = partialChunkHeight === 0 ? fullChunks : fullChunks + 1;
var chunkImgData = ctx.createImageData(width, fullChunkHeight);
var chunkImgData = ctx.createImageData(width, FULL_CHUNK_HEIGHT);
var srcPos = 0;
var src = imgData.data;
var dest = chunkImgData.data;
for (var i = 0; i < totalChunks; i++) {
var thisChunkHeight =
(i < fullChunks) ? fullChunkHeight : partialChunkHeight;
(i < fullChunks) ? FULL_CHUNK_HEIGHT : partialChunkHeight;
// Expand the mask so it can be used by the canvas. Any required
// inversion has already been handled.
@ -3360,7 +3364,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
mask >>= 1;
}
}
ctx.putImageData(chunkImgData, 0, i * fullChunkHeight);
ctx.putImageData(chunkImgData, 0, i * FULL_CHUNK_HEIGHT);
}
}
@ -3370,14 +3374,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
'globalCompositeOperation', 'font'];
for (var i = 0, ii = properties.length; i < ii; i++) {
var property = properties[i];
if (property in sourceCtx) {
if (sourceCtx[property] !== undefined) {
destCtx[property] = sourceCtx[property];
}
}
if ('setLineDash' in sourceCtx) {
if (sourceCtx.setLineDash !== undefined) {
destCtx.setLineDash(sourceCtx.getLineDash());
destCtx.lineDashOffset = sourceCtx.lineDashOffset;
} else if ('mozDash' in sourceCtx) {
} else if (sourceCtx.mozDashOffset !== undefined) {
destCtx.mozDash = sourceCtx.mozDash;
destCtx.mozDashOffset = sourceCtx.mozDashOffset;
}
@ -3604,7 +3608,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
},
setDash: function CanvasGraphics_setDash(dashArray, dashPhase) {
var ctx = this.ctx;
if ('setLineDash' in ctx) {
if (ctx.setLineDash !== undefined) {
ctx.setLineDash(dashArray);
ctx.lineDashOffset = dashPhase;
} else {
@ -3739,10 +3743,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
this.current = this.stateStack.pop();
this.ctx.restore();
this.cachedGetSinglePixelWidth = null;
}
},
transform: function CanvasGraphics_transform(a, b, c, d, e, f) {
this.ctx.transform(a, b, c, d, e, f);
this.cachedGetSinglePixelWidth = null;
},
// Path
@ -3816,9 +3824,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
consumePath = typeof consumePath !== 'undefined' ? consumePath : true;
var ctx = this.ctx;
var strokeColor = this.current.strokeColor;
if (this.current.lineWidth === 0) {
ctx.lineWidth = this.getSinglePixelWidth();
}
// Prevent drawing too thin lines by enforcing a minimum line width.
ctx.lineWidth = Math.max(this.getSinglePixelWidth() * MIN_WIDTH_FACTOR,
this.current.lineWidth);
// For stroke we want to temporarily change the global alpha to the
// stroking alpha.
ctx.globalAlpha = this.current.strokeAlpha;
@ -3847,10 +3855,10 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
consumePath = typeof consumePath !== 'undefined' ? consumePath : true;
var ctx = this.ctx;
var fillColor = this.current.fillColor;
var isPatternFill = this.current.patternFill;
var needRestore = false;
if (fillColor && fillColor.hasOwnProperty('type') &&
fillColor.type === 'Pattern') {
if (isPatternFill) {
ctx.save();
ctx.fillStyle = fillColor.getPattern(ctx, this);
needRestore = true;
@ -4143,7 +4151,13 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var lineWidth = current.lineWidth;
var scale = current.textMatrixScale;
if (scale === 0 || lineWidth === 0) {
lineWidth = this.getSinglePixelWidth();
var fillStrokeMode = current.textRenderingMode &
TextRenderingMode.FILL_STROKE_MASK;
if (fillStrokeMode === TextRenderingMode.STROKE ||
fillStrokeMode === TextRenderingMode.FILL_STROKE) {
this.cachedGetSinglePixelWidth = null;
lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR;
}
} else {
lineWidth /= scale;
}
@ -4324,6 +4338,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
},
setFillColorN: function CanvasGraphics_setFillColorN(/*...*/) {
this.current.fillColor = this.getColorN_Pattern(arguments);
this.current.patternFill = true;
},
setStrokeRGBColor: function CanvasGraphics_setStrokeRGBColor(r, g, b) {
var color = Util.makeCssRgb(r, g, b);
@ -4334,6 +4349,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
var color = Util.makeCssRgb(r, g, b);
this.ctx.fillStyle = color;
this.current.fillColor = color;
this.current.patternFill = false;
},
shadingFill: function CanvasGraphics_shadingFill(patternIR) {
@ -4592,11 +4608,12 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
paintImageMaskXObject: function CanvasGraphics_paintImageMaskXObject(img) {
var ctx = this.ctx;
var width = img.width, height = img.height;
var fillColor = this.current.fillColor;
var isPatternFill = this.current.patternFill;
var glyph = this.processingType3;
if (COMPILE_TYPE3_GLYPHS && glyph && !('compiled' in glyph)) {
var MAX_SIZE_TO_COMPILE = 1000;
if (COMPILE_TYPE3_GLYPHS && glyph && glyph.compiled === undefined) {
if (width <= MAX_SIZE_TO_COMPILE && height <= MAX_SIZE_TO_COMPILE) {
glyph.compiled =
compileType3Glyph({data: img.data, width: width, height: height});
@ -4618,9 +4635,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
maskCtx.globalCompositeOperation = 'source-in';
var fillColor = this.current.fillColor;
maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
fillColor.type === 'Pattern') ?
maskCtx.fillStyle = isPatternFill ?
fillColor.getPattern(maskCtx, this) : fillColor;
maskCtx.fillRect(0, 0, width, height);
@ -4634,7 +4649,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
scaleY, positions) {
var width = imgData.width;
var height = imgData.height;
var ctx = this.ctx;
var fillColor = this.current.fillColor;
var isPatternFill = this.current.patternFill;
var maskCanvas = CachedCanvases.getCanvas('maskCanvas', width, height);
var maskCtx = maskCanvas.context;
@ -4644,14 +4660,13 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
maskCtx.globalCompositeOperation = 'source-in';
var fillColor = this.current.fillColor;
maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
fillColor.type === 'Pattern') ?
fillColor.getPattern(maskCtx, this) : fillColor;
maskCtx.fillStyle = isPatternFill ?
fillColor.getPattern(maskCtx, this) : fillColor;
maskCtx.fillRect(0, 0, width, height);
maskCtx.restore();
var ctx = this.ctx;
for (var i = 0, ii = positions.length; i < ii; i += 2) {
ctx.save();
ctx.transform(scaleX, 0, 0, scaleY, positions[i], positions[i + 1]);
@ -4666,6 +4681,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
function CanvasGraphics_paintImageMaskXObjectGroup(images) {
var ctx = this.ctx;
var fillColor = this.current.fillColor;
var isPatternFill = this.current.patternFill;
for (var i = 0, ii = images.length; i < ii; i++) {
var image = images[i];
var width = image.width, height = image.height;
@ -4678,9 +4695,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
maskCtx.globalCompositeOperation = 'source-in';
var fillColor = this.current.fillColor;
maskCtx.fillStyle = (fillColor && fillColor.hasOwnProperty('type') &&
fillColor.type === 'Pattern') ?
maskCtx.fillStyle = isPatternFill ?
fillColor.getPattern(maskCtx, this) : fillColor;
maskCtx.fillRect(0, 0, width, height);
@ -4883,11 +4898,14 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
ctx.beginPath();
},
getSinglePixelWidth: function CanvasGraphics_getSinglePixelWidth(scale) {
var inverse = this.ctx.mozCurrentTransformInverse;
// max of the current horizontal and vertical scale
return Math.sqrt(Math.max(
(inverse[0] * inverse[0] + inverse[1] * inverse[1]),
(inverse[2] * inverse[2] + inverse[3] * inverse[3])));
if (this.cachedGetSinglePixelWidth === null) {
var inverse = this.ctx.mozCurrentTransformInverse;
// max of the current horizontal and vertical scale
this.cachedGetSinglePixelWidth = Math.sqrt(Math.max(
(inverse[0] * inverse[0] + inverse[1] * inverse[1]),
(inverse[2] * inverse[2] + inverse[3] * inverse[3])));
}
return this.cachedGetSinglePixelWidth;
},
getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) {
var transform = this.ctx.mozCurrentTransform;
@ -5824,7 +5842,6 @@ var FontFaceObject = (function FontFaceObjectClosure() {
})();
var HIGHLIGHT_OFFSET = 4; // px
var ANNOT_MIN_SIZE = 10; // px
var AnnotationUtils = (function AnnotationUtilsClosure() {
@ -5851,41 +5868,36 @@ var AnnotationUtils = (function AnnotationUtilsClosure() {
style.fontFamily = fontFamily + fallbackName;
}
// TODO(mack): Remove this, it's not really that helpful.
function getEmptyContainer(tagName, rect, borderWidth) {
var bWidth = borderWidth || 0;
var element = document.createElement(tagName);
element.style.borderWidth = bWidth + 'px';
var width = rect[2] - rect[0] - 2 * bWidth;
var height = rect[3] - rect[1] - 2 * bWidth;
element.style.width = width + 'px';
element.style.height = height + 'px';
return element;
}
function initContainer(item) {
var container = getEmptyContainer('section', item.rect, item.borderWidth);
container.style.backgroundColor = item.color;
var color = item.color;
item.colorCssRgb = Util.makeCssRgb(Math.round(color[0] * 255),
Math.round(color[1] * 255),
Math.round(color[2] * 255));
var highlight = document.createElement('div');
highlight.className = 'annotationHighlight';
highlight.style.left = highlight.style.top = -HIGHLIGHT_OFFSET + 'px';
highlight.style.right = highlight.style.bottom = -HIGHLIGHT_OFFSET + 'px';
highlight.setAttribute('hidden', true);
item.highlightElement = highlight;
container.appendChild(item.highlightElement);
function initContainer(item, drawBorder) {
var container = document.createElement('section');
var cstyle = container.style;
var width = item.rect[2] - item.rect[0];
var height = item.rect[3] - item.rect[1];
var bWidth = item.borderWidth || 0;
if (bWidth) {
width = width - 2 * bWidth;
height = height - 2 * bWidth;
cstyle.borderWidth = bWidth + 'px';
var color = item.color;
if (drawBorder && color) {
cstyle.borderStyle = 'solid';
cstyle.borderColor = Util.makeCssRgb(Math.round(color[0] * 255),
Math.round(color[1] * 255),
Math.round(color[2] * 255));
}
}
cstyle.width = width + 'px';
cstyle.height = height + 'px';
return container;
}
function getHtmlElementForTextWidgetAnnotation(item, commonObjs) {
var element = getEmptyContainer('div', item.rect, 0);
var element = document.createElement('div');
var width = item.rect[2] - item.rect[0];
var height = item.rect[3] - item.rect[1];
element.style.width = width + 'px';
element.style.height = height + 'px';
element.style.display = 'table';
var content = document.createElement('div');
@ -5915,7 +5927,7 @@ var AnnotationUtils = (function AnnotationUtilsClosure() {
rect[2] = rect[0] + (rect[3] - rect[1]); // make it square
}
var container = initContainer(item);
var container = initContainer(item, false);
container.className = 'annotText';
var image = document.createElement('img');
@ -6024,12 +6036,9 @@ var AnnotationUtils = (function AnnotationUtilsClosure() {
}
function getHtmlElementForLinkAnnotation(item) {
var container = initContainer(item);
var container = initContainer(item, true);
container.className = 'annotLink';
container.style.borderColor = item.colorCssRgb;
container.style.borderStyle = 'solid';
var link = document.createElement('a');
link.href = link.title = item.url || '';

Просмотреть файл

@ -22,8 +22,8 @@ if (typeof PDFJS === 'undefined') {
(typeof window !== 'undefined' ? window : this).PDFJS = {};
}
PDFJS.version = '1.0.978';
PDFJS.build = '20bf84a';
PDFJS.version = '1.0.1040';
PDFJS.build = '997096f';
(function pdfjsWrapper() {
// Use strict in our context only - users might not want it
@ -2298,6 +2298,10 @@ var Page = (function PageClosure() {
* `PDFDocument` objects on the main thread created.
*/
var PDFDocument = (function PDFDocumentClosure() {
var FINGERPRINT_FIRST_BYTES = 1024;
var EMPTY_FINGERPRINT = '\x00\x00\x00\x00\x00\x00\x00' +
'\x00\x00\x00\x00\x00\x00\x00\x00\x00';
function PDFDocument(pdfManager, arg, password) {
if (isStream(arg)) {
init.call(this, pdfManager, arg, password);
@ -2509,16 +2513,25 @@ var PDFDocument = (function PDFDocumentClosure() {
return shadow(this, 'documentInfo', docInfo);
},
get fingerprint() {
var xref = this.xref, hash, fileID = '';
var xref = this.xref, idArray, hash, fileID = '';
if (xref.trailer.has('ID')) {
hash = stringToBytes(xref.trailer.get('ID')[0]);
idArray = xref.trailer.get('ID');
}
if (idArray && isArray(idArray) && idArray[0] !== EMPTY_FINGERPRINT) {
hash = stringToBytes(idArray[0]);
} else {
hash = calculateMD5(this.stream.bytes.subarray(0, 100), 0, 100);
if (this.stream.ensureRange) {
this.stream.ensureRange(0,
Math.min(FINGERPRINT_FIRST_BYTES, this.stream.end));
}
hash = calculateMD5(this.stream.bytes.subarray(0,
FINGERPRINT_FIRST_BYTES), 0, FINGERPRINT_FIRST_BYTES);
}
for (var i = 0, n = hash.length; i < n; i++) {
fileID += hash[i].toString(16);
var hex = hash[i].toString(16);
fileID += hex.length === 1 ? '0' + hex : hex;
}
return shadow(this, 'fingerprint', fileID);
@ -4374,12 +4387,26 @@ var Annotation = (function AnnotationClosure() {
data.annotationFlags = dict.get('F');
var color = dict.get('C');
if (isArray(color) && color.length === 3) {
// TODO(mack): currently only supporting rgb; need support different
// colorspaces
data.color = color;
} else {
if (!color) {
// The PDF spec does not mention how a missing color array is interpreted.
// Adobe Reader seems to default to black in this case.
data.color = [0, 0, 0];
} else if (isArray(color)) {
switch (color.length) {
case 0:
// Empty array denotes transparent border.
data.color = null;
break;
case 1:
// TODO: implement DeviceGray
break;
case 3:
data.color = color;
break;
case 4:
// TODO: implement DeviceCMYK
break;
}
}
// Some types of annotations have border style dict which has more
@ -4396,7 +4423,7 @@ var Annotation = (function AnnotationClosure() {
if (data.borderWidth > 0 && dashArray) {
if (!isArray(dashArray)) {
// Ignore the border if dashArray is not actually an array,
// this is consistent with the behaviour in Adobe Reader.
// this is consistent with the behaviour in Adobe Reader.
data.borderWidth = 0;
} else {
var dashArrayLength = dashArray.length;
@ -4819,11 +4846,7 @@ var LinkAnnotation = (function LinkAnnotationClosure() {
return url;
}
Util.inherit(LinkAnnotation, InteractiveAnnotation, {
hasOperatorList: function LinkAnnotation_hasOperatorList() {
return false;
}
});
Util.inherit(LinkAnnotation, InteractiveAnnotation, { });
return LinkAnnotation;
})();
@ -10120,7 +10143,7 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
buildPaintImageXObject:
function PartialEvaluator_buildPaintImageXObject(resources, image,
inline, operatorList,
cacheKey, cache) {
cacheKey, imageCache) {
var self = this;
var dict = image.dict;
var w = dict.get('Width', 'W');
@ -10158,9 +10181,10 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
args = [imgData];
operatorList.addOp(OPS.paintImageMaskXObject, args);
if (cacheKey) {
cache.key = cacheKey;
cache.fn = OPS.paintImageMaskXObject;
cache.args = args;
imageCache[cacheKey] = {
fn: OPS.paintImageMaskXObject,
args: args
};
}
return;
}
@ -10209,9 +10233,10 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
operatorList.addOp(OPS.paintImageXObject, args);
if (cacheKey) {
cache.key = cacheKey;
cache.fn = OPS.paintImageXObject;
cache.args = args;
imageCache[cacheKey] = {
fn: OPS.paintImageXObject,
args: args
};
}
},
@ -10605,8 +10630,8 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
}
// eagerly compile XForm objects
var name = args[0].name;
if (imageCache.key === name) {
operatorList.addOp(imageCache.fn, imageCache.args);
if (imageCache[name] !== undefined) {
operatorList.addOp(imageCache[name].fn, imageCache[name].args);
args = null;
continue;
}
@ -10655,10 +10680,13 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
}, reject);
case OPS.endInlineImage:
var cacheKey = args[0].cacheKey;
if (cacheKey && imageCache.key === cacheKey) {
operatorList.addOp(imageCache.fn, imageCache.args);
args = null;
continue;
if (cacheKey) {
var cacheEntry = imageCache[cacheKey];
if (cacheEntry !== undefined) {
operatorList.addOp(cacheEntry.fn, cacheEntry.args);
args = null;
continue;
}
}
self.buildPaintImageXObject(resources, args[0], true,
operatorList, cacheKey, imageCache);
@ -13825,9 +13853,12 @@ var stdFontMap = {
'CourierNewPS-BoldMT': 'Courier-Bold',
'CourierNewPS-ItalicMT': 'Courier-Oblique',
'CourierNewPSMT': 'Courier',
'Helvetica': 'Helvetica',
'Helvetica-Bold': 'Helvetica-Bold',
'Helvetica-BoldItalic': 'Helvetica-BoldOblique',
'Helvetica-BoldOblique': 'Helvetica-BoldOblique',
'Helvetica-Italic': 'Helvetica-Oblique',
'Helvetica-Oblique':'Helvetica-Oblique',
'Symbol-Bold': 'Symbol',
'Symbol-BoldItalic': 'Symbol',
'Symbol-Italic': 'Symbol',
@ -13853,6 +13884,10 @@ var stdFontMap = {
* fonts without glyph data.
*/
var nonStdFontMap = {
'CenturyGothic': 'Helvetica',
'CenturyGothic-Bold': 'Helvetica-Bold',
'CenturyGothic-BoldItalic': 'Helvetica-BoldOblique',
'CenturyGothic-Italic': 'Helvetica-Oblique',
'ComicSansMS': 'Comic Sans MS',
'ComicSansMS-Bold': 'Comic Sans MS-Bold',
'ComicSansMS-BoldItalic': 'Comic Sans MS-BoldItalic',
@ -13876,7 +13911,8 @@ var nonStdFontMap = {
'MS-PMincho': 'MS PMincho',
'MS-PMincho-Bold': 'MS PMincho-Bold',
'MS-PMincho-BoldItalic': 'MS PMincho-BoldItalic',
'MS-PMincho-Italic': 'MS PMincho-Italic'
'MS-PMincho-Italic': 'MS PMincho-Italic',
'Wingdings': 'ZapfDingbats'
};
var serifFonts = {
@ -15959,7 +15995,8 @@ var Font = (function FontClosure() {
// The file data is not specified. Trying to fix the font name
// to be used with the canvas.font.
var fontName = name.replace(/[,_]/g, '-');
var isStandardFont = fontName in stdFontMap;
var isStandardFont = !!stdFontMap[fontName] ||
(nonStdFontMap[fontName] && !!stdFontMap[nonStdFontMap[fontName]]);
fontName = stdFontMap[fontName] || nonStdFontMap[fontName] || fontName;
this.bold = (fontName.search(/bold/gi) !== -1);
@ -16005,6 +16042,10 @@ var Font = (function FontClosure() {
this.toFontChar[charCode] = fontChar;
}
} else if (/Dingbats/i.test(fontName)) {
if (/Wingdings/i.test(name)) {
warn('Wingdings font without embedded font file, ' +
'falling back to the ZapfDingbats encoding.');
}
var dingbats = Encodings.ZapfDingbatsEncoding;
for (charCode in dingbats) {
fontChar = DingbatsGlyphsUnicode[dingbats[charCode]];
@ -16190,6 +16231,7 @@ var Font = (function FontClosure() {
fontCharCode <= 0x1f || // Control chars
fontCharCode === 0x7F || // Control char
fontCharCode === 0xAD || // Soft hyphen
fontCharCode === 0xA0 || // Non breaking space
(fontCharCode >= 0x80 && fontCharCode <= 0x9F) || // Control chars
// Prevent drawing characters in the specials unicode block.
(fontCharCode >= 0xFFF0 && fontCharCode <= 0xFFFF) ||
@ -18208,6 +18250,8 @@ function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
glyphId = glyphNames.indexOf(baseEncoding[charCode]);
if (glyphId >= 0) {
charCodeToGlyphId[charCode] = glyphId;
} else {
charCodeToGlyphId[charCode] = 0; // notdef
}
}
} else if (!!(properties.flags & FontFlags.Symbolic)) {
@ -18224,6 +18268,8 @@ function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
glyphId = glyphNames.indexOf(baseEncoding[charCode]);
if (glyphId >= 0) {
charCodeToGlyphId[charCode] = glyphId;
} else {
charCodeToGlyphId[charCode] = 0; // notdef
}
}
}
@ -18236,6 +18282,8 @@ function type1FontGlyphMapping(properties, builtInEncoding, glyphNames) {
glyphId = glyphNames.indexOf(glyphName);
if (glyphId >= 0) {
charCodeToGlyphId[charCode] = glyphId;
} else {
charCodeToGlyphId[charCode] = 0; // notdef
}
}
}
@ -29411,16 +29459,14 @@ function isEOF(v) {
return (v === EOF);
}
var MAX_LENGTH_TO_CACHE = 1000;
var Parser = (function ParserClosure() {
function Parser(lexer, allowStreams, xref) {
this.lexer = lexer;
this.allowStreams = allowStreams;
this.xref = xref;
this.imageCache = {
length: 0,
adler32: 0,
stream: null
};
this.imageCache = {};
this.refill();
}
@ -29511,17 +29557,117 @@ var Parser = (function ParserClosure() {
// simple object
return buf1;
},
/**
* Find the end of the stream by searching for the /EI\s/.
* @returns {number} The inline stream length.
*/
findDefaultInlineStreamEnd:
function Parser_findDefaultInlineStreamEnd(stream) {
var E = 0x45, I = 0x49, SPACE = 0x20, LF = 0xA, CR = 0xD;
var startPos = stream.pos, state = 0, ch, i, n, followingBytes;
while ((ch = stream.getByte()) !== -1) {
if (state === 0) {
state = (ch === E) ? 1 : 0;
} else if (state === 1) {
state = (ch === I) ? 2 : 0;
} else {
assert(state === 2);
if (ch === SPACE || ch === LF || ch === CR) {
// Let's check the next five bytes are ASCII... just be sure.
n = 5;
followingBytes = stream.peekBytes(n);
for (i = 0; i < n; i++) {
ch = followingBytes[i];
if (ch !== LF && ch !== CR && (ch < SPACE || ch > 0x7F)) {
// Not a LF, CR, SPACE or any visible ASCII character, i.e.
// it's binary stuff. Resetting the state.
state = 0;
break;
}
}
if (state === 2) {
break; // Finished!
}
} else {
state = 0;
}
}
}
return ((stream.pos - 4) - startPos);
},
/**
* Find the EOD (end-of-data) marker '~>' (i.e. TILDE + GT) of the stream.
* @returns {number} The inline stream length.
*/
findASCII85DecodeInlineStreamEnd:
function Parser_findASCII85DecodeInlineStreamEnd(stream) {
var TILDE = 0x7E, GT = 0x3E;
var startPos = stream.pos, ch, length;
while ((ch = stream.getByte()) !== -1) {
if (ch === TILDE && stream.peekByte() === GT) {
stream.skip();
break;
}
}
length = stream.pos - startPos;
if (ch === -1) {
warn('Inline ASCII85Decode image stream: ' +
'EOD marker not found, searching for /EI/ instead.');
stream.skip(-length); // Reset the stream position.
return this.findDefaultInlineStreamEnd(stream);
}
this.inlineStreamSkipEI(stream);
return length;
},
/**
* Find the EOD (end-of-data) marker '>' (i.e. GT) of the stream.
* @returns {number} The inline stream length.
*/
findASCIIHexDecodeInlineStreamEnd:
function Parser_findASCIIHexDecodeInlineStreamEnd(stream) {
var GT = 0x3E;
var startPos = stream.pos, ch, length;
while ((ch = stream.getByte()) !== -1) {
if (ch === GT) {
break;
}
}
length = stream.pos - startPos;
if (ch === -1) {
warn('Inline ASCIIHexDecode image stream: ' +
'EOD marker not found, searching for /EI/ instead.');
stream.skip(-length); // Reset the stream position.
return this.findDefaultInlineStreamEnd(stream);
}
this.inlineStreamSkipEI(stream);
return length;
},
/**
* Skip over the /EI/ for streams where we search for an EOD marker.
*/
inlineStreamSkipEI: function Parser_inlineStreamSkipEI(stream) {
var E = 0x45, I = 0x49;
var state = 0, ch;
while ((ch = stream.getByte()) !== -1) {
if (state === 0) {
state = (ch === E) ? 1 : 0;
} else if (state === 1) {
state = (ch === I) ? 2 : 0;
} else if (state === 2) {
break;
}
}
},
makeInlineImage: function Parser_makeInlineImage(cipherTransform) {
var lexer = this.lexer;
var stream = lexer.stream;
// parse dictionary
// Parse dictionary.
var dict = new Dict(null);
while (!isCmd(this.buf1, 'ID') && !isEOF(this.buf1)) {
if (!isName(this.buf1)) {
error('Dictionary key must be a name object');
}
var key = this.buf1.name;
this.shift();
if (isEOF(this.buf1)) {
@ -29530,72 +29676,48 @@ var Parser = (function ParserClosure() {
dict.set(key, this.getObj(cipherTransform));
}
// parse image stream
var startPos = stream.pos;
// searching for the /EI\s/
var state = 0, ch, i, ii;
var E = 0x45, I = 0x49, SPACE = 0x20, NL = 0xA, CR = 0xD;
while ((ch = stream.getByte()) !== -1) {
if (state === 0) {
state = (ch === E) ? 1 : 0;
} else if (state === 1) {
state = (ch === I) ? 2 : 0;
} else {
assert(state === 2);
if (ch === SPACE || ch === NL || ch === CR) {
// Let's check the next five bytes are ASCII... just be sure.
var n = 5;
var followingBytes = stream.peekBytes(n);
for (i = 0; i < n; i++) {
ch = followingBytes[i];
if (ch !== NL && ch !== CR && (ch < SPACE || ch > 0x7F)) {
// Not a LF, CR, SPACE or any visible ASCII character, i.e.
// it's binary stuff. Resetting the state.
state = 0;
break;
}
}
if (state === 2) {
break; // finished!
}
} else {
state = 0;
}
}
// Extract the name of the first (i.e. the current) image filter.
var filter = this.fetchIfRef(dict.get('Filter', 'F')), filterName;
if (isName(filter)) {
filterName = filter.name;
} else if (isArray(filter) && isName(filter[0])) {
filterName = filter[0].name;
}
var length = (stream.pos - 4) - startPos;
// Parse image stream.
var startPos = stream.pos, length, i, ii;
if (filterName === 'ASCII85Decide' || filterName === 'A85') {
length = this.findASCII85DecodeInlineStreamEnd(stream);
} else if (filterName === 'ASCIIHexDecode' || filterName === 'AHx') {
length = this.findASCIIHexDecodeInlineStreamEnd(stream);
} else {
length = this.findDefaultInlineStreamEnd(stream);
}
var imageStream = stream.makeSubStream(startPos, length, dict);
// trying to cache repeat images, first we are trying to "warm up" caching
// using length, then comparing adler32
var MAX_LENGTH_TO_CACHE = 1000;
var cacheImage = false, adler32;
if (length < MAX_LENGTH_TO_CACHE && this.imageCache.length === length) {
// Cache all images below the MAX_LENGTH_TO_CACHE threshold by their
// adler32 checksum.
var adler32;
if (length < MAX_LENGTH_TO_CACHE) {
var imageBytes = imageStream.getBytes();
imageStream.reset();
var a = 1;
var b = 0;
for (i = 0, ii = imageBytes.length; i < ii; ++i) {
a = (a + (imageBytes[i] & 0xff)) % 65521;
b = (b + a) % 65521;
// No modulo required in the loop if imageBytes.length < 5552.
a += imageBytes[i] & 0xff;
b += a;
}
adler32 = (b << 16) | a;
adler32 = ((b % 65521) << 16) | (a % 65521);
if (this.imageCache.stream && this.imageCache.adler32 === adler32) {
if (this.imageCache.adler32 === adler32) {
this.buf2 = Cmd.get('EI');
this.shift();
this.imageCache.stream.reset();
return this.imageCache.stream;
this.imageCache[adler32].reset();
return this.imageCache[adler32];
}
cacheImage = true;
}
if (!cacheImage && !this.imageCache.stream) {
this.imageCache.length = length;
this.imageCache.stream = null;
}
if (cipherTransform) {
@ -29604,10 +29726,9 @@ var Parser = (function ParserClosure() {
imageStream = this.filter(imageStream, dict, length);
imageStream.dict = dict;
if (cacheImage) {
if (adler32 !== undefined) {
imageStream.cacheKey = 'inline_' + length + '_' + adler32;
this.imageCache.adler32 = adler32;
this.imageCache.stream = imageStream;
this.imageCache[adler32] = imageStream;
}
this.buf2 = Cmd.get('EI');
@ -29755,22 +29876,6 @@ var Parser = (function ParserClosure() {
return new LZWStream(stream, maybeLength, earlyChange);
}
if (name === 'DCTDecode' || name === 'DCT') {
// According to the specification: for inline images, the ID operator
// shall be followed by a single whitespace character (unless it uses
// ASCII85Decode or ASCIIHexDecode filters).
// In practice this only seems to be followed for inline JPEG images,
// and generally ignoring the first byte of the stream if it is a
// whitespace char can even *cause* issues (e.g. in the CCITTFaxDecode
// filters used in issue2984.pdf).
// Hence when the first byte of the stream of an inline JPEG image is
// a whitespace character, we thus simply skip over it.
if (isCmd(this.buf1, 'ID')) {
var firstByte = stream.peekByte();
if (firstByte === 0x0A /* LF */ || firstByte === 0x0D /* CR */ ||
firstByte === 0x20 /* SPACE */) {
stream.skip();
}
}
xrefStreamStats[StreamType.DCT] = true;
return new JpegStream(stream, maybeLength, stream.dict, this.xref);
}
@ -31318,8 +31423,15 @@ var PredictorStream = (function PredictorStreamClosure() {
*/
var JpegStream = (function JpegStreamClosure() {
function JpegStream(stream, maybeLength, dict, xref) {
// TODO: per poppler, some images may have 'junk' before that
// need to be removed
// Some images may contain 'junk' before the SOI (start-of-image) marker.
// Note: this seems to mainly affect inline images.
var ch;
while ((ch = stream.getByte()) !== -1) {
if (ch === 0xFF) { // Find the first byte of the SOI marker (0xFFD8).
stream.skip(-1); // Reset the stream position to the SOI.
break;
}
}
this.stream = stream;
this.maybeLength = maybeLength;
this.dict = dict;
@ -32494,7 +32606,7 @@ var CCITTFaxStream = (function CCITTFaxStreamClosure() {
if (!this.eoblock && this.row === this.rows - 1) {
this.eof = true;
} else if (this.eoline || !this.byteAlign) {
} else {
code1 = this.lookBits(12);
if (this.eoline) {
while (code1 !== EOF && code1 !== 1) {
@ -34864,12 +34976,6 @@ var JpxImage = (function JpxImageClosure() {
cod.precinctsSizes = precinctsSizes;
}
var unsupported = [];
if (cod.sopMarkerUsed) {
unsupported.push('sopMarkerUsed');
}
if (cod.ephMarkerUsed) {
unsupported.push('ephMarkerUsed');
}
if (cod.selectiveArithmeticCodingBypass) {
unsupported.push('selectiveArithmeticCodingBypass');
}
@ -35237,6 +35343,230 @@ var JpxImage = (function JpxImageClosure() {
throw new Error('JPX Error: Out of packets');
};
}
function ResolutionPositionComponentLayerIterator(context) {
var siz = context.SIZ;
var tileIndex = context.currentTile.index;
var tile = context.tiles[tileIndex];
var layersCount = tile.codingStyleDefaultParameters.layersCount;
var componentsCount = siz.Csiz;
var l, r, c, p;
var maxDecompositionLevelsCount = 0;
for (c = 0; c < componentsCount; c++) {
var component = tile.components[c];
maxDecompositionLevelsCount = Math.max(maxDecompositionLevelsCount,
component.codingStyleParameters.decompositionLevelsCount);
}
var maxNumPrecinctsInLevel = new Int32Array(
maxDecompositionLevelsCount + 1);
for (r = 0; r <= maxDecompositionLevelsCount; ++r) {
var maxNumPrecincts = 0;
for (c = 0; c < componentsCount; ++c) {
var resolutions = tile.components[c].resolutions;
if (r < resolutions.length) {
maxNumPrecincts = Math.max(maxNumPrecincts,
resolutions[r].precinctParameters.numprecincts);
}
}
maxNumPrecinctsInLevel[r] = maxNumPrecincts;
}
l = 0;
r = 0;
c = 0;
p = 0;
this.nextPacket = function JpxImage_nextPacket() {
// Section B.12.1.3 Resolution-position-component-layer
for (; r <= maxDecompositionLevelsCount; r++) {
for (; p < maxNumPrecinctsInLevel[r]; p++) {
for (; c < componentsCount; c++) {
var component = tile.components[c];
if (r > component.codingStyleParameters.decompositionLevelsCount) {
continue;
}
var resolution = component.resolutions[r];
var numprecincts = resolution.precinctParameters.numprecincts;
if (p >= numprecincts) {
continue;
}
for (; l < layersCount;) {
var packet = createPacket(resolution, p, l);
l++;
return packet;
}
l = 0;
}
c = 0;
}
p = 0;
}
throw new Error('JPX Error: Out of packets');
};
}
function PositionComponentResolutionLayerIterator(context) {
var siz = context.SIZ;
var tileIndex = context.currentTile.index;
var tile = context.tiles[tileIndex];
var layersCount = tile.codingStyleDefaultParameters.layersCount;
var componentsCount = siz.Csiz;
var precinctsSizes = getPrecinctSizesInImageScale(tile);
var precinctsIterationSizes = precinctsSizes;
var l = 0, r = 0, c = 0, px = 0, py = 0;
this.nextPacket = function JpxImage_nextPacket() {
// Section B.12.1.4 Position-component-resolution-layer
for (; py < precinctsIterationSizes.maxNumHigh; py++) {
for (; px < precinctsIterationSizes.maxNumWide; px++) {
for (; c < componentsCount; c++) {
var component = tile.components[c];
var decompositionLevelsCount =
component.codingStyleParameters.decompositionLevelsCount;
for (; r <= decompositionLevelsCount; r++) {
var resolution = component.resolutions[r];
var sizeInImageScale =
precinctsSizes.components[c].resolutions[r];
var k = getPrecinctIndexIfExist(
px,
py,
sizeInImageScale,
precinctsIterationSizes,
resolution);
if (k === null) {
continue;
}
for (; l < layersCount;) {
var packet = createPacket(resolution, k, l);
l++;
return packet;
}
l = 0;
}
r = 0;
}
c = 0;
}
px = 0;
}
throw new Error('JPX Error: Out of packets');
};
}
function ComponentPositionResolutionLayerIterator(context) {
var siz = context.SIZ;
var tileIndex = context.currentTile.index;
var tile = context.tiles[tileIndex];
var layersCount = tile.codingStyleDefaultParameters.layersCount;
var componentsCount = siz.Csiz;
var precinctsSizes = getPrecinctSizesInImageScale(tile);
var l = 0, r = 0, c = 0, px = 0, py = 0;
this.nextPacket = function JpxImage_nextPacket() {
// Section B.12.1.5 Component-position-resolution-layer
for (; c < componentsCount; ++c) {
var component = tile.components[c];
var precinctsIterationSizes = precinctsSizes.components[c];
var decompositionLevelsCount =
component.codingStyleParameters.decompositionLevelsCount;
for (; py < precinctsIterationSizes.maxNumHigh; py++) {
for (; px < precinctsIterationSizes.maxNumWide; px++) {
for (; r <= decompositionLevelsCount; r++) {
var resolution = component.resolutions[r];
var sizeInImageScale = precinctsIterationSizes.resolutions[r];
var k = getPrecinctIndexIfExist(
px,
py,
sizeInImageScale,
precinctsIterationSizes,
resolution);
if (k === null) {
continue;
}
for (; l < layersCount;) {
var packet = createPacket(resolution, k, l);
l++;
return packet;
}
l = 0;
}
r = 0;
}
px = 0;
}
py = 0;
}
throw new Error('JPX Error: Out of packets');
};
}
function getPrecinctIndexIfExist(
pxIndex, pyIndex, sizeInImageScale, precinctIterationSizes, resolution) {
var posX = pxIndex * precinctIterationSizes.minWidth;
var posY = pyIndex * precinctIterationSizes.minHeight;
if (posX % sizeInImageScale.width !== 0 ||
posY % sizeInImageScale.height !== 0) {
return null;
}
var startPrecinctRowIndex =
(posY / sizeInImageScale.width) *
resolution.precinctParameters.numprecinctswide;
return (posX / sizeInImageScale.height) + startPrecinctRowIndex;
}
function getPrecinctSizesInImageScale(tile) {
var componentsCount = tile.components.length;
var minWidth = Number.MAX_VALUE;
var minHeight = Number.MAX_VALUE;
var maxNumWide = 0;
var maxNumHigh = 0;
var sizePerComponent = new Array(componentsCount);
for (var c = 0; c < componentsCount; c++) {
var component = tile.components[c];
var decompositionLevelsCount =
component.codingStyleParameters.decompositionLevelsCount;
var sizePerResolution = new Array(decompositionLevelsCount + 1);
var minWidthCurrentComponent = Number.MAX_VALUE;
var minHeightCurrentComponent = Number.MAX_VALUE;
var maxNumWideCurrentComponent = 0;
var maxNumHighCurrentComponent = 0;
var scale = 1;
for (var r = decompositionLevelsCount; r >= 0; --r) {
var resolution = component.resolutions[r];
var widthCurrentResolution =
scale * resolution.precinctParameters.precinctWidth;
var heightCurrentResolution =
scale * resolution.precinctParameters.precinctHeight;
minWidthCurrentComponent = Math.min(
minWidthCurrentComponent,
widthCurrentResolution);
minHeightCurrentComponent = Math.min(
minHeightCurrentComponent,
heightCurrentResolution);
maxNumWideCurrentComponent = Math.max(maxNumWideCurrentComponent,
resolution.precinctParameters.numprecinctswide);
maxNumHighCurrentComponent = Math.max(maxNumHighCurrentComponent,
resolution.precinctParameters.numprecinctshigh);
sizePerResolution[r] = {
width: widthCurrentResolution,
height: heightCurrentResolution
};
scale <<= 1;
}
minWidth = Math.min(minWidth, minWidthCurrentComponent);
minHeight = Math.min(minHeight, minHeightCurrentComponent);
maxNumWide = Math.max(maxNumWide, maxNumWideCurrentComponent);
maxNumHigh = Math.max(maxNumHigh, maxNumHighCurrentComponent);
sizePerComponent[c] = {
resolutions: sizePerResolution,
minWidth: minWidthCurrentComponent,
minHeight: minHeightCurrentComponent,
maxNumWide: maxNumWideCurrentComponent,
maxNumHigh: maxNumHighCurrentComponent
};
}
return {
components: sizePerComponent,
minWidth: minWidth,
minHeight: minHeight,
maxNumWide: maxNumWide,
maxNumHigh: maxNumHigh
};
}
function buildPackets(context) {
var siz = context.SIZ;
var tileIndex = context.currentTile.index;
@ -35329,6 +35659,18 @@ var JpxImage = (function JpxImageClosure() {
tile.packetsIterator =
new ResolutionLayerComponentPositionIterator(context);
break;
case 2:
tile.packetsIterator =
new ResolutionPositionComponentLayerIterator(context);
break;
case 3:
tile.packetsIterator =
new PositionComponentResolutionLayerIterator(context);
break;
case 4:
tile.packetsIterator =
new ComponentPositionResolutionLayerIterator(context);
break;
default:
throw new Error('JPX Error: Unsupported progression order ' +
progressionOrder);
@ -35356,6 +35698,21 @@ var JpxImage = (function JpxImageClosure() {
bufferSize -= count;
return (buffer >>> bufferSize) & ((1 << count) - 1);
}
function skipMarkerIfEqual(value) {
if (data[offset + position - 1] === 0xFF &&
data[offset + position] === value) {
skipBytes(1);
return true;
} else if (data[offset + position] === 0xFF &&
data[offset + position + 1] === value) {
skipBytes(2);
return true;
}
return false;
}
function skipBytes(count) {
position += count;
}
function alignToByte() {
bufferSize = 0;
if (skipNextBit) {
@ -35383,13 +35740,19 @@ var JpxImage = (function JpxImageClosure() {
}
var tileIndex = context.currentTile.index;
var tile = context.tiles[tileIndex];
var sopMarkerUsed = context.COD.sopMarkerUsed;
var ephMarkerUsed = context.COD.ephMarkerUsed;
var packetsIterator = tile.packetsIterator;
while (position < dataLength) {
alignToByte();
if (sopMarkerUsed && skipMarkerIfEqual(0x91)) {
// Skip also marker segment length and packet sequence ID
skipBytes(4);
}
var packet = packetsIterator.nextPacket();
if (!readBits(1)) {
continue;
}
var packet = packetsIterator.nextPacket();
var layerNumber = packet.layerNumber;
var queue = [], codeblock;
for (var i = 0, ii = packet.codeblocks.length; i < ii; i++) {
@ -35468,6 +35831,9 @@ var JpxImage = (function JpxImageClosure() {
});
}
alignToByte();
if (ephMarkerUsed) {
skipMarkerIfEqual(0x92);
}
while (queue.length > 0) {
var packetItem = queue.shift();
codeblock = packetItem.codeblock;

Просмотреть файл

@ -103,11 +103,6 @@
border: 0;
}
.pdfViewer .page .annotationHighlight {
position: absolute;
border: 2px #FFFF99 solid;
}
.pdfViewer .page .annotText > img {
position: absolute;
cursor: pointer;
@ -811,7 +806,6 @@ html[dir='rtl'] .dropdownToolbarButton {
}
.dropdownToolbarButton > select {
-moz-appearance: none; /* in the future this might matter, see bugzilla bug #649849 */
min-width: 140px;
font-size: 12px;
color: hsl(0,0%,95%);
@ -1754,6 +1748,10 @@ html[dir='rtl'] #documentPropertiesOverlay .row > * {
left: 0;
display: block;
}
#printContainer div {
page-break-after: always;
page-break-inside: avoid;
}
}
.visibleLargeView,

Просмотреть файл

@ -0,0 +1,42 @@
/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=2 sw=2 tw=80 et:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "nsDocShell.h"
#include "TimelineMarker.h"
TimelineMarker::TimelineMarker(nsDocShell* aDocShell, const char* aName,
TracingMetadata aMetaData)
: mName(aName)
, mMetaData(aMetaData)
{
MOZ_COUNT_CTOR(TimelineMarker);
MOZ_ASSERT(aName);
aDocShell->Now(&mTime);
if (aMetaData == TRACING_INTERVAL_START) {
CaptureStack();
}
}
TimelineMarker::TimelineMarker(nsDocShell* aDocShell, const char* aName,
TracingMetadata aMetaData,
const nsAString& aCause)
: mName(aName)
, mMetaData(aMetaData)
, mCause(aCause)
{
MOZ_COUNT_CTOR(TimelineMarker);
MOZ_ASSERT(aName);
aDocShell->Now(&mTime);
if (aMetaData == TRACING_INTERVAL_START) {
CaptureStack();
}
}
TimelineMarker::~TimelineMarker()
{
MOZ_COUNT_DTOR(TimelineMarker);
}

Просмотреть файл

@ -0,0 +1,114 @@
/* -*- Mode: C++; indent-tabs-mode: nil; c-basic-offset: 2 -*-
* vim: set ts=2 sw=2 tw=80 et:
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef TimelineMarker_h__
#define TimelineMarker_h__
#include "nsString.h"
#include "GeckoProfiler.h"
#include "mozilla/dom/ProfileTimelineMarkerBinding.h"
#include "nsContentUtils.h"
#include "jsapi.h"
class nsDocShell;
// Objects of this type can be added to the timeline. The class can
// also be subclassed to let a given marker creator provide custom
// details.
class TimelineMarker
{
public:
TimelineMarker(nsDocShell* aDocShell, const char* aName,
TracingMetadata aMetaData);
TimelineMarker(nsDocShell* aDocShell, const char* aName,
TracingMetadata aMetaData,
const nsAString& aCause);
virtual ~TimelineMarker();
// Check whether two markers should be considered the same,
// for the purpose of pairing start and end markers. Normally
// this definition suffices.
virtual bool Equals(const TimelineMarker* other)
{
return strcmp(mName, other->mName) == 0;
}
// Add details specific to this marker type to aMarker. The
// standard elements have already been set. This method is
// called on both the starting and ending markers of a pair.
// Ordinarily the ending marker doesn't need to do anything
// here.
virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
{
}
virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&)
{
MOZ_ASSERT_UNREACHABLE("can only be called on layer markers");
}
const char* GetName() const
{
return mName;
}
TracingMetadata GetMetaData() const
{
return mMetaData;
}
DOMHighResTimeStamp GetTime() const
{
return mTime;
}
const nsString& GetCause() const
{
return mCause;
}
JSObject* GetStack()
{
if (mStackTrace) {
return mStackTrace->get();
}
return nullptr;
}
protected:
void CaptureStack()
{
JSContext* ctx = nsContentUtils::GetCurrentJSContext();
if (ctx) {
JS::RootedObject stack(ctx);
if (JS::CaptureCurrentStack(ctx, &stack)) {
mStackTrace.emplace(ctx, stack.get());
} else {
JS_ClearPendingException(ctx);
}
}
}
private:
const char* mName;
TracingMetadata mMetaData;
DOMHighResTimeStamp mTime;
nsString mCause;
// While normally it is not a good idea to make a persistent
// root, in this case changing nsDocShell to participate in
// cycle collection was deemed too invasive, the stack trace
// can't actually cause a cycle, and the markers are only held
// here temporarily to boot.
mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace;
};
#endif /* TimelineMarker_h__ */

Просмотреть файл

@ -62,6 +62,7 @@ UNIFIED_SOURCES += [
'nsDSURIContentListener.cpp',
'nsWebNavigationInfo.cpp',
'SerializedLoadContext.cpp',
'TimelineMarker.cpp',
]
FAIL_ON_WARNINGS = True

Просмотреть файл

@ -32,6 +32,7 @@
#include "nsAutoPtr.h"
#include "nsThreadUtils.h"
#include "nsContentUtils.h"
#include "TimelineMarker.h"
// Threshold value in ms for META refresh based redirects
#define REFRESH_REDIRECT_TIMER 15000
@ -260,125 +261,6 @@ public:
// is no longer applied
void NotifyAsyncPanZoomStopped(const mozilla::CSSIntPoint aScrollPos);
// Objects of this type can be added to the timeline. The class
// can also be subclassed to let a given marker creator provide
// custom details.
class TimelineMarker
{
public:
TimelineMarker(nsDocShell* aDocShell, const char* aName,
TracingMetadata aMetaData)
: mName(aName)
, mMetaData(aMetaData)
{
MOZ_COUNT_CTOR(TimelineMarker);
MOZ_ASSERT(aName);
aDocShell->Now(&mTime);
if (aMetaData == TRACING_INTERVAL_START) {
CaptureStack();
}
}
TimelineMarker(nsDocShell* aDocShell, const char* aName,
TracingMetadata aMetaData,
const nsAString& aCause)
: mName(aName)
, mMetaData(aMetaData)
, mCause(aCause)
{
MOZ_COUNT_CTOR(TimelineMarker);
MOZ_ASSERT(aName);
aDocShell->Now(&mTime);
if (aMetaData == TRACING_INTERVAL_START) {
CaptureStack();
}
}
virtual ~TimelineMarker()
{
MOZ_COUNT_DTOR(TimelineMarker);
}
// Check whether two markers should be considered the same,
// for the purpose of pairing start and end markers. Normally
// this definition suffices.
virtual bool Equals(const TimelineMarker* other)
{
return strcmp(mName, other->mName) == 0;
}
// Add details specific to this marker type to aMarker. The
// standard elements have already been set. This method is
// called on both the starting and ending markers of a pair.
// Ordinarily the ending marker doesn't need to do anything
// here.
virtual void AddDetails(mozilla::dom::ProfileTimelineMarker& aMarker)
{
}
virtual void AddLayerRectangles(mozilla::dom::Sequence<mozilla::dom::ProfileTimelineLayerRect>&)
{
MOZ_ASSERT_UNREACHABLE("can only be called on layer markers");
}
const char* GetName() const
{
return mName;
}
TracingMetadata GetMetaData() const
{
return mMetaData;
}
DOMHighResTimeStamp GetTime() const
{
return mTime;
}
const nsString& GetCause() const
{
return mCause;
}
JSObject* GetStack()
{
if (mStackTrace) {
return mStackTrace->get();
}
return nullptr;
}
protected:
void CaptureStack()
{
JSContext* ctx = nsContentUtils::GetCurrentJSContext();
if (ctx) {
JS::RootedObject stack(ctx);
if (JS::CaptureCurrentStack(ctx, &stack)) {
mStackTrace.emplace(ctx, stack.get());
} else {
JS_ClearPendingException(ctx);
}
}
}
private:
const char* mName;
TracingMetadata mMetaData;
DOMHighResTimeStamp mTime;
nsString mCause;
// While normally it is not a good idea to make a persistent
// root, in this case changing nsDocShell to participate in
// cycle collection was deemed too invasive, the stack trace
// can't actually cause a cycle, and the markers are only held
// here temporarily to boot.
mozilla::Maybe<JS::PersistentRooted<JSObject*>> mStackTrace;
};
// Add new profile timeline markers to this docShell. This will only add
// markers if the docShell is currently recording profile timeline markers.
// See nsIDocShell::recordProfileTimelineMarkers

Просмотреть файл

@ -805,22 +805,22 @@ ReifyStack(nsIStackFrame* aStack, nsTArray<ConsoleStackEntry>& aRefiedStack)
return NS_OK;
}
class ConsoleTimelineMarker : public nsDocShell::TimelineMarker
class ConsoleTimelineMarker : public TimelineMarker
{
public:
ConsoleTimelineMarker(nsDocShell* aDocShell,
TracingMetadata aMetaData,
const nsAString& aCause)
: nsDocShell::TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause)
: TimelineMarker(aDocShell, "ConsoleTime", aMetaData, aCause)
{
if (aMetaData == TRACING_INTERVAL_END) {
CaptureStack();
}
}
virtual bool Equals(const nsDocShell::TimelineMarker* aOther)
virtual bool Equals(const TimelineMarker* aOther)
{
if (!nsDocShell::TimelineMarker::Equals(aOther)) {
if (!TimelineMarker::Equals(aOther)) {
return false;
}
// Console markers must have matching causes as well.
@ -969,7 +969,7 @@ Console::Method(JSContext* aCx, MethodName aMethodName,
if (jsString) {
nsAutoJSString key;
if (key.init(aCx, jsString)) {
mozilla::UniquePtr<nsDocShell::TimelineMarker> marker =
mozilla::UniquePtr<TimelineMarker> marker =
MakeUnique<ConsoleTimelineMarker>(docShell,
aMethodName == MethodTime ? TRACING_INTERVAL_START : TRACING_INTERVAL_END,
key);

Просмотреть файл

@ -1024,12 +1024,12 @@ EventListenerManager::GetDocShellForTarget()
return docShell;
}
class EventTimelineMarker : public nsDocShell::TimelineMarker
class EventTimelineMarker : public TimelineMarker
{
public:
EventTimelineMarker(nsDocShell* aDocShell, TracingMetadata aMetaData,
uint16_t aPhase, const nsAString& aCause)
: nsDocShell::TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause)
: TimelineMarker(aDocShell, "DOMEvent", aMetaData, aCause)
, mPhase(aPhase)
{
}
@ -1114,7 +1114,7 @@ EventListenerManager::HandleEventInternal(nsPresContext* aPresContext,
(*aDOMEvent)->GetType(typeStr);
uint16_t phase;
(*aDOMEvent)->GetEventPhase(&phase);
mozilla::UniquePtr<nsDocShell::TimelineMarker> marker =
mozilla::UniquePtr<TimelineMarker> marker =
MakeUnique<EventTimelineMarker>(ds, TRACING_INTERVAL_START,
phase, typeStr);
ds->AddProfileTimelineMarker(marker);

Просмотреть файл

@ -4471,11 +4471,11 @@ static void DrawForcedBackgroundColor(DrawTarget& aDrawTarget,
}
}
class LayerTimelineMarker : public nsDocShell::TimelineMarker
class LayerTimelineMarker : public TimelineMarker
{
public:
LayerTimelineMarker(nsDocShell* aDocShell, const nsIntRegion& aRegion)
: nsDocShell::TimelineMarker(aDocShell, "Layer", TRACING_EVENT)
: TimelineMarker(aDocShell, "Layer", TRACING_EVENT)
, mRegion(aRegion)
{
}
@ -4653,7 +4653,7 @@ FrameLayerBuilder::DrawPaintedLayer(PaintedLayer* aLayer,
bool isRecording;
docShell->GetRecordProfileTimelineMarkers(&isRecording);
if (isRecording) {
mozilla::UniquePtr<nsDocShell::TimelineMarker> marker =
mozilla::UniquePtr<TimelineMarker> marker =
MakeUnique<LayerTimelineMarker>(docShell, aRegionToDraw);
docShell->AddProfileTimelineMarker(marker);
}

Просмотреть файл

@ -444,7 +444,9 @@ public class GeckoMenuItem implements MenuItem {
@Override
public MenuItem setVisible(boolean visible) {
if (mVisible != visible) {
// Action views are not normal menu items and visibility can get out
// of sync unless we dispatch whenever required.
if (isActionItem() || mVisible != visible) {
mVisible = visible;
if (mShouldDispatchChanges) {
mMenu.onItemChanged(this);

Просмотреть файл

@ -6,7 +6,6 @@
<org.mozilla.gecko.tabs.TabsLayoutItemView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:gecko="http://schemas.android.com/apk/res-auto"
style="@style/TabsItem"
android:focusable="true"
android:id="@+id/info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -37,7 +36,7 @@
<!-- Use of baselineAlignBottom only supported from API 11+ - if this needs to work on lower API versions
we'll need to override getBaseLine() and return image height, but we assume this won't happen -->
<ImageButton android:id="@+id/close"
<ImageView android:id="@+id/close"
style="@style/TabsItemClose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

Просмотреть файл

@ -27,6 +27,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.DecelerateInterpolator;
import android.widget.AdapterView;
import android.widget.Button;
import android.widget.GridView;
import com.nineoldandroids.animation.Animator;
@ -94,12 +95,20 @@ class TabsGridLayout extends GridView
final int padding = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding);
final int paddingTop = resources.getDimensionPixelSize(R.dimen.new_tablet_tab_panel_grid_padding_top);
setPadding(padding, paddingTop, padding, padding);
setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
TabsLayoutItemView tab = (TabsLayoutItemView) view;
Tabs.getInstance().selectTab(tab.getTabId());
autoHidePanel();
}
});
}
private class TabsGridLayoutAdapter extends TabsLayoutAdapter {
final private Button.OnClickListener mCloseClickListener;
final private View.OnClickListener mSelectClickListener;
public TabsGridLayoutAdapter (Context context) {
super(context, R.layout.new_tablet_tabs_item_cell);
@ -110,22 +119,14 @@ class TabsGridLayout extends GridView
closeTab(v);
}
};
mSelectClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
TabsLayoutItemView tab = (TabsLayoutItemView) v;
Tabs.getInstance().selectTab(tab.getTabId());
autoHidePanel();
}
};
}
@Override
TabsLayoutItemView newView(int position, ViewGroup parent) {
final TabsLayoutItemView item = super.newView(position, parent);
item.setOnClickListener(mSelectClickListener);
item.setCloseOnClickListener(mCloseClickListener);
return item;
}

Просмотреть файл

@ -46,6 +46,7 @@ public class TabsLayoutAdapter extends BaseAdapter {
final void clear() {
mTabs = null;
notifyDataSetChanged(); // Be sure to call this whenever mTabs changes.
}
@ -71,6 +72,11 @@ public class TabsLayoutAdapter extends BaseAdapter {
return mTabs.indexOf(tab);
}
@Override
public boolean isEnabled(int position) {
return true;
}
@Override
final public TabsLayoutItemView getView(int position, View convertView, ViewGroup parent) {
final TabsLayoutItemView view;

Просмотреть файл

@ -32,7 +32,7 @@ public class TabsLayoutItemView extends LinearLayout
private int mTabId;
private TextView mTitle;
private ImageView mThumbnail;
private ImageButton mCloseButton;
private ImageView mCloseButton;
private TabThumbnailWrapper mThumbnailWrapper;
public TabsLayoutItemView(Context context, AttributeSet attrs) {
@ -50,6 +50,11 @@ public class TabsLayoutItemView extends LinearLayout
return drawableState;
}
@Override
public boolean isEnabled() {
return true;
}
@Override
public boolean isChecked() {
return mChecked;
@ -87,7 +92,7 @@ public class TabsLayoutItemView extends LinearLayout
super.onFinishInflate();
mTitle = (TextView) findViewById(R.id.title);
mThumbnail = (ImageView) findViewById(R.id.thumbnail);
mCloseButton = (ImageButton) findViewById(R.id.close);
mCloseButton = (ImageView) findViewById(R.id.close);
mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
if (NewTabletUI.isEnabled(getContext())) {

Просмотреть файл

@ -132,8 +132,12 @@
// In non-instant apply mode, we must try and use the last saved state
// from any previous opens of a child dialog instead of the value from
// preferences, to pick up any edits a user may have made.
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Components.interfaces.nsIScriptSecurityManager);
if (this.preferences.type == "child" &&
!this.instantApply && window.opener) {
!this.instantApply && window.opener &&
secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) {
var pdoc = window.opener.document;
// Try to find a preference element for the same preference.
@ -1053,7 +1057,10 @@
return false;
}
if (this.type == "child" && window.opener) {
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Components.interfaces.nsIScriptSecurityManager);
if (this.type == "child" && window.opener &&
secMan.isSystemPrincipal(window.opener.document.nodePrincipal)) {
var psvc = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
var instantApply = psvc.getBoolPref("browser.preferences.instantApply");

Просмотреть файл

@ -0,0 +1,780 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const events = require("sdk/event/core");
const protocol = require("devtools/server/protocol");
const { Cu, Ci } = require("chrome");
const { on, once, off, emit } = events;
const { method, Arg, Option, RetVal, types } = protocol;
const { sandbox, evaluate } = require('sdk/loader/sandbox');
const { Class } = require("sdk/core/heritage");
const { PlainTextConsole } = require('sdk/console/plain-text');
const { DirectorRegistry } = require("./director-registry");
/**
* E10S child setup helper
*/
const {DebuggerServer} = require("devtools/server/main");
/**
* Error Messages
*/
const ERR_MESSAGEPORT_FINALIZED = "message port finalized";
const ERR_DIRECTOR_UNKNOWN_SCRIPTID = "unkown director-script id";
const ERR_DIRECTOR_UNINSTALLED_SCRIPTID = "uninstalled director-script id";
/**
* Type describing a messageport event
*/
types.addDictType("messageportevent", {
isTrusted: "boolean",
data: "nullable:primitive",
origin: "nullable:string",
lastEventId: "nullable:string",
source: "messageport",
ports: "nullable:array:messageport"
});
/**
* A MessagePort Actor allowing communication through messageport events
* over the remote debugging protocol.
*/
let MessagePortActor = exports.MessagePortActor = protocol.ActorClass({
typeName: "messageport",
/**
* Create a MessagePort actor.
*
* @param DebuggerServerConnection conn
* The server connection.
* @param MessagePort port
* The wrapped MessagePort.
*/
initialize: function(conn, port) {
protocol.Actor.prototype.initialize.call(this, conn);
// NOTE: can't get a weak reference because we need to subscribe events
// using port.onmessage or addEventListener
this.port = port;
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Sends a message on the wrapped message port.
*
* @param Object msg
* The JSON serializable message event payload
*/
postMessage: method(function (msg) {
if (!this.port) {
console.error(ERR_MESSAGEPORT_FINALIZED);
return;
}
this.port.postMessage(msg);
}, {
oneway: true,
request: {
msg: Arg(0, "nullable:json")
}
}),
/**
* Starts to receive and send queued messages on this message port.
*/
start: method(function () {
if (!this.port) {
console.error(ERR_MESSAGEPORT_FINALIZED);
return;
}
// NOTE: set port.onmessage to a function is an implicit start
// and starts to send queued messages.
// On the client side we should set MessagePortClient.onmessage
// to a setter which register an handler to the message event
// and call the actor start method to start receiving messages
// from the MessagePort's queue.
this.port.onmessage = (evt) => {
var ports;
// TODO: test these wrapped ports
if (Array.isArray(evt.ports)) {
ports = evt.ports.map((port) => {
let actor = new MessagePortActor(this.conn, port);
this.manage(actor);
return actor;
});
}
emit(this, "message", {
isTrusted: evt.isTrusted,
data: evt.data,
origin: evt.origin,
lastEventId: evt.lastEventId,
source: this,
ports: ports
});
};
}, {
oneway: true,
request: {}
}),
/**
* Starts to receive and send queued messages on this message port, or
* raise an exception if the port is null
*/
close: method(function () {
if (!this.port) {
console.error(ERR_MESSAGEPORT_FINALIZED);
return;
}
this.port.onmessage = null;
this.port.close();
}, {
oneway: true,
request: {}
}),
finalize: method(function () {
this.close();
this.port = null;
}, {
oneway: true
}),
/**
* Events emitted by this actor.
*/
events: {
"message": {
type: "message",
msg: Arg(0, "nullable:messageportevent")
}
}
});
/**
* The corresponding Front object for the MessagePortActor.
*/
let MessagePortFront = exports.MessagePortFront = protocol.FrontClass(MessagePortActor, {
initialize: function (client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
}
});
/**
* Type describing a director-script error
*/
types.addDictType("director-script-error", {
directorScriptId: "string",
message: "string",
stack: "string",
fileName: "string",
lineNumber: "number",
columnNumber: "number"
});
/**
* Type describing a director-script attach event
*/
types.addDictType("director-script-attach", {
directorScriptId: "string",
url: "string",
innerId: "number",
port: "nullable:messageport"
});
/**
* Type describing a director-script detach event
*/
types.addDictType("director-script-detach", {
directorScriptId: "string",
innerId: "number"
});
/**
* The Director Script Actor manage javascript code running in a non-privileged sandbox with the same
* privileges of the target global (browser tab or a firefox os app).
*
* After retrieving an instance of this actor (from the tab director actor), you'll need to set it up
* by calling setup().
*
* After the setup, this actor will automatically attach/detach the content script (and optionally a
* directly connect the debugger client and the content script using a MessageChannel) on tab
* navigation.
*/
let DirectorScriptActor = exports.DirectorScriptActor = protocol.ActorClass({
typeName: "director-script",
/**
* Events emitted by this actor.
*/
events: {
"error": {
type: "error",
data: Arg(0, "director-script-error")
},
"attach": {
type: "attach",
data: Arg(0, "director-script-attach")
},
"detach": {
type: "detach",
data: Arg(0, "director-script-detach")
}
},
/**
* Creates the director script actor
*
* @param DebuggerServerConnection conn
* The server connection.
* @param Actor tabActor
* The tab (or root) actor.
* @param String scriptId
* The director-script id.
* @param String scriptCode
* The director-script javascript source.
* @param Object scriptOptions
* The director-script options object.
*/
initialize: function(conn, tabActor, { scriptId, scriptCode, scriptOptions }) {
protocol.Actor.prototype.initialize.call(this, conn, tabActor);
this.tabActor = tabActor;
this._scriptId = scriptId;
this._scriptCode = scriptCode;
this._scriptOptions = scriptOptions;
this._setupCalled = false;
this._onGlobalCreated = this._onGlobalCreated.bind(this);
this._onGlobalDestroyed = this._onGlobalDestroyed.bind(this);
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Starts listening to the tab global created, in order to create the director-script sandbox
* using the configured scriptCode, attached/detached automatically to the tab
* window on tab navigation.
*
* @param Boolean reload
* attach the page immediately or reload it first.
* @param Boolean skipAttach
* skip the attach
*/
setup: method(function ({ reload, skipAttach }) {
if (this._setupCalled) {
// do nothing
return;
}
this._setupCalled = true;
on(this.tabActor, "window-ready", this._onGlobalCreated);
on(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
// optional skip attach (needed by director-manager for director scripts bulk activation)
if (skipAttach) {
return;
}
if (reload) {
this.window.location.reload();
} else {
// fake a global created event to attach without reload
this._onGlobalCreated({ id: getWindowID(this.window), window: this.window, isTopLevel: true });
}
}, {
request: {
reload: Option(0, "boolean"),
skipAttach: Option(0, "boolean")
},
oneway: true
}),
/**
* Get the attached MessagePort actor if any
*/
getMessagePort: method(function () {
return this._messagePortActor;
}, {
request: { },
response: {
port: RetVal("nullable:messageport")
}
}),
/**
* Stop listening for document global changes, destroy the content worker and puts
* this actor to hibernation.
*/
finalize: method(function () {
if (!this._setupCalled) {
return;
}
off(this.tabActor, "window-ready", this._onGlobalCreated);
off(this.tabActor, "window-destroyed", this._onGlobalDestroyed);
this._onGlobalDestroyed({ id: this._lastAttachedWinId });
this._setupCalled = false;
}, {
oneway: true
}),
// local helpers
get window() {
return this.tabActor.window;
},
/* event handlers */
_onGlobalCreated: function({ id, window, isTopLevel }) {
if (!isTopLevel) {
// filter iframes
return;
}
if (this._lastAttachedWinId) {
// if we have received a global created without a previous global destroyed,
// it's time to cleanup the previous state
this._onGlobalDestroyed(this._lastAttachedWinId);
}
// TODO: check if we want to share a single sandbox per global
// for multiple debugger clients
// create & attach the new sandbox
this._scriptSandbox = new DirectorScriptSandbox({
scriptId: this._scriptId,
scriptCode: this._scriptCode,
scriptOptions: this._scriptOptions
});
try {
// attach the global window
this._lastAttachedWinId = id;
var port = this._scriptSandbox.attach(window, id);
this._onDirectorScriptAttach(window, port);
} catch(e) {
this._onDirectorScriptError(e);
}
},
_onGlobalDestroyed: function({ id }) {
if (id !== this._lastAttachedWinId) {
// filter destroyed globals
return;
}
// unmanage and cleanup the messageport actor
if (this._messagePortActor) {
this.unmanage(this._messagePortActor);
this._messagePortActor = null;
}
// NOTE: destroy here the old worker
if (this._scriptSandbox) {
this._scriptSandbox.destroy(this._onDirectorScriptError.bind(this));
// send a detach event to the debugger client
emit(this, "detach", {
directorScriptId: this._scriptId,
innerId: this._lastAttachedWinId
});
this._lastAttachedWinId = null;
this._scriptSandbox = null;
}
},
_onDirectorScriptError: function(error) {
// route the content script error to the debugger client
emit(this, "error", {
directorScriptId: this._scriptId,
message: error.toString(),
stack: error.stack,
fileName: error.fileName,
lineNumber: error.lineNumber,
columnNumber: error.columnNumber
});
},
_onDirectorScriptAttach: function(window, port) {
let portActor = new MessagePortActor(this.conn, port);
this.manage(portActor);
this._messagePortActor = portActor;
emit(this, "attach", {
directorScriptId: this._scriptId,
url: (window && window.location) ? window.location.toString() : "",
innerId: this._lastAttachedWinId,
port: this._messagePortActor
});
}
});
/**
* The corresponding Front object for the DirectorScriptActor.
*/
let DirectorScriptFront = exports.DirectorScriptFront = protocol.FrontClass(DirectorScriptActor, {
initialize: function (client, form) {
protocol.Front.prototype.initialize.call(this, client, form);
}
});
/**
* The DirectorManager Actor is a tab actor which manages enabling/disabling director scripts.
*/
const DirectorManagerActor = exports.DirectorManagerActor = protocol.ActorClass({
typeName: "director-manager",
/**
* Events emitted by this actor.
*/
events: {
"director-script-error": {
type: "error",
data: Arg(0, "director-script-error")
},
"director-script-attach": {
type: "attach",
data: Arg(0, "director-script-attach")
},
"director-script-detach": {
type: "detach",
data: Arg(0, "director-script-detach")
}
},
/* init & destroy methods */
initialize: function(conn, tabActor) {
protocol.Actor.prototype.initialize.call(this, conn);
this.tabActor = tabActor;
this._directorScriptActorsMap = new Map();
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
/**
* Retrieves the list of installed director-scripts.
*/
list: method(function () {
var enabled_script_ids = [for (id of this._directorScriptActorsMap.keys()) id];
return {
installed: DirectorRegistry.list(),
enabled: enabled_script_ids
};
}, {
response: {
directorScripts: RetVal("json")
}
}),
/**
* Bulk enabling director-scripts.
*
* @param Array[String] selectedIds
* The list of director-script ids to be enabled,
* ["*"] will activate all the installed director-scripts
* @param Boolean reload
* optionally reload the target window
*/
enableByScriptIds: method(function(selectedIds, { reload }) {
if (selectedIds && selectedIds.length === 0) {
// filtered all director scripts ids
return;
}
for (let scriptId of DirectorRegistry.list()) {
// filter director script ids
if (selectedIds.indexOf("*") < 0 &&
selectedIds.indexOf(scriptId) < 0) {
continue;
}
let actor = this.getByScriptId(scriptId);
// skip attach if reload is true (activated director scripts
// will be automatically attached on the final reload)
actor.setup({ reload: false, skipAttach: reload });
}
if (reload) {
this.tabActor.window.location.reload();
}
}, {
oneway: true,
request: {
selectedIds: Arg(0, "array:string"),
reload: Option(1, "boolean")
}
}),
/**
* Bulk disabling director-scripts.
*
* @param Array[String] selectedIds
* The list of director-script ids to be disable,
* ["*"] will de-activate all the enable director-scripts
* @param Boolean reload
* optionally reload the target window
*/
disableByScriptIds: method(function(selectedIds, { reload }) {
if (selectedIds && selectedIds.length === 0) {
// filtered all director scripts ids
return;
}
for (let scriptId of this._directorScriptActorsMap.keys()) {
// filter director script ids
if (selectedIds.indexOf("*") < 0 &&
selectedIds.indexOf(scriptId) < 0) {
continue;
}
let actor = this._directorScriptActorsMap.get(scriptId);
this._directorScriptActorsMap.delete(scriptId);
// finalize the actor (which will produce director-script-detach event)
actor.finalize();
// unsubscribe event handlers on the disabled actor
off(actor);
this.unmanage(actor);
}
if (reload) {
this.tabActor.window.location.reload();
}
}, {
oneway: true,
request: {
selectedIds: Arg(0, "array:string"),
reload: Option(1, "boolean")
}
}),
/**
* Retrieves the actor instance of an installed director-script
* (and create the actor instance if it doesn't exists yet).
*/
getByScriptId: method(function(scriptId) {
var id = scriptId;
// raise an unknown director-script id exception
if (!DirectorRegistry.checkInstalled(id)) {
console.error(ERR_DIRECTOR_UNKNOWN_SCRIPTID, id);
throw Error(ERR_DIRECTOR_UNKNOWN_SCRIPTID);
}
// get a previous created actor instance
let actor = this._directorScriptActorsMap.get(id);
// create a new actor instance
if (!actor) {
let directorScriptDefinition = DirectorRegistry.get(id);
// test lazy director-script (e.g. uninstalled in the parent process)
if (!directorScriptDefinition) {
console.error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID, id);
throw Error(ERR_DIRECTOR_UNINSTALLED_SCRIPTID);
}
actor = new DirectorScriptActor(this.conn, this.tabActor, directorScriptDefinition);
this._directorScriptActorsMap.set(id, actor);
on(actor, "error", emit.bind(null, this, "director-script-error"));
on(actor, "attach", emit.bind(null, this, "director-script-attach"));
on(actor, "detach", emit.bind(null, this, "director-script-detach"));
this.manage(actor);
}
return actor;
}, {
request: {
scriptId: Arg(0, "string")
},
response: {
directorScript: RetVal("director-script")
}
}),
finalize: method(function() {
this.disableByScriptIds(["*"], false);
}, {
oneway: true
})
});
/**
* The corresponding Front object for the DirectorManagerActor.
*/
exports.DirectorManagerFront = protocol.FrontClass(DirectorManagerActor, {
initialize: function(client, { directorManagerActor }) {
protocol.Front.prototype.initialize.call(this, client, {
actor: directorManagerActor
});
this.manage(this);
}
});
/* private helpers */
/**
* DirectorScriptSandbox is a private utility class, which attach a non-priviliged sandbox
* to a target window.
*/
const DirectorScriptSandbox = Class({
initialize: function({scriptId, scriptCode, scriptOptions}) {
this._scriptId = scriptId;
this._scriptCode = scriptCode;
this._scriptOptions = scriptOptions;
},
attach: function(window, innerId) {
this._innerId = innerId,
this._window = window;
this._proto = Cu.createObjectIn(this._window);
var id = this._scriptId;
var uri = this._scriptCode;
this._sandbox = sandbox(window, {
sandboxName: uri,
sandboxPrototype: this._proto,
sameZoneAs: window,
wantXrays: true,
wantComponents: false,
wantExportHelpers: false,
metadata: {
URI: uri,
addonID: id,
SDKDirectorScript: true,
"inner-window-id": innerId
}
});
// create a CommonJS module object which match the interface from addon-sdk
// (addon-sdk/sources/lib/toolkit/loader.js#L678-L686)
var module = Cu.cloneInto(Object.create(null, {
id: { enumerable: true, value: id },
uri: { enumerable: true, value: uri },
exports: { enumerable: true, value: Cu.createObjectIn(this._sandbox) }
}), this._sandbox);
// create a console API object
let directorScriptConsole = new PlainTextConsole(null, this._innerId);
// inject CommonJS module globals into the sandbox prototype
Object.defineProperties(this._proto, {
module: { enumerable: true, value: module },
exports: { enumerable: true, value: module.exports },
console: {
enumerable: true,
value: Cu.cloneInto(directorScriptConsole, this._sandbox, { cloneFunctions: true })
}
});
Object.defineProperties(this._sandbox, {
require: {
enumerable: true,
value: Cu.cloneInto(function() {
throw Error("NOT IMPLEMENTED");
}, this._sandbox, { cloneFunctions: true })
}
});
// evaluate the director script source in the sandbox
evaluate(this._sandbox, this._scriptCode, this._scriptId);
// prepare the messageport connected to the debugger client
let { port1, port2 } = new this._window.MessageChannel();
// prepare the unload callbacks queue
var sandboxOnUnloadQueue = this._sandboxOnUnloadQueue = [];
// create the attach options
var attachOptions = this._attachOptions = Cu.createObjectIn(this._sandbox);
Object.defineProperties(attachOptions, {
port: { enumerable: true, value: port1 },
window: { enumerable: true, value: window },
scriptOptions: { enumerable: true, value: Cu.cloneInto(this._scriptOptions, this._sandbox) },
onUnload: {
enumerable: true,
value: Cu.cloneInto(function (cb) {
// collect unload callbacks
if (typeof cb == "function") {
sandboxOnUnloadQueue.push(cb);
}
}, this._sandbox, { cloneFunctions: true })
}
});
// select the attach method
var exports = this._proto.module.exports;
if ("attachMethod" in this._scriptOptions) {
this._sandboxOnAttach = exports[this._scriptOptions.attachMethod];
} else {
this._sandboxOnAttach = exports;
}
if (typeof this._sandboxOnAttach !== "function") {
throw Error("the configured attachMethod '" +
(this._scriptOptions.attachMethod || "module.exports") +
"' is not exported by the directorScript");
}
// call the attach method
this._sandboxOnAttach.call(this._sandbox, attachOptions);
return port2;
},
destroy: function(onError) {
// evaluate queue unload methods if any
while(this._sandboxOnUnloadQueue.length > 0) {
let cb = this._sandboxOnUnloadQueue.pop();
try {
cb();
} catch(e) {
console.error("Exception on DirectorScript Sandbox destroy", e);
onError(e);
}
}
Cu.nukeSandbox(this._sandbox);
}
});
function getWindowID(window) {
return window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.currentInnerWindowID;
}

Просмотреть файл

@ -0,0 +1,295 @@
/* -*- indent-tabs-mode: nil; js-indent-level: 2; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const protocol = require("devtools/server/protocol");
const { method, Arg, Option, RetVal } = protocol;
const {DebuggerServer} = require("devtools/server/main");
/**
* Error Messages
*/
const ERR_DIRECTOR_INSTALL_TWICE = "Trying to install a director-script twice";
const ERR_DIRECTOR_INSTALL_EMPTY = "Trying to install an empty director-script";
const ERR_DIRECTOR_UNINSTALL_UNKNOWN = "Trying to uninstall an unkown director-script";
const ERR_DIRECTOR_PARENT_UNKNOWN_METHOD = "Unknown parent process method";
const ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD = "Unexpected call to notImplemented method";
const ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES = "Unexpected multiple replies to called parent method";
const ERR_DIRECTOR_CHILD_NO_REPLY = "Unexpected no reply to called parent method";
/**
* Director Registry
*/
// Map of director scripts ids to director script definitions
var gDirectorScripts = Object.create(null);
const DirectorRegistry = exports.DirectorRegistry = {
/**
* Register a Director Script with the debugger server.
* @param id string
* The ID of a director script.
* @param directorScriptDef object
* The definition of a director script.
*/
install: function (id, scriptDef) {
if (id in gDirectorScripts) {
console.error(ERR_DIRECTOR_INSTALL_TWICE,id);
return false;
}
if (!scriptDef) {
console.error(ERR_DIRECTOR_INSTALL_EMPTY, id);
return false;
}
gDirectorScripts[id] = scriptDef;
return true;
},
/**
* Unregister a Director Script with the debugger server.
* @param id string
* The ID of a director script.
*/
uninstall: function(id) {
if (id in gDirectorScripts) {
delete gDirectorScripts[id];
return true;
}
console.error(ERR_DIRECTOR_UNINSTALL_UNKNOWN, id);
return false;
},
/**
* Returns true if a director script id has been registered.
* @param id string
* The ID of a director script.
*/
checkInstalled: function (id) {
return (this.list().indexOf(id) >= 0);
},
/**
* Returns a registered director script definition by id.
* @param id string
* The ID of a director script.
*/
get: function(id) {
return gDirectorScripts[id];
},
/**
* Returns an array of registered director script ids.
*/
list: function() {
return Object.keys(gDirectorScripts);
},
/**
* Removes all the registered director scripts.
*/
clear: function() {
gDirectorScripts = Object.create(null);
}
};
/**
* E10S parent/child setup helpers
*/
let gTrackedMessageManager = new Set();
exports.setupParentProcess = function setupParentProcess({mm, childID}) {
// prevents multiple subscriptions on the same messagemanager
if (gTrackedMessageManager.has(mm)) {
return;
}
gTrackedMessageManager.add(mm);
// listen for director-script requests from the child process
mm.addMessageListener("debug:director-registry-request", handleChildRequest);
DebuggerServer.once("disconnected-from-child:" + childID, handleMessageManagerDisconnected);
/* parent process helpers */
function handleMessageManagerDisconnected(evt, { mm: disconnected_mm }) {
// filter out not subscribed message managers
if (disconnected_mm !== mm || !gTrackedMessageManager.has(mm)) {
return;
}
gTrackedMessageManager.delete(mm);
// unregister for director-script requests handlers from the parent process (if any)
mm.removeMessageListener("debug:director-registry-request", handleChildRequest);
}
function handleChildRequest(msg) {
switch (msg.json.method) {
case "get":
return DirectorRegistry.get(msg.json.args[0]);
case "list":
return DirectorRegistry.list();
default:
console.error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD, msg.json.method);
throw new Error(ERR_DIRECTOR_PARENT_UNKNOWN_METHOD);
}
}
};
// skip child setup if this actor module is not running in a child process
if (DebuggerServer.isInChildProcess) {
setupChildProcess();
}
function setupChildProcess() {
const { sendSyncMessage } = DebuggerServer.parentMessageManager;
DebuggerServer.setupInParent({
module: "devtools/server/actors/director-registry",
setupParent: "setupParentProcess"
});
DirectorRegistry.install = notImplemented.bind(null, "install");
DirectorRegistry.uninstall = notImplemented.bind(null, "uninstall");
DirectorRegistry.clear = notImplemented.bind(null, "clear");
DirectorRegistry.get = callParentProcess.bind(null, "get");
DirectorRegistry.list = callParentProcess.bind(null, "list");
/* child process helpers */
function notImplemented(method) {
console.error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD, method);
throw Error(ERR_DIRECTOR_CHILD_NOTIMPLEMENTED_METHOD);
}
function callParentProcess(method, ...args) {
var reply = sendSyncMessage("debug:director-registry-request", {
method: method,
args: args
});
if (reply.length === 0) {
console.error(ERR_DIRECTOR_CHILD_NO_REPLY);
throw Error(ERR_DIRECTOR_CHILD_NO_REPLY);
} else if (reply.length > 1) {
console.error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
throw Error(ERR_DIRECTOR_CHILD_MULTIPLE_REPLIES);
}
return reply[0];
};
};
/**
* The DirectorRegistry Actor is a global actor which manages install/uninstall of
* director scripts definitions.
*/
const DirectorRegistryActor = exports.DirectorRegistryActor = protocol.ActorClass({
typeName: "director-registry",
/* init & destroy methods */
initialize: function(conn, parentActor) {
protocol.Actor.prototype.initialize.call(this, conn);
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
this.finalize();
},
finalize: method(function() {
// nothing to cleanup
}, {
oneway: true
}),
/**
* Install a new director-script definition.
*
* @param String id
* The director-script definition identifier.
* @param String scriptCode
* The director-script javascript source.
* @param Object scriptOptions
* The director-script option object.
*/
install: method(function(id, { scriptCode, scriptOptions }) {
// TODO: add more checks on id format?
if (!id || id.length === 0) {
throw Error("director-script id is mandatory");
}
if (!scriptCode) {
throw Error("director-script scriptCode is mandatory");
}
return DirectorRegistry.install(id, {
scriptId: id,
scriptCode: scriptCode,
scriptOptions: scriptOptions
});
}, {
request: {
scriptId: Arg(0, "string"),
scriptCode: Option(1, "string"),
scriptOptions: Option(1, "nullable:json")
},
response: {
success: RetVal("boolean")
}
}),
/**
* Uninstall a director-script definition.
*
* @param String id
* The identifier of the director-script definition to be removed
*/
uninstall: method(function (id) {
return DirectorRegistry.uninstall(id);
}, {
request: {
scritpId: Arg(0, "string")
},
response: {
success: RetVal("boolean")
}
}),
/**
* Retrieves the list of installed director-scripts.
*/
list: method(function () {
return DirectorRegistry.list();
}, {
response: {
directorScripts: RetVal("array:string")
}
})
});
/**
* The corresponding Front object for the DirectorRegistryActor.
*/
exports.DirectorRegistryFront = protocol.FrontClass(DirectorRegistryActor, {
initialize: function(client, { directorRegistryActor }) {
protocol.Front.prototype.initialize.call(this, client, {
actor: directorRegistryActor
});
this.manage(this);
}
});

Просмотреть файл

@ -158,6 +158,8 @@ RootActor.prototype = {
addNewRule: true,
// Whether the dom node actor implements the getUniqueSelector method
getUniqueSelector: true,
// Whether the director scripts are supported
directorScripts: true,
// Whether the debugger server supports
// blackboxing/pretty-printing (not supported in Fever Dream yet)
noBlackBoxing: false,

Просмотреть файл

@ -238,10 +238,42 @@ var PageStyleActor = protocol.ActorClass({
}
}),
/**
* Get all the fonts from a page.
*
* @param object options
* `includePreviews`: Whether to also return image previews of the fonts.
* `previewText`: The text to display in the previews.
* `previewFontSize`: The font size of the text in the previews.
*
* @returns object
* object with 'fontFaces', a list of fonts that apply to this node.
*/
getAllUsedFontFaces: method(function(options) {
let windows = this.inspector.tabActor.windows;
let fontsList = [];
for(let win of windows){
fontsList = [...fontsList,
...this.getUsedFontFaces(win.document.body, options)];
}
return fontsList;
},
{
request: {
includePreviews: Option(0, "boolean"),
previewText: Option(0, "string"),
previewFontSize: Option(0, "string"),
previewFillStyle: Option(0, "string")
},
response: {
fontFaces: RetVal("array:fontface")
}
}),
/**
* Get the font faces used in an element.
*
* @param NodeActor node
* @param NodeActor node / actual DOM node
* The node to get fonts from.
* @param object options
* `includePreviews`: Whether to also return image previews of the fonts.
@ -252,11 +284,12 @@ var PageStyleActor = protocol.ActorClass({
* object with 'fontFaces', a list of fonts that apply to this node.
*/
getUsedFontFaces: method(function(node, options) {
let contentDocument = node.rawNode.ownerDocument;
// node.rawNode is defined for NodeActor objects
let actualNode = node.rawNode || node;
let contentDocument = actualNode.ownerDocument;
// We don't get fonts for a node, but for a range
let rng = contentDocument.createRange();
rng.selectNodeContents(node.rawNode);
rng.selectNodeContents(actualNode);
let fonts = DOMUtils.getUsedFontFaces(rng);
let fontsArray = [];

Просмотреть файл

@ -389,6 +389,11 @@ var DebuggerServer = {
constructor: "DeviceActor",
type: { global: true }
});
this.registerModule("devtools/server/actors/director-registry", {
prefix: "directorRegistry",
constructor: "DirectorRegistryActor",
type: { global: true }
});
},
/**
@ -504,6 +509,11 @@ var DebuggerServer = {
constructor: "TimelineActor",
type: { global: true, tab: true }
});
this.registerModule("devtools/server/actors/director-manager", {
prefix: "directorManager",
constructor: "DirectorManagerActor",
type: { global: false, tab: true }
});
if ("nsIProfiler" in Ci) {
this.registerModule("devtools/server/actors/profiler", {
prefix: "profiler",

Просмотреть файл

@ -42,6 +42,8 @@ EXTRA_JS_MODULES.devtools.server.actors += [
'actors/common.js',
'actors/csscoverage.js',
'actors/device.js',
'actors/director-manager.js',
'actors/director-registry.js',
'actors/eventlooplag.js',
'actors/framerate.js',
'actors/gcli.js',

Просмотреть файл

@ -1,5 +1,7 @@
[DEFAULT]
support-files =
director-helpers.js
director-script-target.html
inspector-helpers.js
inspector-styles-data.css
inspector-styles-data.html
@ -73,5 +75,8 @@ skip-if = buildapp == 'mulet'
[test_preference.html]
[test_connectToChild.html]
skip-if = buildapp == 'mulet'
[test_director.html]
[test_director_connectToChild.html]
+skip-if = buildapp == 'mulet'
[test_attachProcess.html]
skip-if = buildapp == 'mulet'

Просмотреть файл

@ -0,0 +1,100 @@
var Cu = Components.utils;
Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
Cu.import("resource://gre/modules/devtools/Loader.jsm");
const Services = devtools.require("Services");
// Always log packets when running tests.
Services.prefs.setBoolPref("devtools.debugger.log", true);
Services.prefs.setBoolPref("dom.mozBrowserFramesEnabled", true);
SimpleTest.registerCleanupFunction(function() {
Services.prefs.clearUserPref("devtools.debugger.log");
Services.prefs.clearUserPref("dom.mozBrowserFramesEnabled");
});
const {Class} = devtools.require("sdk/core/heritage");
const {promiseInvoke} = devtools.require("devtools/async-utils");
const { DirectorRegistry,
DirectorRegistryFront } = devtools.require("devtools/server/actors/director-registry");
const { DirectorManagerFront } = devtools.require("devtools/server/actors/director-manager");
const protocol = devtools.require("devtools/server/protocol");
const {Task} = devtools.require("resource://gre/modules/Task.jsm");
/***********************************
* director helpers functions
**********************************/
function waitForEvent(target, name) {
return new Promise((resolve, reject) => {
target.once(name, (...args) => { resolve(args); });
});
}
function* newConnectedDebuggerClient(opts) {
var transport = DebuggerServer.connectPipe();
var client = new DebuggerClient(transport);
yield promiseInvoke(client, client.connect);
var root = yield promiseInvoke(client, client.listTabs);
return {
client: client,
root: root,
transport: transport
};
}
function* installTestDirectorScript(client, root, scriptId, scriptDefinition) {
var directorRegistryClient = new DirectorRegistryFront(client, root);
yield directorRegistryClient.install(scriptId, scriptDefinition);
directorRegistryClient.destroy();
}
function* getTestDirectorScript(manager, tab, scriptId) {
var directorScriptClient = yield manager.getByScriptId(scriptId);
return directorScriptClient;
}
function purgeInstalledDirectorScripts() {
DirectorRegistry.clear();
}
function* installDirectorScriptAndWaitAttachOrError({client, root, manager,
scriptId, scriptDefinition}) {
yield installTestDirectorScript(client, root, scriptId, scriptDefinition);
var selectedTab = root.tabs[root.selected];
var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, scriptId);
var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
testDirectorScriptClient.setup({reload: false});
var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach,
waitForDirectorScriptError]);
testDirectorScriptClient.finalize();
return receivedEvent;
}
function assertIsDirectorScriptError(error) {
ok(!!error, "received error should be defined");
ok(!!error.message, "errors should contain a message");
ok(!!error.stack, "errors should contain a stack trace");
ok(!!error.fileName, "errors should contain a fileName");
ok(typeof error.columnNumber == "number", "errors should contain a columnNumber");
ok(typeof error.lineNumber == "number", "errors should contain a lineNumber");
ok(!!error.directorScriptId, "errors should contain a directorScriptId");
}

Просмотреть файл

@ -0,0 +1,15 @@
<html>
<head>
<script>
// change the eval function to ensure the window object in the debug-script is correctly wrapped
window.eval = function () {
return "unsecure-eval-called";
};
var globalAccessibleVar = "global-value";
</script>
</head>
<body>
<h1>debug script target</h1>
</body>
</html>

Просмотреть файл

@ -0,0 +1,479 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=
-->
<head>
<meta charset="utf-8">
<title>Test for Bug </title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script type="application/javascript;version=1.8" src="./director-helpers.js"></script>
<script type="application/javascript;version=1.8">
window.onload = function() {
Task.spawn(function* () {
SimpleTest.waitForExplicitFinish();
var tests = [
runDirectorScriptModuleExports,
runDirectorScriptErrorOnNoAttachExports,
runDirectorScriptErrorOnLoadTest,
runDirectorScriptErrorOnRequire,
runDirectorScriptErrorOnUnloadTest,
runDirectorScriptSetupAndReceiveMessagePortTest,
runDirectorEnableDirectorScriptsTest,
runDirectorScriptDetachEventTest,
runDirectorScriptWindowEval
].map((testCase) => {
return function* () {
setup();
yield testCase().then(null, (e) => {
console.error("Exception during testCase run", e);
ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
});
teardown();
};
});
for (var test of tests) {
yield test();
}
}).then(
function success() {
SimpleTest.finish()
},
function error(e) {
console.error("Exception during testCase run", e);
ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
SimpleTest.finish();
}
);
};
var targetWin = null;
function setup() {
if (!DebuggerServer.initialized) {
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
SimpleTest.registerCleanupFunction(teardown);
}
}
function teardown() {
purgeInstalledDirectorScripts();
DebuggerServer.destroy();
if (targetWin) {
targetWin.close();
}
}
/***********************************
* test cases
**********************************/
function runDirectorScriptModuleExports() {
targetWin = window.open("about:blank");
var testDirectorScriptModuleExports = {
scriptCode: "(" + (function() {
module.exports = function() {};
}).toString() + ")();",
scriptOptions: {}
}
var testDirectorScriptAttachMethodOption = {
scriptCode: "(" + (function() {
exports.attach = function() {};
}).toString() + ")();",
scriptOptions: {
attachMethod: "attach"
}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
var selectedTab = root.tabs[root.selected];
var manager = new DirectorManagerFront(client, selectedTab);
var receivedEvent1 = yield installDirectorScriptAndWaitAttachOrError({
client: client, root: root, manager: manager,
scriptId: "testDirectorscriptModuleExports",
scriptDefinition: testDirectorScriptModuleExports
});
ok(!!receivedEvent1.port, "received attach from testDirectorScriptModuleExports");
var receivedEvent2 = yield installDirectorScriptAndWaitAttachOrError({
client: client, root: root, manager: manager,
scriptId: "testDirectorscriptAttachMethodOption",
scriptDefinition: testDirectorScriptModuleExports
});
ok(!!receivedEvent2.port, "received attach event from testDirectorScriptAttachMethodOption");
client.close();
})
}
function runDirectorScriptErrorOnNoAttachExports() {
targetWin = window.open("about:blank");
var testDirectorScriptRaiseErrorOnNoAttachExports = {
scriptCode: "(" + (function() {
// this director script should raise an error
// because it doesn't export any attach method
}).toString() + ")();",
scriptOptions: {}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
var selectedTab = root.tabs[root.selected];
var manager = new DirectorManagerFront(client, selectedTab);
var error = yield installDirectorScriptAndWaitAttachOrError({
client: client, root: root, manager: manager,
scriptId: "testDirectorscriptRaiseErrorOnNoAttachExports",
scriptDefinition: testDirectorScriptRaiseErrorOnNoAttachExports
});
assertIsDirectorScriptError(error);
client.close();
});
}
function runDirectorScriptErrorOnRequire() {
targetWin = window.open("about:blank");
var testDirectorScriptRaiseErrorOnRequire = {
scriptCode: "(" + (function() {
// this director script should raise an error
// because require raise a "not implemented" exception
console.log("PROVA", this)
require("fake_module");
}).toString() + ")();",
scriptOptions: {}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
var selectedTab = root.tabs[root.selected];
var manager = new DirectorManagerFront(client, selectedTab);
var error = yield installDirectorScriptAndWaitAttachOrError({
client: client, root: root, manager: manager,
scriptId: "testDirectorscriptRaiseErrorOnRequire",
scriptDefinition: testDirectorScriptRaiseErrorOnRequire
});
assertIsDirectorScriptError(error);
is(error.message, "Error: NOT IMPLEMENTED", "error message should contains the expected error message");
client.close();
});
}
function runDirectorScriptErrorOnLoadTest() {
targetWin = window.open("about:blank");
var testDirectorScriptRaiseErrorOnLoad = {
scriptCode: "(" + (function() {
// this will raise an exception on evaluating
// the director script
raise.an_error.during.content_script.load();
}).toString() + ")();",
scriptOptions: {}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
yield installTestDirectorScript(client, root, "testDirectorScript",
testDirectorScriptRaiseErrorOnLoad);
var selectedTab = root.tabs[root.selected];
var manager = new DirectorManagerFront(client, selectedTab);
var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
// activate the director script without window reloading
testDirectorScriptClient.setup({reload: false});
var [error] = yield waitForDirectorScriptError;
assertIsDirectorScriptError(error);
client.close();
});
}
function runDirectorScriptErrorOnUnloadTest() {
targetWin = window.open("about:blank");
var testDirectorScriptRaiseErrorOnUnload = {
scriptCode: "(" + (function() {
module.exports = function({onUnload}) {
// this will raise an exception on unload the director script
onUnload(function() {
raise_an_error_onunload();
});
};
}).toString() + ")();",
scriptOptions: {}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
yield installTestDirectorScript(client, root, "testDirectorScript",
testDirectorScriptRaiseErrorOnUnload);
var selectedTab = root.tabs[root.selected];
var manager = new DirectorManagerFront(client, selectedTab);
var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
// activate the director script without window reloading
testDirectorScriptClient.setup({reload: false});
yield waitForDirectorScriptAttach;
var waitForDirectorScriptError = waitForEvent(testDirectorScriptClient, "error");
testDirectorScriptClient.finalize();
var [error] = yield waitForDirectorScriptError;
assertIsDirectorScriptError(error);
client.close();
});
}
function runDirectorScriptSetupAndReceiveMessagePortTest() {
targetWin = window.open("about:blank");
var testDirectorScriptOptions = {
scriptCode: "(" + (function() {
module.exports = function({port}) {
port.onmessage = function(evt) {
// echo messages
evt.source.postMessage(evt.data);
};
};
}).toString() + ")();",
scriptOptions: {}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
yield installTestDirectorScript(client, root, "testDirectorScript",
testDirectorScriptOptions);
var selectedTab = root.tabs[root.selected];
// get a testDirectorScriptClient
var manager = new DirectorManagerFront(client, selectedTab);
var testDirectorScriptClient = yield getTestDirectorScript(manager, selectedTab, "testDirectorScript");
var waitForDirectorScriptAttach = waitForEvent(testDirectorScriptClient, "attach");
// activate the director script without window reloading
// (and wait for attach)
testDirectorScriptClient.setup({reload: false});
var [attachEvent] = yield waitForDirectorScriptAttach;
// call the connectPort method to get a MessagePortClient
var port = attachEvent.port;
ok(!!port && !!port.postMessage, "messageport actor client received");
// exchange messages over the MessagePort
var waitForMessagePortMessage = waitForEvent(port, "message");
// needs to explicit start the port
port.start();
var msg = { k1: "v1", k2: [1, 2, 3] };
port.postMessage(msg);
var reply = yield waitForMessagePortMessage;
ok(JSON.stringify(reply[0].data) === JSON.stringify(msg),
"echo reply received on the MessagePortClient");
yield client.close();
});
}
function runDirectorEnableDirectorScriptsTest() {
targetWin = window.open("about:blank");
var testDirectorScriptOptions = {
scriptCode: "(" + (function() {
module.exports = function({port}) {
port.onmessage = function(evt) {
// echo messages
evt.source.postMessage(evt.data);
};
};
}).toString() + ")();",
scriptOptions: {}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
yield installTestDirectorScript(client, root, "testDirectorScript",
testDirectorScriptOptions);
var selectedTab = root.tabs[root.selected];
var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
tabDirectorClient.enableByScriptIds(["*"], { reload: false });
var [attachEvent] = yield waitForDirectorScriptAttach;
is(attachEvent.directorScriptId, "testDirectorScript", "attach event should contains directorScriptId");
yield client.close();
});
}
function runDirectorScriptDetachEventTest() {
targetWin = window.open("director-script-target.html");
var testDirectorScriptOptions = {
scriptCode: "(" + (function() {
exports.attach = function({port, onUnload}) {
onUnload(function() {
port.postMessage("ONUNLOAD");
});
};
}).toString() + ")();",
scriptOptions: {
attachMethod: "attach"
}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
yield installTestDirectorScript(client, root, "testDirectorScript",
testDirectorScriptOptions);
var selectedTab = root.tabs[root.selected];
// NOTE: tab needs to be attached to receive director-script-detach events
yield promiseInvoke(client, client.attachTab, selectedTab.actor);
var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
var waitForDirectorScriptDetach = waitForEvent(tabDirectorClient, "director-script-detach");
tabDirectorClient.enableByScriptIds(["*"], {reload: true});
var [attachEvent] = yield waitForDirectorScriptAttach;
// exchange messages over the MessagePort
var waitForMessagePortEvent = waitForEvent(attachEvent.port, "message");
// needs to explicit start the port
attachEvent.port.start();
tabDirectorClient.disableByScriptIds(["*"], {reload: false});
// changing the window location should generate a director-script-detach event
var [detachEvent] = yield waitForDirectorScriptDetach;
is(detachEvent.directorScriptId, "testDirectorScript", "detach event should contains directorScriptId");
var [portEvent] = yield waitForMessagePortEvent;
is(portEvent.data, "ONUNLOAD", "director-script's exports.onUnload called on detach");
yield client.close();
});
}
function runDirectorScriptWindowEval() {
targetWin = window.open("http://mochi.test:8888/chrome/toolkit/devtools/server/tests/mochitest/director-script-target.html");
var testDirectorScriptOptions = {
scriptCode: "(" + (function() {
exports.attach = function({window, port}) {
var onpageloaded = function() {
var globalVarValue = window.eval("window.globalAccessibleVar;");
port.postMessage(globalVarValue);
};
if (window.document.readyState === "complete") {
onpageloaded();
} else {
window.onload = onpageloaded;
}
};
}).toString() + ")();",
scriptOptions: {
attachMethod: "attach"
}
}
return Task.spawn(function* () {
var { client, root } = yield newConnectedDebuggerClient();
yield installTestDirectorScript(client, root, "testDirectorScript",
testDirectorScriptOptions);
var selectedTab = root.tabs[root.selected];
// NOTE: tab needs to be attached to receive director-script-detach events
yield promiseInvoke(client, client.attachTab, selectedTab.actor);
var tabDirectorClient = new DirectorManagerFront(client, selectedTab);
var waitForDirectorScriptAttach = waitForEvent(tabDirectorClient, "director-script-attach");
var waitForDirectorScriptError = waitForEvent(tabDirectorClient, "director-script-error");
tabDirectorClient.enableByScriptIds(["*"], {reload: false});
var [receivedEvent] = yield Promise.race([waitForDirectorScriptAttach,
waitForDirectorScriptError]);
ok(!!receivedEvent.port, "received director-script-attach");
// exchange messages over the MessagePort
var waitForMessagePortEvent = waitForEvent(receivedEvent.port, "message");
// needs to explicit start the port
receivedEvent.port.start();
var [portEvent] = yield waitForMessagePortEvent;
ok(portEvent.data !== "unsecure-eval", "window.eval should be wrapped and safe");
is(portEvent.data, "global-value", "window.globalAccessibleVar should be accessible through window.eval");
yield client.close();
});
}
</script>
</pre>
</body>
</html>

Просмотреть файл

@ -0,0 +1,98 @@
<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=
-->
<head>
<meta charset="utf-8">
<title>Test for Bug </title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<pre id="test">
<script type="application/javascript;version=1.8" src="./director-helpers.js"></script>
<script type="application/javascript;version=1.8">
window.onload = function() {
Task.spawn(function* () {
SimpleTest.waitForExplicitFinish();
var tests = [
runPropagateDirectorScriptsToChildTest,
].map((testCase) => {
return function* () {
setup();
yield testCase().then(null, (e) => {
ok(false, "Exception during testCase run: " + [e, e.fileName, e.lineNumber].join("\n\t"));
});
teardown();
};
});
for (var test of tests) {
yield test();
}
SimpleTest.finish();
});
};
function setup() {
if (!DebuggerServer.initialized) {
DebuggerServer.init(() => true);
DebuggerServer.addBrowserActors();
SimpleTest.registerCleanupFunction(function() {
DebuggerServer.destroy();
});
}
}
function teardown() {
purgeInstalledDirectorScripts();
DebuggerServer.destroy();
}
/***********************************
* test cases
**********************************/
function runPropagateDirectorScriptsToChildTest() {
let iframe = document.createElement("iframe");
iframe.mozbrowser = true;
document.body.appendChild(iframe);
return Task.spawn(function* () {
var { client, root, transport } = yield newConnectedDebuggerClient();
var directorRegistryClient = new DirectorRegistryFront(client, root);
// install a director script
yield directorRegistryClient.install("testPropagatedDirectorScript", {
scriptCode: "console.log('director script test');",
scriptOptions: {}
});
var conn = transport._serverConnection;
var childActor = yield DebuggerServer.connectToChild(conn, iframe);
ok(typeof childActor.directorManagerActor !== "undefined",
"childActor.directorActor should be defined");
var childDirectorManagerClient = new DirectorManagerFront(client, childActor);
var directorScriptList = yield childDirectorManagerClient.list();
ok(directorScriptList.installed.length === 1 &&
directorScriptList.installed[0] === "testPropagatedDirectorScript",
"director scripts propagated correctly")
yield client.close();
});
}
</script>
</pre>
</body>
</html>