зеркало из https://github.com/mozilla/gecko-dev.git
Merge fx-team to m-c a=merge
This commit is contained in:
Коммит
e62807e2fa
|
@ -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>
|
Загрузка…
Ссылка в новой задаче