Bug 972017 Part 2 - Set up actions and a dispatcher and start to handle obtaining call data for outgoing Loop calls from the desktop client. r=mikedeboer

This commit is contained in:
Mark Banner 2014-09-30 20:44:05 +01:00
parent 631b97c2df
commit 122e4eddcd
21 changed files with 1462 additions and 30 deletions

View File

@ -1613,6 +1613,7 @@ pref("loop.retry_delay.limit", 300000);
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
pref("loop.feedback.product", "Loop");
pref("loop.debug.loglevel", "Error");
pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);

View File

@ -30,6 +30,9 @@
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
<script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>

View File

@ -15,6 +15,12 @@ loop.Client = (function($) {
// The expected properties to be returned from the GET /calls request.
var expectedCallProperties = ["calls"];
// THe expected properties to be returned from the POST /calls request.
var expectedPostCallProperties = [
"apiKey", "callId", "progressURL",
"sessionId", "sessionToken", "websocketToken"
];
/**
* Loop server client.
*
@ -209,6 +215,44 @@ loop.Client = (function($) {
}.bind(this));
},
/**
* Sets up an outgoing call, getting the relevant data from the server.
*
* Callback parameters:
* - err null on successful registration, non-null otherwise.
* - result an object of the obtained data for starting the call, if successful
*
* @param {Array} calleeIds an array of emails and phone numbers.
* @param {String} callType the type of call.
* @param {Function} cb Callback(err, result)
*/
setupOutgoingCall: function(calleeIds, callType, cb) {
this.mozLoop.hawkRequest(this.mozLoop.LOOP_SESSION_TYPE.FXA,
"/calls", "POST", {
calleeId: calleeIds,
callType: callType
},
function (err, responseText) {
if (err) {
this._failureHandler(cb, err);
return;
}
try {
var postData = JSON.parse(responseText);
var outgoingCallData = this._validate(postData,
expectedPostCallProperties);
cb(null, outgoingCallData);
} catch (err) {
console.log("Error requesting call info", err);
cb(err);
}
}.bind(this)
);
},
/**
* Adds a value to a telemetry histogram, ignoring errors.
*

View File

@ -502,14 +502,25 @@ loop.conversation = (function(mozL10n) {
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.ConversationStore).isRequired
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired
},
getInitialState: function() {
return this.props.store.attributes;
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
}, this);
},
render: function() {
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (OutgoingConversationView({
store: this.props.store}
@ -545,19 +556,19 @@ loop.conversation = (function(mozL10n) {
}
});
var conversationStore = new loop.ConversationStore();
var dispatcher = new loop.Dispatcher();
var client = new loop.Client();
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher
});
// XXX For now key this on the pref, but this should really be
// set by the information from the mozLoop API when we can get it (bug 1072323).
var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
if (outgoingEmail) {
conversationStore.set("outgoing", true);
conversationStore.set("calleeId", outgoingEmail);
}
// XXX Old class creation for the incoming conversation view, whilst
// we transition across (bug 1072323).
var client = new loop.Client();
var conversation = new sharedModels.ConversationModel(
{}, // Model attributes
{sdk: window.OT} // Model dependencies
@ -567,8 +578,10 @@ loop.conversation = (function(mozL10n) {
// Obtain the callId and pass it through
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationHash();
var callId;
if (locationHash) {
conversation.set("callId", locationHash.match(/\#incoming\/(.*)/)[1]);
callId = locationHash.match(/\#incoming\/(.*)/)[1]
conversation.set("callId", callId);
}
window.addEventListener("unload", function(event) {
@ -585,6 +598,11 @@ loop.conversation = (function(mozL10n) {
notifications: notifications,
sdk: window.OT}
), document.querySelector('#main'));
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
callId: callId,
calleeId: outgoingEmail
}));
}
return {

View File

@ -502,14 +502,25 @@ loop.conversation = (function(mozL10n) {
sdk: React.PropTypes.object.isRequired,
// XXX New types for OutgoingConversationView
store: React.PropTypes.instanceOf(loop.ConversationStore).isRequired
store: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired
},
getInitialState: function() {
return this.props.store.attributes;
},
componentWillMount: function() {
this.props.store.on("change:outgoing", function() {
this.setState(this.props.store.attributes);
}, this);
},
render: function() {
// Don't display anything, until we know what type of call we are.
if (this.state.outgoing === undefined) {
return null;
}
if (this.state.outgoing) {
return (<OutgoingConversationView
store={this.props.store}
@ -545,19 +556,19 @@ loop.conversation = (function(mozL10n) {
}
});
var conversationStore = new loop.ConversationStore();
var dispatcher = new loop.Dispatcher();
var client = new loop.Client();
var conversationStore = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher
});
// XXX For now key this on the pref, but this should really be
// set by the information from the mozLoop API when we can get it (bug 1072323).
var outgoingEmail = navigator.mozLoop.getLoopCharPref("outgoingemail");
if (outgoingEmail) {
conversationStore.set("outgoing", true);
conversationStore.set("calleeId", outgoingEmail);
}
// XXX Old class creation for the incoming conversation view, whilst
// we transition across (bug 1072323).
var client = new loop.Client();
var conversation = new sharedModels.ConversationModel(
{}, // Model attributes
{sdk: window.OT} // Model dependencies
@ -567,8 +578,10 @@ loop.conversation = (function(mozL10n) {
// Obtain the callId and pass it through
var helper = new loop.shared.utils.Helper();
var locationHash = helper.locationHash();
var callId;
if (locationHash) {
conversation.set("callId", locationHash.match(/\#incoming\/(.*)/)[1]);
callId = locationHash.match(/\#incoming\/(.*)/)[1]
conversation.set("callId", callId);
}
window.addEventListener("unload", function(event) {
@ -585,6 +598,11 @@ loop.conversation = (function(mozL10n) {
notifications={notifications}
sdk={window.OT}
/>, document.querySelector('#main'));
dispatcher.dispatch(new loop.shared.actions.GatherCallData({
callId: callId,
calleeId: outgoingEmail
}));
}
return {

View File

@ -9,6 +9,8 @@
var loop = loop || {};
loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
/**
* Displays details of the incoming/outgoing conversation
* (name, link, audio/video type etc).
@ -45,8 +47,8 @@ loop.conversationViews = (function(mozL10n) {
render: function() {
var pendingStateString;
if (this.props.callState === "ringing") {
pendingStateString = mozL10n.get("call_progress_pending_description");
if (this.props.callState === CALL_STATES.ALERTING) {
pendingStateString = mozL10n.get("call_progress_ringing_description");
} else {
pendingStateString = mozL10n.get("call_progress_connecting_description");
}
@ -69,6 +71,19 @@ loop.conversationViews = (function(mozL10n) {
}
});
/**
* Call failed view. Displayed when a call fails.
*/
var CallFailedView = React.createClass({displayName: 'CallFailedView',
render: function() {
return (
React.DOM.div({className: "call-window"},
React.DOM.h2(null, mozL10n.get("generic_failure_title"))
)
);
}
});
/**
* Master View Controller for outgoing calls. This manages
* the different views that need displaying.
@ -76,14 +91,24 @@ loop.conversationViews = (function(mozL10n) {
var OutgoingConversationView = React.createClass({displayName: 'OutgoingConversationView',
propTypes: {
store: React.PropTypes.instanceOf(
loop.ConversationStore).isRequired
loop.store.ConversationStore).isRequired
},
getInitialState: function() {
return this.props.store.attributes;
},
componentWillMount: function() {
this.props.store.on("change", function() {
this.setState(this.props.store.attributes);
}, this);
},
render: function() {
if (this.state.callState === CALL_STATES.TERMINATED) {
return (CallFailedView(null));
}
return (PendingConversationView({
callState: this.state.callState,
calleeId: this.state.calleeId}
@ -94,6 +119,7 @@ loop.conversationViews = (function(mozL10n) {
return {
PendingConversationView: PendingConversationView,
ConversationDetailView: ConversationDetailView,
CallFailedView: CallFailedView,
OutgoingConversationView: OutgoingConversationView
};

View File

@ -9,6 +9,8 @@
var loop = loop || {};
loop.conversationViews = (function(mozL10n) {
var CALL_STATES = loop.store.CALL_STATES;
/**
* Displays details of the incoming/outgoing conversation
* (name, link, audio/video type etc).
@ -45,8 +47,8 @@ loop.conversationViews = (function(mozL10n) {
render: function() {
var pendingStateString;
if (this.props.callState === "ringing") {
pendingStateString = mozL10n.get("call_progress_pending_description");
if (this.props.callState === CALL_STATES.ALERTING) {
pendingStateString = mozL10n.get("call_progress_ringing_description");
} else {
pendingStateString = mozL10n.get("call_progress_connecting_description");
}
@ -69,6 +71,19 @@ loop.conversationViews = (function(mozL10n) {
}
});
/**
* Call failed view. Displayed when a call fails.
*/
var CallFailedView = React.createClass({
render: function() {
return (
<div className="call-window">
<h2>{mozL10n.get("generic_failure_title")}</h2>
</div>
);
}
});
/**
* Master View Controller for outgoing calls. This manages
* the different views that need displaying.
@ -76,14 +91,24 @@ loop.conversationViews = (function(mozL10n) {
var OutgoingConversationView = React.createClass({
propTypes: {
store: React.PropTypes.instanceOf(
loop.ConversationStore).isRequired
loop.store.ConversationStore).isRequired
},
getInitialState: function() {
return this.props.store.attributes;
},
componentWillMount: function() {
this.props.store.on("change", function() {
this.setState(this.props.store.attributes);
}, this);
},
render: function() {
if (this.state.callState === CALL_STATES.TERMINATED) {
return (<CallFailedView />);
}
return (<PendingConversationView
callState={this.state.callState}
calleeId={this.state.calleeId}
@ -94,6 +119,7 @@ loop.conversationViews = (function(mozL10n) {
return {
PendingConversationView: PendingConversationView,
ConversationDetailView: ConversationDetailView,
CallFailedView: CallFailedView,
OutgoingConversationView: OutgoingConversationView
};

View File

@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.actions = (function() {
"use strict";
/**
* Actions are events that are triggered by the user, e.g. clicking a button,
* or by an async event, e.g. status received.
*
* They should be dispatched to stores via the dispatcher.
*/
function Action(name, schema, values) {
var validatedData = new loop.validate.Validator(schema || {})
.validate(values || {});
for (var prop in validatedData)
this[prop] = validatedData[prop];
this.name = name;
}
Action.define = function(name, schema) {
return Action.bind(null, name, schema);
};
return {
/**
* Used to trigger gathering of initial call data.
*/
GatherCallData: Action.define("gatherCallData", {
// XXX This may change when bug 1072323 is implemented.
// Optional: Specify the calleeId for an outgoing call
calleeId: [String, null],
// Specify the callId for an incoming call.
callId: [String, null]
}),
/**
* Used to cancel call setup.
*/
CancelCall: Action.define("cancelCall", {
}),
/**
* Used to initiate connecting of a call with the relevant
* sessionData.
*/
ConnectCall: Action.define("connectCall", {
// This object contains the necessary details for the
// connection of the websocket, and the SDK
sessionData: Object
}),
/**
* Used for notifying of connection progress state changes.
* The connection refers to the overall connection flow as indicated
* on the websocket.
*/
ConnectionProgress: Action.define("connectionProgress", {
// The new connection state
state: String
}),
/**
* Used for notifying of connection failures.
*/
ConnectionFailure: Action.define("connectionFailure", {
// A string relating to the reason the connection failed.
reason: String
})
};
})();

View File

@ -5,15 +5,240 @@
/* global loop:true */
var loop = loop || {};
loop.ConversationStore = (function() {
loop.store = (function() {
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var CALL_STATES = {
// The initial state of the view.
INIT: "init",
// The store is gathering the call data from the server.
GATHER: "gather",
// The websocket has connected to the server and is waiting
// for the other peer to connect to the websocket.
CONNECTING: "connecting",
// The websocket has received information that we're now alerting
// the peer.
ALERTING: "alerting",
// The call was terminated due to an issue during connection.
TERMINATED: "terminated"
};
var ConversationStore = Backbone.Model.extend({
defaults: {
outgoing: false,
// The current state of the call
callState: CALL_STATES.INIT,
// The reason if a call was terminated
callStateReason: undefined,
// The error information, if there was a failure
error: undefined,
// True if the call is outgoing, false if not, undefined if unknown
outgoing: undefined,
// The id of the person being called for outgoing calls
calleeId: undefined,
callState: "gather"
// The call type for the call.
// XXX Don't hard-code, this comes from the data in bug 1072323
callType: sharedUtils.CALL_TYPES.AUDIO_VIDEO,
// Call Connection information
// The call id from the loop-server
callId: undefined,
// The connection progress url to connect the websocket
progressURL: undefined,
// The websocket token that allows connection to the progress url
websocketToken: undefined,
// SDK API key
apiKey: undefined,
// SDK session ID
sessionId: undefined,
// SDK session token
sessionToken: undefined
},
/**
* Constructor
*
* Options:
* - {loop.Dispatcher} dispatcher The dispatcher for dispatching actions and
* registering to consume actions.
* - {Object} client A client object for communicating with the server.
*
* @param {Object} attributes Attributes object.
* @param {Object} options Options object.
*/
initialize: function(attributes, options) {
options = options || {};
if (!options.dispatcher) {
throw new Error("Missing option dispatcher");
}
if (!options.client) {
throw new Error("Missing option client");
}
this.client = options.client;
this.dispatcher = options.dispatcher;
this.dispatcher.register(this, [
"connectionFailure",
"connectionProgress",
"gatherCallData",
"connectCall"
]);
},
/**
* Handles the connection failure action, setting the state to
* terminated.
*
* @param {sharedActions.ConnectionFailure} actionData The action data.
*/
connectionFailure: function(actionData) {
this.set({
callState: CALL_STATES.TERMINATED,
callStateReason: actionData.reason
});
},
/**
* Handles the connection progress action, setting the next state
* appropriately.
*
* @param {sharedActions.ConnectionProgress} actionData The action data.
*/
connectionProgress: function(actionData) {
// XXX Turn this into a state machine?
if (actionData.state === "alerting" &&
(this.get("callState") === CALL_STATES.CONNECTING ||
this.get("callState") === CALL_STATES.GATHER)) {
this.set({
callState: CALL_STATES.ALERTING
});
}
if (actionData.state === "connecting" &&
this.get("callState") === CALL_STATES.GATHER) {
this.set({
callState: CALL_STATES.CONNECTING
});
}
},
/**
* Handles the gather call data action, setting the state
* and starting to get the appropriate data for the type of call.
*
* @param {sharedActions.GatherCallData} actionData The action data.
*/
gatherCallData: function(actionData) {
this.set({
calleeId: actionData.calleeId,
outgoing: !!actionData.calleeId,
callId: actionData.callId,
callState: CALL_STATES.GATHER
});
if (this.get("outgoing")) {
this._setupOutgoingCall();
} // XXX Else, other types aren't supported yet.
},
/**
* Handles the connect call action, this saves the appropriate
* data and starts the connection for the websocket to notify the
* server of progress.
*
* @param {sharedActions.ConnectCall} actionData The action data.
*/
connectCall: function(actionData) {
this.set(actionData.sessionData);
this._connectWebSocket();
},
/**
* Obtains the outgoing call data from the server and handles the
* result.
*/
_setupOutgoingCall: function() {
// XXX For now, we only have one calleeId, so just wrap that in an array.
this.client.setupOutgoingCall([this.get("calleeId")],
this.get("callType"),
function(err, result) {
if (err) {
console.error("Failed to get outgoing call data", err);
this.dispatcher.dispatch(
new sharedActions.ConnectionFailure({reason: "setup"}));
return;
}
// Success, dispatch a new action.
this.dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: result}));
}.bind(this)
);
},
/**
* Sets up and connects the websocket to the server. The websocket
* deals with sending and obtaining status via the server about the
* setup of the call.
*/
_connectWebSocket: function() {
this._websocket = new loop.CallConnectionWebSocket({
url: this.get("progressURL"),
callId: this.get("callId"),
websocketToken: this.get("websocketToken")
});
this._websocket.promiseConnect().then(
function() {
this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
// This is the websocket call state, i.e. waiting for the
// other end to connect to the server.
state: "connecting"
}));
}.bind(this),
function(error) {
console.error("Websocket failed to connect", error);
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: "websocket-setup"
}));
}.bind(this)
);
this._websocket.on("progress", this._handleWebSocketProgress, this);
},
/**
* Used to handle any progressed received from the websocket. This will
* dispatch new actions so that the data can be handled appropriately.
*/
_handleWebSocketProgress: function(progressData) {
var action;
switch(progressData.state) {
case "terminated":
action = new sharedActions.ConnectionFailure({
reason: progressData.reason
});
break;
case "alerting":
action = new sharedActions.ConnectionProgress({
state: progressData.state
});
break;
default:
console.warn("Received unexpected state in _handleWebSocketProgress", progressData.state);
return;
}
this.dispatcher.dispatch(action);
}
});
return ConversationStore;
return {
CALL_STATES: CALL_STATES,
ConversationStore: ConversationStore
};
})();

View File

@ -0,0 +1,84 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true */
/**
* The dispatcher for actions. This dispatches actions to stores registered
* for those actions.
*
* If stores need to perform async operations for actions, they should return
* straight away, and set up a new action for the changes if necessary.
*
* It is an error if a returned promise rejects - they should always pass.
*/
var loop = loop || {};
loop.Dispatcher = (function() {
function Dispatcher() {
this._eventData = {};
this._actionQueue = [];
this._debug = loop.shared.utils.getBoolPreference("debug.dispatcher");
}
Dispatcher.prototype = {
/**
* Register a store to receive notifications of specific actions.
*
* @param {Object} store The store object to register
* @param {Array} eventTypes An array of action names
*/
register: function(store, eventTypes) {
eventTypes.forEach(function(type) {
if (this._eventData.hasOwnProperty(type)) {
this._eventData[type].push(store);
} else {
this._eventData[type] = [store];
}
}.bind(this));
},
/**
* Dispatches an action to all registered stores.
*/
dispatch: function(action) {
// Always put it on the queue, to make it simpler.
this._actionQueue.push(action);
this._dispatchNextAction();
},
/**
* Dispatches the next action in the queue if one is not already active.
*/
_dispatchNextAction: function() {
if (!this._actionQueue.length || this._active) {
return;
}
var action = this._actionQueue.shift();
var type = action.name;
var registeredStores = this._eventData[type];
if (!registeredStores) {
console.warn("No stores registered for event type ", type);
return;
}
this._active = true;
if (this._debug) {
console.log("[Dispatcher] Dispatching action", action);
}
registeredStores.forEach(function(store) {
store[type](action);
});
this._active = false;
this._dispatchNextAction();
}
};
return Dispatcher;
})();

View File

@ -9,6 +9,14 @@ loop.shared = loop.shared || {};
loop.shared.utils = (function() {
"use strict";
/**
* Call types used for determining if a call is audio/video or audio-only.
*/
var CALL_TYPES = {
AUDIO_VIDEO: "audio-video",
AUDIO_ONLY: "audio"
};
/**
* Used for adding different styles to the panel
* @returns {String} Corresponds to the client platform
@ -77,6 +85,7 @@ loop.shared.utils = (function() {
};
return {
CALL_TYPES: CALL_TYPES,
Helper: Helper,
getTargetPlatform: getTargetPlatform,
getBoolPreference: getBoolPreference

View File

@ -0,0 +1,127 @@
/* 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/. */
/* jshint unused:false */
var loop = loop || {};
loop.validate = (function() {
"use strict";
/**
* Computes the difference between two arrays.
*
* @param {Array} arr1 First array
* @param {Array} arr2 Second array
* @return {Array} Array difference
*/
function difference(arr1, arr2) {
return arr1.filter(function(item) {
return arr2.indexOf(item) === -1;
});
}
/**
* Retrieves the type name of an object or constructor. Fallback to "unknown"
* when it fails.
*
* @param {Object} obj
* @return {String}
*/
function typeName(obj) {
if (obj === null)
return "null";
if (typeof obj === "function")
return obj.name || obj.toString().match(/^function\s?([^\s(]*)/)[1];
if (typeof obj.constructor === "function")
return typeName(obj.constructor);
return "unknown";
}
/**
* Simple typed values validator.
*
* @constructor
* @param {Object} schema Validation schema
*/
function Validator(schema) {
this.schema = schema || {};
}
Validator.prototype = {
/**
* Validates all passed values against declared dependencies.
*
* @param {Object} values The values object
* @return {Object} The validated values object
* @throws {TypeError} If validation fails
*/
validate: function(values) {
this._checkRequiredProperties(values);
this._checkRequiredTypes(values);
return values;
},
/**
* Checks if any of Object values matches any of current dependency type
* requirements.
*
* @param {Object} values The values object
* @throws {TypeError}
*/
_checkRequiredTypes: function(values) {
Object.keys(this.schema).forEach(function(name) {
var types = this.schema[name];
types = Array.isArray(types) ? types : [types];
if (!this._dependencyMatchTypes(values[name], types)) {
throw new TypeError("invalid dependency: " + name +
"; expected " + types.map(typeName).join(", ") +
", got " + typeName(values[name]));
}
}, this);
},
/**
* Checks if a values object owns the required keys defined in dependencies.
* Values attached to these properties shouldn't be null nor undefined.
*
* @param {Object} values The values object
* @throws {TypeError} If any dependency is missing.
*/
_checkRequiredProperties: function(values) {
var definedProperties = Object.keys(values).filter(function(name) {
return typeof values[name] !== "undefined";
});
var diff = difference(Object.keys(this.schema), definedProperties);
if (diff.length > 0)
throw new TypeError("missing required " + diff.join(", "));
},
/**
* Checks if a given value matches any of the provided type requirements.
*
* @param {Object} value The value to check
* @param {Array} types The list of types to check the value against
* @return {Boolean}
* @throws {TypeError} If the value doesn't match any types.
*/
_dependencyMatchTypes: function(value, types) {
return types.some(function(Type) {
/*jshint eqeqeq:false*/
try {
return typeof Type === "undefined" || // skip checking
Type === null && value === null || // null type
value.constructor == Type || // native type
Type.prototype.isPrototypeOf(value) || // custom type
typeName(value) === typeName(Type); // type string eq.
} catch (e) {
return false;
}
});
}
};
return {
Validator: Validator
};
})();

View File

@ -53,13 +53,16 @@ browser.jar:
content/browser/loop/shared/img/icons-16x16.svg (content/shared/img/icons-16x16.svg)
# Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
# Shared libs
#ifdef DEBUG

View File

@ -253,5 +253,70 @@ describe("loop.Client", function() {
});
});
});
describe("#setupOutgoingCall", function() {
var calleeIds, callType;
beforeEach(function() {
calleeIds = [
"fakeemail", "fake phone"
];
callType = "audio";
});
it("should make a POST call to /calls", function() {
client.setupOutgoingCall(calleeIds, callType);
sinon.assert.calledOnce(hawkRequestStub);
sinon.assert.calledWith(hawkRequestStub,
mozLoop.LOOP_SESSION_TYPE.FXA,
"/calls",
"POST",
{ calleeId: calleeIds, callType: callType }
);
});
it("should call the callback if the request is successful", function() {
var requestData = {
apiKey: "fake",
callId: "fakeCall",
progressURL: "fakeurl",
sessionId: "12345678",
sessionToken: "15263748",
websocketToken: "13572468"
};
hawkRequestStub.callsArgWith(4, null, JSON.stringify(requestData));
client.setupOutgoingCall(calleeIds, callType, callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithExactly(callback, null, requestData);
});
it("should send an error when the request fails", function() {
hawkRequestStub.callsArgWith(4, fakeErrorRes);
client.setupOutgoingCall(calleeIds, callType, callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithExactly(callback, sinon.match(function(err) {
return /400.*invalid token/.test(err.message);
}));
});
it("should send an error if the data is not valid", function() {
// Sets up the hawkRequest stub to trigger the callback with
// an error
hawkRequestStub.callsArgWith(4, null, "{}");
client.setupOutgoingCall(calleeIds, callType, callback);
sinon.assert.calledOnce(callback);
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /Invalid data received/.test(err.message);
}));
});
});
});
});

View File

@ -0,0 +1,131 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.conversationViews", function () {
var sandbox, oldTitle, view;
var CALL_STATES = loop.store.CALL_STATES;
beforeEach(function() {
sandbox = sinon.sandbox.create();
oldTitle = document.title;
sandbox.stub(document.mozL10n, "get", function(x) {
return x;
});
});
afterEach(function() {
document.title = oldTitle;
view = undefined;
sandbox.restore();
});
describe("ConversationDetailView", function() {
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
loop.conversationViews.ConversationDetailView(props));
}
it("should set the document title to the calledId", function() {
mountTestComponent({calleeId: "mrsmith"});
expect(document.title).eql("mrsmith");
});
it("should set display the calledId", function() {
view = mountTestComponent({calleeId: "mrsmith"});
expect(TestUtils.findRenderedDOMComponentWithTag(
view, "h2").props.children).eql("mrsmith");
});
});
describe("PendingConversationView", function() {
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
loop.conversationViews.PendingConversationView(props));
}
it("should set display connecting string when the state is not alerting",
function() {
view = mountTestComponent({
callState: CALL_STATES.CONNECTING,
calleeId: "mrsmith"
});
var label = TestUtils.findRenderedDOMComponentWithClass(
view, "btn-label").props.children;
expect(label).to.have.string("connecting");
});
it("should set display ringing string when the state is alerting",
function() {
view = mountTestComponent({
callState: CALL_STATES.ALERTING,
calleeId: "mrsmith"
});
var label = TestUtils.findRenderedDOMComponentWithClass(
view, "btn-label").props.children;
expect(label).to.have.string("ringing");
});
});
describe("OutgoingConversationView", function() {
var store;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
loop.conversationViews.OutgoingConversationView({
store: store
}));
}
beforeEach(function() {
store = new loop.store.ConversationStore({}, {
dispatcher: new loop.Dispatcher(),
client: {}
});
});
it("should render the CallFailedView when the call state is 'terminated'",
function() {
store.set({callState: CALL_STATES.TERMINATED});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.CallFailedView);
});
it("should render the PendingConversationView when the call state is connecting",
function() {
store.set({callState: CALL_STATES.CONNECTING});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.PendingConversationView);
});
it("should update the rendered views when the state is changed.",
function() {
store.set({callState: CALL_STATES.CONNECTING});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.PendingConversationView);
store.set({callState: CALL_STATES.TERMINATED});
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.CallFailedView);
});
});
});

View File

@ -41,7 +41,7 @@ describe("loop.conversation", function() {
return "en-US";
},
setLoopCharPref: sinon.stub(),
getLoopCharPref: sinon.stub(),
getLoopCharPref: sinon.stub().returns(null),
getLoopBoolPref: sinon.stub(),
getCallData: sinon.stub(),
releaseCallData: sinon.stub(),
@ -75,6 +75,11 @@ describe("loop.conversation", function() {
sandbox.stub(loop.shared.models.ConversationModel.prototype,
"initialize");
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
sandbox.stub(loop.shared.utils.Helper.prototype,
"locationHash").returns("#incoming/42");
window.OT = {
overrideGuidStorage: sinon.stub()
};
@ -102,10 +107,21 @@ describe("loop.conversation", function() {
loop.conversation.ConversationControllerView);
}));
});
it("should trigger a gatherCallData action", function() {
loop.conversation.init();
sinon.assert.calledOnce(loop.Dispatcher.prototype.dispatch);
sinon.assert.calledWithExactly(loop.Dispatcher.prototype.dispatch,
new loop.shared.actions.GatherCallData({
calleeId: null,
callId: "42"
}));
});
});
describe("ConversationControllerView", function() {
var store, conversation, client, ccView, oldTitle;
var store, conversation, client, ccView, oldTitle, dispatcher;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
@ -124,7 +140,11 @@ describe("loop.conversation", function() {
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {}
});
store = new loop.ConversationStore();
dispatcher = new loop.Dispatcher();
store = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher
});
});
afterEach(function() {

View File

@ -39,6 +39,9 @@
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/js/conversationViews.js"></script>
<script src="../../content/js/conversation.js"></script>
@ -49,6 +52,7 @@
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
<script src="panel_test.js"></script>
<script src="conversationViews_test.js"></script>
<script>
// Stop the default init functions running to avoid conflicts in tests
document.removeEventListener('DOMContentLoaded', loop.panel.init);

View File

@ -0,0 +1,321 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.ConversationStore", function () {
"use strict";
var CALL_STATES = loop.store.CALL_STATES;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sandbox, dispatcher, client, store, fakeSessionData;
var connectPromise, resolveConnectPromise, rejectConnectPromise;
function checkFailures(done, f) {
try {
f();
done();
} catch (err) {
done(err);
}
}
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
client = {
setupOutgoingCall: sinon.stub()
};
store = new loop.store.ConversationStore({}, {
client: client,
dispatcher: dispatcher
});
fakeSessionData = {
apiKey: "fakeKey",
callId: "142536",
sessionId: "321456",
sessionToken: "341256",
websocketToken: "543216",
progressURL: "fakeURL"
};
var dummySocket = {
close: sinon.spy(),
send: sinon.spy()
};
connectPromise = new Promise(function(resolve, reject) {
resolveConnectPromise = resolve;
rejectConnectPromise = reject;
});
sandbox.stub(loop.CallConnectionWebSocket.prototype,
"promiseConnect").returns(connectPromise);
});
afterEach(function() {
sandbox.restore();
});
describe("#initialize", function() {
it("should throw an error if the dispatcher is missing", function() {
expect(function() {
new loop.store.ConversationStore({}, {client: client});
}).to.Throw(/dispatcher/);
});
it("should throw an error if the client is missing", function() {
expect(function() {
new loop.store.ConversationStore({}, {dispatcher: dispatcher});
}).to.Throw(/client/);
});
});
describe("#connectionFailure", function() {
it("should set the state to 'terminated'", function() {
store.set({callState: CALL_STATES.ALERTING});
dispatcher.dispatch(
new sharedActions.ConnectionFailure({reason: "fake"}));
expect(store.get("callState")).eql(CALL_STATES.TERMINATED);
expect(store.get("callStateReason")).eql("fake");
});
});
describe("#connectionProgress", function() {
describe("progress: connecting", function() {
it("should change the state from 'gather' to 'connecting'", function() {
store.set({callState: CALL_STATES.GATHER});
dispatcher.dispatch(
new sharedActions.ConnectionProgress({state: "connecting"}));
expect(store.get("callState")).eql(CALL_STATES.CONNECTING);
});
});
describe("progress: alerting", function() {
it("should set the state from 'gather' to 'alerting'", function() {
store.set({callState: CALL_STATES.GATHER});
dispatcher.dispatch(
new sharedActions.ConnectionProgress({state: "alerting"}));
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
});
it("should set the state from 'connecting' to 'alerting'", function() {
store.set({callState: CALL_STATES.CONNECTING});
dispatcher.dispatch(
new sharedActions.ConnectionProgress({state: "alerting"}));
expect(store.get("callState")).eql(CALL_STATES.ALERTING);
});
});
});
describe("#gatherCallData", function() {
beforeEach(function() {
store.set({callState: CALL_STATES.INIT});
});
it("should set the state to 'gather'", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
calleeId: "",
callId: "76543218"
}));
expect(store.get("callState")).eql(CALL_STATES.GATHER);
});
it("should save the basic call information", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData({
calleeId: "fake",
callId: "123456"
}));
expect(store.get("calleeId")).eql("fake");
expect(store.get("callId")).eql("123456");
expect(store.get("outgoing")).eql(true);
});
describe("outgoing calls", function() {
var outgoingCallData;
beforeEach(function() {
outgoingCallData = {
calleeId: "fake",
callId: "135246"
};
});
it("should request the outgoing call data", function() {
dispatcher.dispatch(
new sharedActions.GatherCallData(outgoingCallData));
sinon.assert.calledOnce(client.setupOutgoingCall);
sinon.assert.calledWith(client.setupOutgoingCall,
["fake"], sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
describe("server response handling", function() {
beforeEach(function() {
sandbox.stub(dispatcher, "dispatch");
});
it("should dispatch a connect call action on success", function() {
var callData = {
apiKey: "fakeKey"
};
client.setupOutgoingCall.callsArgWith(2, null, callData);
store.gatherCallData(
new sharedActions.GatherCallData(outgoingCallData));
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "connectCall"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("sessionData", callData));
});
it("should dispatch a connection failure action on failure", function() {
client.setupOutgoingCall.callsArgWith(2, {});
store.gatherCallData(
new sharedActions.GatherCallData(outgoingCallData));
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "connectionFailure"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("reason", "setup"));
});
});
});
});
describe("#connectCall", function() {
it("should save the call session data", function() {
dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
expect(store.get("apiKey")).eql("fakeKey");
expect(store.get("callId")).eql("142536");
expect(store.get("sessionId")).eql("321456");
expect(store.get("sessionToken")).eql("341256");
expect(store.get("websocketToken")).eql("543216");
expect(store.get("progressURL")).eql("fakeURL");
});
it("should initialize the websocket", function() {
sandbox.stub(loop, "CallConnectionWebSocket").returns({
promiseConnect: function() { return connectPromise; },
on: sinon.spy()
});
dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
url: "fakeURL",
callId: "142536",
websocketToken: "543216"
});
});
it("should connect the websocket to the server", function() {
dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sinon.assert.calledOnce(store._websocket.promiseConnect);
});
describe("WebSocket connection result", function() {
beforeEach(function() {
dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sandbox.stub(dispatcher, "dispatch");
});
it("should dispatch a connection progress action on success", function(done) {
resolveConnectPromise();
connectPromise.then(function() {
checkFailures(done, function() {
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "connectionProgress"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("state", "connecting"));
});
}, function() {
done(new Error("Promise should have been resolve, not rejected"));
});
});
it("should dispatch a connection failure action on failure", function(done) {
rejectConnectPromise();
connectPromise.then(function() {
done(new Error("Promise should have been rejected, not resolved"));
}, function() {
checkFailures(done, function() {
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "connectionFailure"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("reason", "websocket-setup"));
});
});
});
});
});
describe("Events", function() {
describe("Websocket progress", function() {
beforeEach(function() {
dispatcher.dispatch(
new sharedActions.ConnectCall({sessionData: fakeSessionData}));
sandbox.stub(dispatcher, "dispatch");
});
it("should dispatch a connection failure action on 'terminate'", function() {
store._websocket.trigger("progress", {state: "terminated", reason: "reject"});
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "connectionFailure"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("reason", "reject"));
});
it("should dispatch a connection progress action on 'alerting'", function() {
store._websocket.trigger("progress", {state: "alerting"});
sinon.assert.calledOnce(dispatcher.dispatch);
// Can't use instanceof here, as that matches any action
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "connectionProgress"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("state", "alerting"));
});
});
});
});

View File

@ -0,0 +1,140 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
var expect = chai.expect;
describe("loop.Dispatcher", function () {
"use strict";
var sharedActions = loop.shared.actions;
var dispatcher, sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
});
afterEach(function() {
sandbox.restore();
});
describe("#register", function() {
it("should register a store against an action name", function() {
var object = { fake: true };
dispatcher.register(object, ["gatherCallData"]);
expect(dispatcher._eventData["gatherCallData"][0]).eql(object);
});
it("should register multiple store against an action name", function() {
var object1 = { fake: true };
var object2 = { fake2: true };
dispatcher.register(object1, ["gatherCallData"]);
dispatcher.register(object2, ["gatherCallData"]);
expect(dispatcher._eventData["gatherCallData"][0]).eql(object1);
expect(dispatcher._eventData["gatherCallData"][1]).eql(object2);
});
});
describe("#dispatch", function() {
var gatherStore1, gatherStore2, cancelStore1, connectStore1;
var gatherAction, cancelAction, connectAction, resolveCancelStore1;
beforeEach(function() {
gatherAction = new sharedActions.GatherCallData({
callId: "42",
calleeId: null
});
cancelAction = new sharedActions.CancelCall();
connectAction = new sharedActions.ConnectCall({
sessionData: {}
});
gatherStore1 = {
gatherCallData: sinon.stub()
};
gatherStore2 = {
gatherCallData: sinon.stub()
};
cancelStore1 = {
cancelCall: sinon.stub()
};
connectStore1 = {
connectCall: function() {}
};
dispatcher.register(gatherStore1, ["gatherCallData"]);
dispatcher.register(gatherStore2, ["gatherCallData"]);
dispatcher.register(cancelStore1, ["cancelCall"]);
dispatcher.register(connectStore1, ["connectCall"]);
});
it("should dispatch an action to the required object", function() {
dispatcher.dispatch(cancelAction);
sinon.assert.notCalled(gatherStore1.gatherCallData);
sinon.assert.calledOnce(cancelStore1.cancelCall);
sinon.assert.calledWithExactly(cancelStore1.cancelCall, cancelAction);
sinon.assert.notCalled(gatherStore2.gatherCallData);
});
it("should dispatch actions to multiple objects", function() {
dispatcher.dispatch(gatherAction);
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledWithExactly(gatherStore1.gatherCallData, gatherAction);
sinon.assert.notCalled(cancelStore1.cancelCall);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
sinon.assert.calledWithExactly(gatherStore2.gatherCallData, gatherAction);
});
it("should dispatch multiple actions", function() {
dispatcher.dispatch(cancelAction);
dispatcher.dispatch(gatherAction);
sinon.assert.calledOnce(cancelStore1.cancelCall);
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
});
describe("Queued actions", function() {
beforeEach(function() {
// Restore the stub, so that we can easily add a function to be
// returned. Unfortunately, sinon doesn't make this easy.
sandbox.stub(connectStore1, "connectCall", function() {
dispatcher.dispatch(gatherAction);
sinon.assert.notCalled(gatherStore1.gatherCallData);
sinon.assert.notCalled(gatherStore2.gatherCallData);
});
});
it("should not dispatch an action if the previous action hasn't finished", function() {
// Dispatch the first action. The action handler dispatches the second
// action - see the beforeEach above.
dispatcher.dispatch(connectAction);
sinon.assert.calledOnce(connectStore1.connectCall);
});
it("should dispatch an action when the previous action finishes", function() {
// Dispatch the first action. The action handler dispatches the second
// action - see the beforeEach above.
dispatcher.dispatch(connectAction);
sinon.assert.calledOnce(connectStore1.connectCall);
// These should be called, because the dispatcher synchronously queues actions.
sinon.assert.calledOnce(gatherStore1.gatherCallData);
sinon.assert.calledOnce(gatherStore2.gatherCallData);
});
});
});
});

View File

@ -39,6 +39,10 @@
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
@ -47,6 +51,9 @@
<script src="views_test.js"></script>
<script src="websocket_test.js"></script>
<script src="feedbackApiClient_test.js"></script>
<script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script>
<script src="conversationStore_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");

View File

@ -0,0 +1,82 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
/*global chai, validate */
var expect = chai.expect;
describe("Validator", function() {
"use strict";
// test helpers
function create(dependencies, values) {
var validator = new loop.validate.Validator(dependencies);
return validator.validate.bind(validator, values);
}
// test types
function X(){}
function Y(){}
describe("#validate", function() {
it("should check for a single required dependency when no option passed",
function() {
expect(create({x: Number}, {}))
.to.Throw(TypeError, /missing required x$/);
});
it("should check for a missing required dependency, undefined passed",
function() {
expect(create({x: Number}, {x: undefined}))
.to.Throw(TypeError, /missing required x$/);
});
it("should check for multiple missing required dependencies", function() {
expect(create({x: Number, y: String}, {}))
.to.Throw(TypeError, /missing required x, y$/);
});
it("should check for required dependency types", function() {
expect(create({x: Number}, {x: "woops"})).to.Throw(
TypeError, /invalid dependency: x; expected Number, got String$/);
});
it("should check for a dependency to match at least one of passed types",
function() {
expect(create({x: [X, Y]}, {x: 42})).to.Throw(
TypeError, /invalid dependency: x; expected X, Y, got Number$/);
expect(create({x: [X, Y]}, {x: new Y()})).to.not.Throw();
});
it("should skip type check if required dependency type is undefined",
function() {
expect(create({x: undefined}, {x: /whatever/})).not.to.Throw();
});
it("should check for a String dependency", function() {
expect(create({foo: String}, {foo: 42})).to.Throw(
TypeError, /invalid dependency: foo/);
});
it("should check for a Number dependency", function() {
expect(create({foo: Number}, {foo: "x"})).to.Throw(
TypeError, /invalid dependency: foo/);
});
it("should check for a custom constructor dependency", function() {
expect(create({foo: X}, {foo: null})).to.Throw(
TypeError, /invalid dependency: foo; expected X, got null$/);
});
it("should check for a native constructor dependency", function() {
expect(create({foo: mozRTCSessionDescription}, {foo: "x"}))
.to.Throw(TypeError,
/invalid dependency: foo; expected mozRTCSessionDescription/);
});
it("should check for a null dependency", function() {
expect(create({foo: null}, {foo: "x"})).to.Throw(
TypeError, /invalid dependency: foo; expected null, got String$/);
});
});
});