Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
parent
93e3a48004
commit
b38dea0597
25901 changed files with 9982925 additions and 369 deletions
123
gaia/apps/sms/services/js/activity/activity_client.js
Executable file
123
gaia/apps/sms/services/js/activity/activity_client.js
Executable file
|
|
@ -0,0 +1,123 @@
|
|||
/* global bridge,
|
||||
BroadcastChannel,
|
||||
EventDispatcher
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for managing activities.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'activity-service';
|
||||
|
||||
/**
|
||||
* Reference to active bridge client instance. For every app instance that is
|
||||
* run as inline activity we have only one bridge service (hosted in the main
|
||||
* window context) and only one bridge client (hosted in the same context).
|
||||
* Client should only communicate with the service that holds reference to
|
||||
* the corresponding activity request. Since we can potentially have more than
|
||||
* one inline activity instance we should use unique bridge service name for
|
||||
* every app instance to avoid message collision.
|
||||
*
|
||||
* @type {Client}
|
||||
*/
|
||||
var client;
|
||||
|
||||
/**
|
||||
* Indicates if there is pending activity request.
|
||||
* @type {boolean}
|
||||
*/
|
||||
var hasPendingRequest;
|
||||
|
||||
/**
|
||||
* ActivityClient is a wrapper around bridge client connected to the bridge
|
||||
* service hosted in the context that can handle system messages. It allows
|
||||
* to access "activity" request coming from system message when consumer can't
|
||||
* receive it directly (document in a different URL or even worker).
|
||||
* @type {Object}
|
||||
*/
|
||||
var ActivityClient = {
|
||||
/**
|
||||
* Initialized activity bridge client.
|
||||
* @param {string} appInstanceId Unique identifier of app instance where
|
||||
* client resides in.
|
||||
*/
|
||||
init(appInstanceId) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
client = bridge.client({
|
||||
service: SERVICE_NAME,
|
||||
endpoint: new BroadcastChannel(
|
||||
`${SERVICE_NAME}-channel-${appInstanceId}`
|
||||
),
|
||||
// Disable service response timeouts.
|
||||
timeout: false
|
||||
});
|
||||
hasPendingRequest = false;
|
||||
|
||||
client.on('activity-request', (request) => {
|
||||
// This case should never happen since we use "inline" activities only.
|
||||
if (hasPendingRequest) {
|
||||
this.postError('Concurrent activity requests are not allowed');
|
||||
return;
|
||||
}
|
||||
|
||||
hasPendingRequest = true;
|
||||
this.emit(request.name + '-activity-request', request.data);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts results to the activity caller.
|
||||
* @param {*?} result
|
||||
* @returns {Promise} Promise that resolves once operation is completed.
|
||||
*/
|
||||
postResult(result) {
|
||||
if (!hasPendingRequest) {
|
||||
return Promise.reject(
|
||||
new Error('There is no any activity request to post result to!')
|
||||
);
|
||||
}
|
||||
|
||||
hasPendingRequest = false;
|
||||
return client.method('postResult', result);
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies activity caller that exception occurred.
|
||||
* @param {*?} error
|
||||
* @returns {Promise} Promise that resolves once operation is completed.
|
||||
*/
|
||||
postError(error) {
|
||||
if (!hasPendingRequest) {
|
||||
return Promise.reject(
|
||||
new Error('There is no any activity request to post error to!')
|
||||
);
|
||||
}
|
||||
|
||||
hasPendingRequest = false;
|
||||
|
||||
console.error('Activity request was rejected with "%s" reason', error);
|
||||
|
||||
return client.method('postError', error);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if client has pending activity request.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasPendingRequest() {
|
||||
return hasPendingRequest;
|
||||
}
|
||||
};
|
||||
|
||||
exports.ActivityClient = Object.freeze(
|
||||
EventDispatcher.mixin(
|
||||
ActivityClient, ['new-activity-request', 'share-activity-request']
|
||||
)
|
||||
);
|
||||
})(window);
|
||||
144
gaia/apps/sms/services/js/activity/activity_shim.js
Executable file
144
gaia/apps/sms/services/js/activity/activity_shim.js
Executable file
|
|
@ -0,0 +1,144 @@
|
|||
/* global BridgeServiceMixin,
|
||||
BroadcastChannel
|
||||
*/
|
||||
/* exported ActivityShim */
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
const priv = Object.freeze({
|
||||
request: Symbol('request'),
|
||||
setMessageHandler: Symbol('setMessageHandler'),
|
||||
|
||||
onActivityRequest: Symbol('onActivityRequest')
|
||||
});
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for managing activities.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'activity-service';
|
||||
|
||||
/**
|
||||
* Name of the system message that is leveraged by activities.
|
||||
* @type {string}
|
||||
*/
|
||||
const SYSTEM_MESSAGE_NAME = 'activity';
|
||||
|
||||
/**
|
||||
* List of methods exposed by shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['postResult', 'postError']);
|
||||
|
||||
/**
|
||||
* List of event that can be broadcasted by shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const EVENTS = Object.freeze(['activity-request']);
|
||||
|
||||
/**
|
||||
* ActivityShim is a shim around "activity" system message handling code that
|
||||
* allows to access activity request data by the consumer that can't handle
|
||||
* system messages directly (document hosted in a different URl or worker) via
|
||||
* exposing bridge service with required methods.
|
||||
* @type {Object}
|
||||
*/
|
||||
var ActivityShim = {
|
||||
/**
|
||||
* Reference to currently active activity request.
|
||||
* @type {ActivityRequestHandler}
|
||||
* @private
|
||||
*/
|
||||
[priv.request]: null,
|
||||
|
||||
/**
|
||||
* Reference to mozSetMessageHandler function.
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
[priv.setMessageHandler]: null,
|
||||
|
||||
/**
|
||||
* Initialized activity shim bridge service.
|
||||
* @param {string} appInstanceId Unique identifier of app instance where
|
||||
* shim resides in.
|
||||
*/
|
||||
init(appInstanceId, setMessageHandler) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
this[priv.setMessageHandler] = setMessageHandler;
|
||||
|
||||
// We may consider using exports.parent as endpoint instead in patch for
|
||||
// bug 1223363, once it is unblocked.
|
||||
this.initService(
|
||||
new BroadcastChannel(`${SERVICE_NAME}-channel-${appInstanceId}`)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method is called by bridge when a new client is connected to the service.
|
||||
* @private
|
||||
*/
|
||||
onConnected() {
|
||||
// We subscribe for the system message only when we have client connected,
|
||||
// to avoid the case when service broadcasts message, but no one listens
|
||||
// for it yet.
|
||||
this[priv.setMessageHandler](
|
||||
SYSTEM_MESSAGE_NAME, this[priv.onActivityRequest].bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts activity request result.
|
||||
* @param {*} result Data to post as activity request result.
|
||||
* @private
|
||||
*/
|
||||
postResult(result) {
|
||||
var request = this[priv.request];
|
||||
if (!request) {
|
||||
throw new Error('There is no any activity request to post result to!');
|
||||
}
|
||||
|
||||
request.postResult(result || { success: true });
|
||||
|
||||
this[priv.request] = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Posts activity request error.
|
||||
* @param {*} error Error to post as activity request error.
|
||||
* @private
|
||||
*/
|
||||
postError(error) {
|
||||
var request = this[priv.request];
|
||||
if (!request) {
|
||||
throw new Error('There is no any activity request to post error to!');
|
||||
}
|
||||
|
||||
request.postError(error);
|
||||
|
||||
this[priv.request] = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Handler that fires once app receives activity request via system message.
|
||||
* @param {ActivityRequestHandler} request Activity request.
|
||||
* @private
|
||||
*/
|
||||
[priv.onActivityRequest](request) {
|
||||
this[priv.request] = request;
|
||||
|
||||
this.broadcast('activity-request', request.source);
|
||||
}
|
||||
};
|
||||
|
||||
exports.ActivityShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
ActivityShim,
|
||||
SERVICE_NAME,
|
||||
{ methods: METHODS, events: EVENTS }
|
||||
)
|
||||
);
|
||||
})(window);
|
||||
110
gaia/apps/sms/services/js/bridge_service_mixin.js
Executable file
110
gaia/apps/sms/services/js/bridge_service_mixin.js
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
/*global bridge,
|
||||
streamService
|
||||
*/
|
||||
/*jshint esnext: true */
|
||||
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const priv = Object.freeze({
|
||||
methods: Symbol('methods'),
|
||||
streams: Symbol('streams'),
|
||||
events: Symbol('events'),
|
||||
service: Symbol('service')
|
||||
});
|
||||
|
||||
/**
|
||||
* This mixin allows to expose easily a service from an object.
|
||||
*
|
||||
* This needs the following properties to work properly:
|
||||
*
|
||||
* - SERVICE_NAME holds the service name.
|
||||
* - METHODS holds the methods that will be exposed by the service.
|
||||
* - STREAMS holds the methods that handle streams exposed by the service.
|
||||
*
|
||||
* EVENTS, METHODS and STREAMS are all optional.
|
||||
*/
|
||||
var serviceMixin = {
|
||||
/**
|
||||
* Initialize service to listen on the default context and given endpoint.
|
||||
* @param {(Iframe|Worker|MessagePort|BroadcastChannel|Window)} endpoint
|
||||
* The context/thread that service could listen for.
|
||||
*/
|
||||
initService(endpoint) {
|
||||
if (!('bridge' in self) || !('service' in bridge)) {
|
||||
importScripts('/lib/bridge/service.js');
|
||||
}
|
||||
|
||||
var service = bridge.service(this[priv.name]);
|
||||
|
||||
this[priv.methods].forEach((exposedMethod) => {
|
||||
service.method(exposedMethod, this[exposedMethod].bind(this));
|
||||
});
|
||||
|
||||
var streams = this[priv.streams];
|
||||
if (streams.length > 0) {
|
||||
if (!('streamService' in self)) {
|
||||
importScripts('/lib/bridge/plugins/stream/service.js');
|
||||
}
|
||||
|
||||
service.plugin(streamService);
|
||||
|
||||
streams.forEach((exposedStream) => {
|
||||
service.stream(exposedStream, this[exposedStream].bind(this));
|
||||
});
|
||||
}
|
||||
|
||||
if (this.onConnected) {
|
||||
service.on('connected', this.onConnected.bind(this));
|
||||
}
|
||||
|
||||
service.listen();
|
||||
|
||||
endpoint && service.listen(endpoint);
|
||||
|
||||
this[priv.service] = service;
|
||||
},
|
||||
|
||||
/**
|
||||
* Broadcast an event to all listeners.
|
||||
* @param {String} eventName The event name to send.
|
||||
* @param {any} data Data to send with the event.
|
||||
*/
|
||||
broadcast(eventName, data) {
|
||||
if (this[priv.events].indexOf(eventName) < 0) {
|
||||
throw new Error('Event "' + eventName + '" is not allowed!');
|
||||
}
|
||||
|
||||
this[priv.service].broadcast(eventName, data);
|
||||
}
|
||||
};
|
||||
|
||||
exports.BridgeServiceMixin = {
|
||||
mixin(target, name, { methods, streams, events }) {
|
||||
if (!name) {
|
||||
throw new Error(
|
||||
'A service name is mandatory to define a service.'
|
||||
);
|
||||
}
|
||||
|
||||
Object.keys(serviceMixin).forEach(function(method) {
|
||||
if (typeof this[method] !== 'undefined') {
|
||||
throw new Error(
|
||||
'Object to mix into already has "' + method + '" property defined!'
|
||||
);
|
||||
}
|
||||
|
||||
this[method] = serviceMixin[method];
|
||||
}, target);
|
||||
|
||||
target[priv.service] = null;
|
||||
target[priv.name] = name;
|
||||
target[priv.methods] = methods || [];
|
||||
target[priv.streams] = streams || [];
|
||||
target[priv.events] = events || [];
|
||||
|
||||
return target;
|
||||
}
|
||||
};
|
||||
})(self);
|
||||
99
gaia/apps/sms/services/js/conversation/conversation_client.js
Executable file
99
gaia/apps/sms/services/js/conversation/conversation_client.js
Executable file
|
|
@ -0,0 +1,99 @@
|
|||
/* global bridge,
|
||||
streamClient,
|
||||
Thread
|
||||
*/
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const debug = 0 ?
|
||||
(arg1, ...args) => console.log(`[ConversationClient] ${arg1}`, ...args):
|
||||
() => {};
|
||||
|
||||
const mark = 0 ?
|
||||
(...args) => exports.performance.mark(`[ConversationClient] ${args}`):
|
||||
() => {};
|
||||
|
||||
const priv = {
|
||||
client: Symbol('client'),
|
||||
appInstanceId: Symbol('appInstanceId')
|
||||
};
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for managing conversation.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'conversation-service';
|
||||
|
||||
/**
|
||||
* ConversationClient is a wrapper around bridge client connected to the
|
||||
* bridge service hosted in a Shared Worker.
|
||||
* @type {Object}
|
||||
*/
|
||||
var ConversationClient = {
|
||||
/**
|
||||
* Reference to active bridge client instance.
|
||||
* @type {Client}
|
||||
*/
|
||||
[priv.client]: null,
|
||||
|
||||
/**
|
||||
* Unique identifier of app instance where client resides in.
|
||||
* @type {string}
|
||||
*/
|
||||
[priv.appInstanceId]: null,
|
||||
|
||||
/**
|
||||
* Initialized conversation service client bridge.
|
||||
* @param {string} appInstanceId Unique identifier of app instance where
|
||||
* client resides in.
|
||||
*/
|
||||
init(appInstanceId) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
this[priv.client] = bridge.client({
|
||||
service: SERVICE_NAME,
|
||||
endpoint: new SharedWorker(
|
||||
'/services/js/conversation/conversation_service.js'
|
||||
),
|
||||
timeout: false
|
||||
}).plugin(streamClient);
|
||||
|
||||
this[priv.appInstanceId] = appInstanceId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves all conversations and drafts.
|
||||
* @param {Function} onConversationRetrieved Callback that is called for
|
||||
* every retrieved conversation.
|
||||
* @returns {Promise} Promise is resolved once all conversations are
|
||||
* retrieved and rejected if conversation stream is closed unexpectedly.
|
||||
*/
|
||||
getAllConversations(onConversationRetrieved) {
|
||||
mark('start retrieving conversations');
|
||||
|
||||
var getAllConversationsStream = this[priv.client].stream(
|
||||
'getAllConversations', this[priv.appInstanceId]
|
||||
);
|
||||
|
||||
var index = 0;
|
||||
getAllConversationsStream.listen((thread) => {
|
||||
mark(++index, ' conversation retrieved');
|
||||
debug('Conversation with id %s is retrieved from stream', thread.id);
|
||||
|
||||
onConversationRetrieved(new Thread(thread));
|
||||
});
|
||||
|
||||
return getAllConversationsStream.closed.then(() => {
|
||||
mark('all conversations retrieved');
|
||||
}).catch((e) => {
|
||||
console.error('Conversation stream is closed unexpectedly', e);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.ConversationClient = Object.seal(ConversationClient);
|
||||
})(window);
|
||||
234
gaia/apps/sms/services/js/conversation/conversation_service.js
Executable file
234
gaia/apps/sms/services/js/conversation/conversation_service.js
Executable file
|
|
@ -0,0 +1,234 @@
|
|||
/*global BridgeServiceMixin,
|
||||
Draft,
|
||||
Drafts,
|
||||
MozMobileMessageClient
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Type description for the ConversationSummary object that is return from the
|
||||
* Conversation service.
|
||||
* @typedef {Object} ConversationSummary
|
||||
* @property {number} id Unique identifier of the the conversation.
|
||||
* @property {string} body Short content of the last message in the
|
||||
* conversation.
|
||||
* @property {Array.<string>} participants List of the conversation
|
||||
* participants.
|
||||
* @property {Date} timestamp Conversation timestamp.
|
||||
* @property {{ hasUnread: boolean, hasNewError: boolean }} status Conversation
|
||||
* status that indicates whether conversation has unread messages or/and failed
|
||||
* messages that user hasn't seen yet.
|
||||
* @property {string} lastMessageType Type of the last message in the
|
||||
* conversation. Can be either 'sms' or 'mms'.
|
||||
* @property {Draft} draft If conversation is draft or has draft, then this
|
||||
* field contains appropriate draft information, otherwise it's null.
|
||||
*/
|
||||
|
||||
[
|
||||
['BridgeServiceMixin', '/services/js/bridge_service_mixin.js'],
|
||||
[
|
||||
'MozMobileMessageClient',
|
||||
'/services/js/moz_mobile_message/moz_mobile_message_client.js'
|
||||
],
|
||||
['Drafts', '/services/js/drafts.js']
|
||||
].forEach(([dependencyName, dependencyPath]) => {
|
||||
if (!(dependencyName in self)) {
|
||||
importScripts(dependencyPath);
|
||||
}
|
||||
});
|
||||
|
||||
(function(exports) {
|
||||
const debug = 0 ?
|
||||
(arg1, ...args) => console.log(`[ConversationService] ${arg1}`, ...args):
|
||||
() => {};
|
||||
|
||||
// We use @-directive to override marker context that is scoped by
|
||||
// SharedWorker lifetime by default, but we need it to be aligned with the
|
||||
// main app context.
|
||||
const mark = 0 ?
|
||||
(...args) => exports.performance.mark(
|
||||
`[ConversationService] ${args}@sms.gaiamobile.org`
|
||||
):
|
||||
() => {};
|
||||
|
||||
const SERVICE_CONTRACT = Object.freeze({
|
||||
name: 'conversation-service',
|
||||
|
||||
streams: Object.freeze(
|
||||
['getAllConversations', 'getMessagesForConversation']
|
||||
),
|
||||
|
||||
methods: Object.freeze([
|
||||
'deleteConversations', 'deleteMessages', 'markConversationsAs',
|
||||
'getConversationSummary', 'getMessage', 'findConversationFromAddress'
|
||||
]),
|
||||
|
||||
events: Object.freeze(['message-change'])
|
||||
});
|
||||
|
||||
function draftToConversationSummary(draft) {
|
||||
var body = Array.isArray(draft.content) ?
|
||||
draft.content.find((content) => typeof content === 'string') : '';
|
||||
|
||||
return {
|
||||
id: +draft.id,
|
||||
participants: Array.isArray(draft.recipients) ? draft.recipients : [],
|
||||
body: body,
|
||||
timestamp: new Date(draft.timestamp),
|
||||
status: { hasUnread: false, hasNewError: false },
|
||||
lastMessageType: draft.type || 'sms',
|
||||
draft: new Draft(draft)
|
||||
};
|
||||
}
|
||||
|
||||
function threadToConversationSummary(thread) {
|
||||
return {
|
||||
id: thread.id,
|
||||
participants: thread.participants,
|
||||
body: thread.body,
|
||||
timestamp: thread.timestamp,
|
||||
status: { hasUnread: thread.unreadCount > 0, hasNewError: false },
|
||||
lastMessageType: thread.lastMessageType,
|
||||
draft: Drafts.byThreadId(thread.id)
|
||||
};
|
||||
}
|
||||
|
||||
var ConversationService = {
|
||||
/**
|
||||
* Initializes our service using the Service mixin's initService.
|
||||
*/
|
||||
init() {
|
||||
this.initService();
|
||||
},
|
||||
|
||||
/**
|
||||
* Stream that returns everything needed to display conversations in the
|
||||
* inbox view.
|
||||
* @param {ServiceStream.<ConversationSummary>} serviceStream The stream to
|
||||
* use in the implementation: it will be passed automatically by the Bridge
|
||||
* library.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
*/
|
||||
getAllConversations(serviceStream, appInstanceId) {
|
||||
mark('start retrieving conversations');
|
||||
|
||||
var getThreadsStream = MozMobileMessageClient.forApp(
|
||||
appInstanceId
|
||||
).getThreads();
|
||||
|
||||
// If conversation stream is cancelled we should cancel internal threads
|
||||
// stream as well.
|
||||
serviceStream.cancel = (reason) => {
|
||||
debug('getAllConversations stream is cancelled: %s', reason);
|
||||
return getThreadsStream.cancel(reason);
|
||||
};
|
||||
|
||||
var draftsPromise = Drafts.request().then(() => {
|
||||
// Return draft list that is sorted by timestamp in inverse order.
|
||||
return Drafts.getAllThreadless().sort(
|
||||
(draftA, draftB) => draftB.timestamp - draftA.timestamp
|
||||
);
|
||||
});
|
||||
|
||||
// Assume that we get the data in an inverse sort order.
|
||||
var index = 0;
|
||||
getThreadsStream.listen((thread) => {
|
||||
mark(++index, ' conversation retrieved');
|
||||
debug('Retrieved conversation with id %s', thread.id);
|
||||
|
||||
draftsPromise.then((drafts) => {
|
||||
// Flush drafts that are created earlier than current thread.
|
||||
while(drafts.length && drafts[0].timestamp >= thread.timestamp) {
|
||||
serviceStream.write(draftToConversationSummary(drafts.shift()));
|
||||
}
|
||||
|
||||
serviceStream.write(threadToConversationSummary(thread));
|
||||
});
|
||||
});
|
||||
|
||||
return getThreadsStream.closed.then(() => draftsPromise).then(
|
||||
(drafts) => {
|
||||
for(var draft of drafts) {
|
||||
serviceStream.write(draftToConversationSummary(draft));
|
||||
}
|
||||
|
||||
mark('All conversations retrieved');
|
||||
|
||||
serviceStream.close();
|
||||
}).catch((e) => {
|
||||
// Pass error object once the following issue is fixed:
|
||||
// https://github.com/gaia-components/threads/issues/74
|
||||
serviceStream.abort(`[${e.name}] ${e.message || ''}`);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Stream that returns messages in a conversation, with all suitable
|
||||
* information to display in the conversation view.
|
||||
* @param {ServiceStream.<MessageSummary>} The stream to use in the
|
||||
* implementation: it will be passed automatically by the Bridge library.
|
||||
* @param {Number} conversationId Conversation to retrieve.
|
||||
*/
|
||||
getMessagesForConversation(stream, conversationId) {},
|
||||
|
||||
/**
|
||||
* Returns all information related to a single conversation, to display in
|
||||
* the conversation view.
|
||||
* @param {Number} conversationId Conversation to retrieve.
|
||||
* @returns {ConversationSummary}
|
||||
*/
|
||||
getConversationSummary(conversationId) {},
|
||||
|
||||
/**
|
||||
* Deletes one or several conversations, including their messages.
|
||||
* @param {Array.<Number>} conversationIds ID of the conversations to be
|
||||
* deleted.
|
||||
*/
|
||||
deleteConversations(conversationIds) {},
|
||||
|
||||
/**
|
||||
* Deletes one or several messages.
|
||||
* @param {Array.<Number>} messageIds ID of the messages to be deleted.
|
||||
*/
|
||||
deleteMessages(messageIds) {},
|
||||
|
||||
/**
|
||||
* Marks a conversation as read or unread.
|
||||
* @param {Object} param
|
||||
* @param {Array.<Number>} param.conversations Conversation IDs to be
|
||||
* marked.
|
||||
* @param {Boolean} param.asRead Specifies whether we mark as read of
|
||||
* unread.
|
||||
*/
|
||||
markConversationsAs({conversations, asRead}) {},
|
||||
|
||||
/**
|
||||
* Fetches a message information.
|
||||
* @param {Number} messageId The message ID to retrieve.
|
||||
* @returns {Message} All message information, including contact, delivery,
|
||||
* read reports, size, timestamps, etc.
|
||||
*/
|
||||
getMessage(messageId) {},
|
||||
|
||||
/**
|
||||
* Finds a conversation from an address (that is: a phone number or an
|
||||
* email).
|
||||
* @param {String} address This is either a phone number or an email, to
|
||||
* match an existing conversation from.
|
||||
* @returns {Number?} The id of the conversation, or null if not found.
|
||||
*/
|
||||
findConversationFromAddress(address) {}
|
||||
};
|
||||
|
||||
exports.ConversationService = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
ConversationService, SERVICE_CONTRACT.name, SERVICE_CONTRACT
|
||||
)
|
||||
);
|
||||
|
||||
// Automatically init service if it's run inside worker.
|
||||
if (!self.document) {
|
||||
ConversationService.init();
|
||||
}
|
||||
})(self);
|
||||
266
gaia/apps/sms/services/js/drafts.js
Executable file
266
gaia/apps/sms/services/js/drafts.js
Executable file
|
|
@ -0,0 +1,266 @@
|
|||
/*global asyncStorage,
|
||||
InterInstanceEventDispatcher,
|
||||
Utils,
|
||||
EventDispatcher
|
||||
*/
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
[
|
||||
['asyncStorage', '/shared/js/async_storage.js'],
|
||||
['EventDispatcher', '/shared/js/event_dispatcher.js'],
|
||||
['Utils', '/views/shared/js/utils.js']
|
||||
].forEach(([dependencyName, dependencyPath]) => {
|
||||
if (!(dependencyName in self)) {
|
||||
importScripts(dependencyPath);
|
||||
}
|
||||
});
|
||||
|
||||
var draftIndex = new Map();
|
||||
var deferredDraftRequest = null;
|
||||
|
||||
/**
|
||||
* Drafts
|
||||
*
|
||||
* A collection of active Draft objects, indexed by thread id.
|
||||
*/
|
||||
var Drafts = {
|
||||
/**
|
||||
* size
|
||||
*
|
||||
* Returns the number of Draft Lists.
|
||||
*
|
||||
* There is one Draft.List per Thread and
|
||||
* one Draft.List for all threadless Drafts.
|
||||
*
|
||||
* @return {Number} Maps to draftIndex size property.
|
||||
*/
|
||||
get size() {
|
||||
return draftIndex.size;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initialize the InterInstanceEventDispatcher and relay the draft
|
||||
* saved/deleted events from another instance.
|
||||
*/
|
||||
init() {
|
||||
InterInstanceEventDispatcher.on('draft-saved', (draft) =>
|
||||
this.request(true).then(() => this.emit('saved', draft))
|
||||
);
|
||||
|
||||
InterInstanceEventDispatcher.on('draft-deleted', (draft) =>
|
||||
this.request(true).then(() => this.emit('deleted', draft))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Pushes a Draft instance to the Drafts in-memory collection and commits
|
||||
* it to the persistent storage.
|
||||
* @param {Draft} draft Draft instance.
|
||||
* @returns {Drafts} Returns the Drafts object.
|
||||
*/
|
||||
add: function(draft) {
|
||||
if (!draft || !(draft instanceof Draft)) {
|
||||
throw new Error('Draft should be valid Draft instance.');
|
||||
}
|
||||
|
||||
// Remove existing draft.
|
||||
this.delete(draft);
|
||||
|
||||
var drafts = draftIndex.get(draft.threadId) || [];
|
||||
drafts.push(draft);
|
||||
draftIndex.set(draft.threadId, drafts);
|
||||
|
||||
this.emit('saved', draft);
|
||||
InterInstanceEventDispatcher.emit('draft-saved', draft);
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes a draft record from Drafts in-memory collection.
|
||||
* @param {Draft} draft Draft instance to delete.
|
||||
* @returns {Drafts} Returns the Drafts object.
|
||||
*/
|
||||
delete: function(draft) {
|
||||
if (!draft || !(draft instanceof Draft)) {
|
||||
throw new Error('Draft should be valid Draft instance.');
|
||||
}
|
||||
|
||||
var drafts = draftIndex.get(draft.threadId);
|
||||
if (!drafts || !drafts.length) {
|
||||
return this;
|
||||
}
|
||||
|
||||
// Flag to track that draft was really removed.
|
||||
var isDeleted = false;
|
||||
|
||||
// For drafts bound to thread we should remove all drafts we have, but for
|
||||
// thread-less drafts we should remove draft with the same id only.
|
||||
if (draft.threadId) {
|
||||
drafts.length = 0;
|
||||
isDeleted = true;
|
||||
} else {
|
||||
var indexToRemove = drafts.findIndex(
|
||||
(storedDraft) => storedDraft.id === draft.id
|
||||
);
|
||||
|
||||
if (indexToRemove > -1) {
|
||||
isDeleted = true;
|
||||
drafts.splice(indexToRemove, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (isDeleted) {
|
||||
this.emit('deleted', draft);
|
||||
InterInstanceEventDispatcher.emit('draft-deleted', draft);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns draft for the specified thread id or null if it doesn't exist.
|
||||
* @param {Number} threadId Thread id to recall draft for.
|
||||
* @returns {Draft}
|
||||
*/
|
||||
byThreadId: function(threadId) {
|
||||
if (!threadId) {
|
||||
throw new Error('Thread Id is not defined');
|
||||
}
|
||||
|
||||
var threadDrafts = draftIndex.get(threadId);
|
||||
return threadDrafts && threadDrafts.length ?
|
||||
threadDrafts[threadDrafts.length - 1] : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the draft object with the specified id.
|
||||
* @param {Number|String} id Id of the draft to return.
|
||||
* @returns {Draft} Found draft instance or null if nothing is found.
|
||||
*/
|
||||
byDraftId: function(id) {
|
||||
// Ensure a number is used for the comparison, as this value may come from
|
||||
// a dataset property.
|
||||
id = +id;
|
||||
|
||||
for (var draft of this.getAllThreadless()) {
|
||||
if (draft.id === id) {
|
||||
return draft;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns iterator for all available drafts. For the drafts associated with
|
||||
* the thread, only the latest draft is returned.
|
||||
*/
|
||||
getAll: function* () {
|
||||
for (var [threadId, drafts] of draftIndex) {
|
||||
// For drafts with associated thread we consider only the latest draft.
|
||||
yield* threadId ? drafts.slice(-1) : drafts;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns list of all thread less drafts.
|
||||
* @returns {Array.<Draft>}
|
||||
*/
|
||||
getAllThreadless: function () {
|
||||
return draftIndex.get(null) || [];
|
||||
},
|
||||
|
||||
/**
|
||||
* clear
|
||||
*
|
||||
* Delete drafts from the map.
|
||||
*
|
||||
* @return {Drafts} return the Drafts object.
|
||||
*/
|
||||
clear: function() {
|
||||
draftIndex = new Map();
|
||||
deferredDraftRequest = null;
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* Stores drafts that are held in memory to local storage.
|
||||
*/
|
||||
store: function() {
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
asyncStorage.setItem('draft index', [...draftIndex], () => {
|
||||
defer.resolve();
|
||||
});
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Requests drafts from asyncStorage or in-memory cache. Result is cached.
|
||||
* @param {boolean?} force Indicates whether we should respect already
|
||||
* cached result or _force_ asyncStorage request once again.
|
||||
* @returns {Promise} Promise that resolves when all drafts are retrieved
|
||||
* from persistent storage.
|
||||
*/
|
||||
request: function(force) {
|
||||
// Loading from storage only happens once or when specifically requested
|
||||
// with force parameter.
|
||||
if (deferredDraftRequest && !force) {
|
||||
return deferredDraftRequest.promise;
|
||||
}
|
||||
|
||||
deferredDraftRequest = Utils.Promise.defer();
|
||||
|
||||
asyncStorage.getItem('draft index', function (records) {
|
||||
// Convert every plain JS draft object into Draft "class" instance.
|
||||
// record[0] is the threadId or null key, record[1] is the array of
|
||||
// draft objects associated with that threadId or null key.
|
||||
records && records.forEach((record) => {
|
||||
record[1] = record[1].map((draft) => new Draft(draft));
|
||||
});
|
||||
draftIndex = new Map(records || []);
|
||||
|
||||
deferredDraftRequest.resolve();
|
||||
});
|
||||
|
||||
return deferredDraftRequest.promise;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Drafts = EventDispatcher.mixin(Drafts, ['saved', 'deleted']);
|
||||
|
||||
/**
|
||||
* Draft
|
||||
*
|
||||
* A message-like object containing unsent
|
||||
* message content to be stored temporarily
|
||||
* in a Drafts collection.
|
||||
*
|
||||
* @param {Object} draft Draft or empty object.
|
||||
*/
|
||||
function Draft(opts) {
|
||||
var draft = opts || {};
|
||||
|
||||
if (draft.id && typeof draft.id !== 'number') {
|
||||
throw new Error('Draft id must be a number');
|
||||
}
|
||||
|
||||
this.id = draft.id || guid();
|
||||
this.recipients = draft.recipients || [];
|
||||
this.content = draft.content || [];
|
||||
this.subject = draft.subject || '';
|
||||
this.timestamp = +draft.timestamp || Date.now();
|
||||
this.threadId = draft.threadId || null;
|
||||
this.type = draft.type;
|
||||
this.isEdited = false;
|
||||
}
|
||||
|
||||
function guid() {
|
||||
return +(Date.now() + '' + (Math.random() * 1000 | 0));
|
||||
}
|
||||
|
||||
exports.Draft = Draft;
|
||||
}(this));
|
||||
715
gaia/apps/sms/services/js/message_manager.js
Executable file
715
gaia/apps/sms/services/js/message_manager.js
Executable file
|
|
@ -0,0 +1,715 @@
|
|||
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
|
||||
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
|
||||
|
||||
|
||||
/* global EventDispatcher,
|
||||
Promise,
|
||||
Settings,
|
||||
SMIL,
|
||||
Threads,
|
||||
Utils
|
||||
*/
|
||||
|
||||
/*exported MessageManager */
|
||||
|
||||
/**
|
||||
* MessageManager wraps around {@link http://mdn.io/MozMobileMessageManager}
|
||||
* that makes access to SMS/MMS related operations (sending, handling
|
||||
* read/delivery reports etc.) and respective events easier.
|
||||
* @module MessageManager
|
||||
*/
|
||||
'use strict';
|
||||
(function(exports) {
|
||||
var MessageManager = {
|
||||
|
||||
/**
|
||||
* Initializes MessageManager and subscribes for required
|
||||
* {@link http://mdn.io/MozMobileMessageManager} events.
|
||||
* Should be always called before calling any other MessageManager methods.
|
||||
*/
|
||||
init: function mm_init() {
|
||||
this._mozMobileMessage = navigator.mozMobileMessage;
|
||||
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'received', this.onMessageReceived.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'sending', this.onMessageSending.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'sent', this.onMessageSent.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'failed', this.onMessageFailed.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'readsuccess', this.onReadSuccess.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'deliverysuccess', this.onDeliverySuccess.bind(this)
|
||||
);
|
||||
this._mozMobileMessage.addEventListener(
|
||||
'deleted', this.onDeleted.bind(this)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Event occurs when an SMS or MMS enters the send flow.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageSending: function mm_onMessageSending(e) {
|
||||
Threads.registerMessage(e.message);
|
||||
|
||||
this.emit('message-sending', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "failed" events. These events occur
|
||||
* when sending an MMS or SMS has failed.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageFailed: function mm_onMessageFailed(e) {
|
||||
this.emit('message-failed-to-send', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "deliverysuccess" events. These
|
||||
* events occur when an SMS or MMS is delivered.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onDeliverySuccess: function mm_onDeliverySuccess(e) {
|
||||
this.emit('message-delivered', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "read" events. These events
|
||||
* occur when MMS is read by recipient (read status isn't supported by SMS).
|
||||
* @param {MozMmsEvent} e Instance of {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onReadSuccess: function mm_onReadSuccess(e) {
|
||||
this.emit('message-read', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "sent" events. These events occur
|
||||
* when an SMS or MMS is sent.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageSent: function mm_onMessageSent(e) {
|
||||
this.emit('message-sent', { message: e.message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Specifies an event listener to receive "received" events. These events
|
||||
* occur when an SMS or MMS is received. Event handler executed when new SMS
|
||||
* or MMS is received.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onMessageReceived: function mm_onMessageReceived(e) {
|
||||
var message = e.message;
|
||||
|
||||
if (message.messageClass && message.messageClass === 'class-0') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Here we can only have one sender, so deliveryInfo[0].deliveryStatus =>
|
||||
// message status from sender. Ignore 'pending' messages that are received
|
||||
// this means we are in automatic download mode.
|
||||
if (message.delivery === 'not-downloaded' &&
|
||||
message.deliveryInfo[0].deliveryStatus === 'pending') {
|
||||
return;
|
||||
}
|
||||
|
||||
Threads.registerMessage(message);
|
||||
|
||||
this.emit('message-received', { message: message });
|
||||
},
|
||||
|
||||
/**
|
||||
* Event handler executed when message is deleted.
|
||||
* @param {MozSmsEvent|MozMmsEvent} e Instance of
|
||||
* {@link http://mdn.io/MozSmsEvent} or {@link http://mdn.io/MozMmsEvent}.
|
||||
*/
|
||||
onDeleted: function(e) {
|
||||
// The method verifies that some threads have been deleted.
|
||||
if (e.deletedThreadIds && e.deletedThreadIds.length) {
|
||||
this.emit('threads-deleted', {
|
||||
ids: e.deletedThreadIds
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtains all the treads and takes a set of callback functions as an
|
||||
* argument.
|
||||
* @param {Object} options A set of callback functions as described below:-
|
||||
* @param {function) options.each: callback function invoked for each message.
|
||||
* @param {function} options.end: callback function run when cursor is "done".
|
||||
* @param {function} options.done: callback function run when we iterated
|
||||
* through all the threads or consumer requested to do so.
|
||||
*/
|
||||
getThreads: function mm_getThreads(options) {
|
||||
|
||||
var each = options.each;
|
||||
var end = options.end;
|
||||
var done = options.done;
|
||||
var cursor = null;
|
||||
|
||||
// WORKAROUND for bug 958738. We can remove 'try\catch' block once this bug
|
||||
// is resolved.
|
||||
try {
|
||||
cursor = this._mozMobileMessage.getThreads();
|
||||
} catch(e) {
|
||||
console.error('Error occurred while retrieving threads:', e.name, e);
|
||||
end && end();
|
||||
done && done();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
if (this.result) {
|
||||
// Invoking callback on each thread.
|
||||
each && each(this.result);
|
||||
|
||||
this.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Invoking terminating callbacks.
|
||||
end && end();
|
||||
done && done();
|
||||
};
|
||||
|
||||
cursor.onerror = function onerror() {
|
||||
console.error('Reading the database. Error: ' + this.error.name);
|
||||
done && done();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Method used to access message having a given id.
|
||||
* @param {number} id The id of the message we wish to get.
|
||||
* @return {DOMRequest<SmsMessage|MmsMessage>}
|
||||
*/
|
||||
getMessage: function mm_getMsg(id) {
|
||||
return this._mozMobileMessage.getMessage(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to get the MMS object having a given id.
|
||||
* @param {number} id The id of the MMS we wish to get.
|
||||
* @return {DOMRequest<MmsMessage>}
|
||||
*/
|
||||
retrieveMMS: function mm_retrieveMMS(id) {
|
||||
return this._mozMobileMessage.retrieveMMS(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtains all the messages using a given filter.
|
||||
* @param {Object} options A set of callback functions as described below:-
|
||||
* @param {function} options.each callback function invoked for each message.
|
||||
* @param {function} options.end callback function run when cursor is "done".
|
||||
* @param {function} options.endArgs specify arguments for the "end" callback.
|
||||
* @param {function} options.done callback function invoked when we iterated
|
||||
* through all the messages or consumer requested to do so.
|
||||
* @param {function} options.filter a MobileMessageFilter or similar object.
|
||||
* @param {boolean} invert option to invert the selection.
|
||||
*/
|
||||
getMessages: function mm_getMgs(options) {
|
||||
var each = options.each;
|
||||
var invert = options.invert;
|
||||
var end = options.end;
|
||||
var endArgs = options.endArgs;
|
||||
var done = options.done;
|
||||
var filter = options.filter;
|
||||
var cursor = this._mozMobileMessage.getMessages(filter, !invert);
|
||||
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
if (!this.done) {
|
||||
var shouldContinue = true;
|
||||
if (each) {
|
||||
shouldContinue = each(this.result);
|
||||
}
|
||||
// if each returns false the iteration stops.
|
||||
if (shouldContinue !== false) { // if this is undefined this is fine.
|
||||
this.continue();
|
||||
} else {
|
||||
done && done();
|
||||
}
|
||||
} else {
|
||||
end && end(endArgs);
|
||||
done && done();
|
||||
}
|
||||
};
|
||||
cursor.onerror = function onerror() {
|
||||
var msg = 'Reading the database. Error: ' + this.error.name;
|
||||
console.log(msg);
|
||||
done && done();
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Sanitizes the "serviceId". It works as follows:-
|
||||
* 0 is a valid value so we need to take care to not consider it as a falsy
|
||||
* value. We want to return null for anything that's not a number or a string
|
||||
* containing a number.
|
||||
* @param {number|string} id The service ID.
|
||||
* @private
|
||||
* @return {?number}
|
||||
*/
|
||||
_sanitizeServiceId: function mm_sanitizeServiceId(serviceId) {
|
||||
if (serviceId == null || // null or undefined
|
||||
isNaN(+serviceId)) {
|
||||
serviceId = null;
|
||||
} else {
|
||||
serviceId = +serviceId;
|
||||
}
|
||||
|
||||
return serviceId;
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets send options for the specified "serviceId".
|
||||
* @param {number|string} id The service ID.
|
||||
* @private
|
||||
* @return {{ serviceId: number|string }}
|
||||
*/
|
||||
_getSendOptionsFromServiceId: function mm_gSOFSI(serviceId) {
|
||||
var sendOpts;
|
||||
|
||||
if (serviceId != null && // not null, not undefined
|
||||
Settings.hasSeveralSim()) {
|
||||
sendOpts = { serviceId: serviceId };
|
||||
}
|
||||
|
||||
return sendOpts;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an SMS based on the specified parameters object
|
||||
* @param {Object} opts Options object that contains :-
|
||||
* - recipients (string or array of string): contains the list of recipients
|
||||
* for this message.
|
||||
* - content (string): the message's body.
|
||||
* - serviceId (optional long or string): the SIM serviceId we use to send the
|
||||
* message.
|
||||
* - onsuccess (optional function): will be called when one SMS has been
|
||||
* sent successfully, with the request's result as argument. Can be called
|
||||
* several times.
|
||||
* - onerror (optional function): will be called when one SMS transmission
|
||||
* failed, with the error object as argument. Can be called several times.
|
||||
* - oncomplete (optional function): will be called when all messages have
|
||||
* been sent. It's argument will have the following properties:
|
||||
* + hasError (boolean): whether we had at least one error
|
||||
* + return (array): each item is an object with the following properties:
|
||||
* . success (boolean): whether this is a success or an error.
|
||||
* . result (request's result): the request's result object.
|
||||
* . recipient (string): the recipient used for this transmission.
|
||||
*/
|
||||
sendSMS: function mm_send(opts) {
|
||||
var recipients = opts.recipients || [],
|
||||
content = opts.content,
|
||||
serviceId = this._sanitizeServiceId(opts.serviceId),
|
||||
onsuccess = opts.onsuccess,
|
||||
onerror = opts.onerror,
|
||||
oncomplete = opts.oncomplete;
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
// The returned value is not a DOM request!
|
||||
// Instead, It's an array of DOM requests.
|
||||
var i = 0;
|
||||
var requestResult = { hasError: false, return: [] };
|
||||
|
||||
// Getting serviceId field in sendOpts.
|
||||
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
|
||||
|
||||
var requests = this._mozMobileMessage.send(recipients, content, sendOpts);
|
||||
|
||||
var numberOfRequests = requests.length;
|
||||
|
||||
requests.forEach(function(request, idx) {
|
||||
request.onsuccess = function onSuccess(event) {
|
||||
// Callback functions executed message has been successfully sent.
|
||||
onsuccess && onsuccess(event.target.result);
|
||||
|
||||
requestResult.return.push({
|
||||
success: true,
|
||||
result: event.target.result,
|
||||
recipient: recipients[idx]
|
||||
});
|
||||
|
||||
// Last callback, oncomplete, executed at the end of the loop.
|
||||
if (i === numberOfRequests - 1) {
|
||||
oncomplete && oncomplete(requestResult);
|
||||
}
|
||||
i++;
|
||||
};
|
||||
|
||||
request.onerror = function onError(event) {
|
||||
console.error('Error Sending: ' + JSON.stringify(event.target.error));
|
||||
onerror && onerror(event.target.error);
|
||||
|
||||
requestResult.hasError = true;
|
||||
requestResult.return.push({
|
||||
success: false,
|
||||
code: event.target.error,
|
||||
recipient: recipients[idx]
|
||||
});
|
||||
|
||||
if (i === numberOfRequests - 1) {
|
||||
oncomplete && oncomplete(requestResult);
|
||||
}
|
||||
i++;
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an MMS based on the specified parameters object
|
||||
* @param {Object} opts is the options field with the following properties:
|
||||
* - recipients (string or array of string): recipients for this message
|
||||
* - subject (optional string): subject for this message
|
||||
* - content (array of SMIL slides): this is the content for the message (see
|
||||
* ConversationView for more information)
|
||||
* - serviceId (optional long or string): the SIM that should be used for
|
||||
* sending this message. If this is not the current default configuration
|
||||
* for sending MMS, then we'll first switch the configuration to this
|
||||
* serviceId, and only then send the message. That means that the "sending"
|
||||
* event will come quite late in this case.
|
||||
* - onsuccess (optional func): called only once, even for several recipients,
|
||||
* when the message is successfully sent.
|
||||
* - onerror (optional func): called only once if there is an error.
|
||||
*/
|
||||
sendMMS: function mm_sendMMS(opts) {
|
||||
var request;
|
||||
var recipients = opts.recipients,
|
||||
subject = opts.subject,
|
||||
content = opts.content,
|
||||
serviceId = opts.serviceId = this._sanitizeServiceId(opts.serviceId),
|
||||
onsuccess = opts.onsuccess,
|
||||
onerror = opts.onerror;
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
var message = SMIL.generate(content);
|
||||
|
||||
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
|
||||
|
||||
request = this._mozMobileMessage.sendMMS({
|
||||
receivers: recipients,
|
||||
subject: subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
|
||||
request.onsuccess = function onSuccess(event) {
|
||||
onsuccess && onsuccess(event.target.result);
|
||||
};
|
||||
|
||||
request.onerror = function onError(event) {
|
||||
onerror && onerror(event.target.error);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Resends message with the given options.
|
||||
* @param {Object} opts Options is the object with the following properties:-
|
||||
* - message (object): the message object containing all the information
|
||||
* depending on the type of the message which can be 'sms' or 'mms'.
|
||||
* - onsuccess (optional function): will be called when the message has been
|
||||
* sent successfully, with the request's result as argument. Can be called
|
||||
* several times.
|
||||
* - onerror (optional function): will be called when the message transmission
|
||||
* failed, with the error object as argument. Can be called several times.
|
||||
*/
|
||||
resendMessage: function mm_resendMessage(opts) {
|
||||
var message = opts.message;
|
||||
|
||||
if (!message) {
|
||||
throw new Error('Message to resend is not defined.');
|
||||
}
|
||||
|
||||
var serviceId = Settings.getServiceIdByIccId(message.iccId);
|
||||
var sendOpts = this._getSendOptionsFromServiceId(serviceId);
|
||||
var onsuccess = opts.onsuccess;
|
||||
var onerror = opts.onerror;
|
||||
var request;
|
||||
|
||||
if (message.type === 'sms') {
|
||||
request = this._mozMobileMessage.send(
|
||||
message.receiver, message.body, sendOpts);
|
||||
}
|
||||
if (message.type === 'mms') {
|
||||
request = this._mozMobileMessage.sendMMS({
|
||||
receivers: message.receivers,
|
||||
subject: message.subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
}
|
||||
|
||||
request.onsuccess = function onSuccess(evt) {
|
||||
MessageManager.deleteMessages(message.id);
|
||||
onsuccess && onsuccess(evt.target.result);
|
||||
};
|
||||
|
||||
request.onerror = function onError(evt) {
|
||||
MessageManager.deleteMessages(message.id);
|
||||
onerror && onerror(evt.target.error);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Deletes messages using the message id provided.
|
||||
* @param {number} id The ID of the message to be deleted.
|
||||
* @return {Promise}
|
||||
*/
|
||||
deleteMessages: function mm_deleteMessages(id) {
|
||||
// As DOMRequest doesn't have "catch" method we should use "then" with
|
||||
// "null" for the success callback to handle reject case only.
|
||||
return this._mozMobileMessage.delete(id).then(null, (e) => {
|
||||
// TODO: If the messages could not be deleted completely, conversation
|
||||
// view will also update without notification currently. May need more
|
||||
// information for user that the messages were not removed completely.
|
||||
// See bug #1045666 for details.
|
||||
console.error(`Deleting in the database. [${e.name}] ${e.message || ''}`);
|
||||
|
||||
return null;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks a thread as read or unread depending on value of isRead as true or
|
||||
* false respectively.
|
||||
* @param {number|string} threadId The ID of the thread to be marked as read.
|
||||
* @param {boolean} isRead If "true" then message is to be marked as read
|
||||
* otherwise message is to be marked as unread.
|
||||
*/
|
||||
markThreadRead: function mm_markThreadRead(threadId, isRead = true) {
|
||||
// Create filter object to mark all unread messages of the specified thread.
|
||||
var filter = {};
|
||||
filter.threadId = +threadId;
|
||||
filter.read = !isRead;
|
||||
|
||||
var messagesUnreadIDs = [];
|
||||
|
||||
// Building the options field in getMessages() with above filter.
|
||||
var changeStatusOptions = {
|
||||
each: function addUnreadMessage(message) {
|
||||
// First build up array of Unread message ID.
|
||||
messagesUnreadIDs.push(message.id);
|
||||
return true;
|
||||
},
|
||||
filter: filter,
|
||||
invert: true,
|
||||
end: function handleUnread() {
|
||||
// end with marking each of them as read.
|
||||
MessageManager.markMessagesRead(messagesUnreadIDs, isRead);
|
||||
}
|
||||
};
|
||||
MessageManager.getMessages(changeStatusOptions);
|
||||
},
|
||||
|
||||
/**
|
||||
* Marks messages with specified ids as read.
|
||||
* @param {Array<Number>} list List of all the message IDs to be changed to
|
||||
* read.
|
||||
* @param {boolean} isRead Pass true if message is to be marked as read and
|
||||
* false if message is to be marked as unread.
|
||||
*/
|
||||
markMessagesRead: function mm_markMessagesRead(list, isRead = true) {
|
||||
if (!this._mozMobileMessage || !list.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We chain the calls to the API in a way that we make no call to
|
||||
// 'markMessageRead' until a previous call is completed. This way any
|
||||
// other potential call to the API, like the one for getting a message
|
||||
// list, could be done within the calls to mark the messages as read.
|
||||
|
||||
var id = list.pop();
|
||||
// TODO: Third parameter of markMessageRead is return read request.
|
||||
// Here we always return read request for now, but we can let user
|
||||
// decide to return request or not in Bug 971658.
|
||||
var req = this._mozMobileMessage.markMessageRead(id, isRead, true);
|
||||
// isRead == false i.e mark as unread case, marking only one message
|
||||
// as unread is sufficient.
|
||||
req.onsuccess = (function onsuccess() {
|
||||
if (!list.length || !isRead) {
|
||||
return;
|
||||
} else if (isRead) {
|
||||
this.markMessagesRead(list, isRead);
|
||||
}
|
||||
}).bind(this);
|
||||
|
||||
req.onerror = function onerror() {
|
||||
console.error(
|
||||
'Error while marking message %d as read: %s', id, this.error.name
|
||||
);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Method used to get the information necessary to create a multipart SMS for
|
||||
* a given text.
|
||||
* @param {string} text The text used to create the SMS.
|
||||
* @return {Promise<MozSmsSegmentInfo>} Instance of
|
||||
* {@link http://mdn.io/MozSmsSegmentInfo}
|
||||
*/
|
||||
getSegmentInfo: function mm_getSegmentInfo(text) {
|
||||
if (!(this._mozMobileMessage &&
|
||||
this._mozMobileMessage.getSegmentInfoForText)) {
|
||||
return Promise.reject(new Error('mozMobileMessage is unavailable.'));
|
||||
}
|
||||
|
||||
var defer = Utils.Promise.defer();
|
||||
|
||||
var request = this._mozMobileMessage.getSegmentInfoForText(text);
|
||||
request.onsuccess = function onsuccess(e) {
|
||||
defer.resolve(e.target.result);
|
||||
};
|
||||
|
||||
request.onerror = function onerror(e) {
|
||||
defer.reject(e.target.error);
|
||||
};
|
||||
|
||||
return defer.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method checks whether message is received from a sender with specified
|
||||
* number or sent to this particular number. (in case of MMS only 1-to-1,
|
||||
* non-group MMS's are counted).
|
||||
* It's used to differentiate from broadcast messages ---> group MMS.
|
||||
* @param {number} number Phone number.
|
||||
* @param {Object} message A composite object having details about message
|
||||
* delivery, type, sender and receiver.
|
||||
* @private
|
||||
* @return {boolean}
|
||||
*/
|
||||
_isMessageBelongTo1to1Conversation:
|
||||
function isMessageBelongTo1to1Conversation(number, message) {
|
||||
var isIncoming = message.delivery === 'received' ||
|
||||
message.delivery === 'not-downloaded';
|
||||
// if it is a received message, it is a candidate
|
||||
// we still need to test the sender in case the user filters with his own
|
||||
// number, because we would get all the received messages in this case.
|
||||
if (isIncoming) {
|
||||
return Utils.probablyMatches(message.sender, number);
|
||||
} else {
|
||||
switch (message.type) {
|
||||
case 'sms':
|
||||
// in case of sent messages and sms, we test if the receiver match the
|
||||
// filter, to filter out other sent messages in the case user is
|
||||
// sending message to himself
|
||||
return Utils.probablyMatches(message.receiver, number);
|
||||
case 'mms':
|
||||
return message.receivers.length === 1 &&
|
||||
Utils.probablyMatches(message.receivers[0], number);
|
||||
default:
|
||||
console.error('Got an unknown message type: ' + message.type);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Find a SMS/MMS thread from a number.
|
||||
* @param {number} number Phone number to find thread for.
|
||||
* @return {Promise<number>} that is resolved to a threadId or rejected if
|
||||
* not found
|
||||
*/
|
||||
findThreadFromNumber: function mm_findThread(number) {
|
||||
|
||||
function checkCandidate(message) {
|
||||
var isMessageInThread = MessageManager.
|
||||
_isMessageBelongTo1to1Conversation(number, message);
|
||||
if (isMessageInThread) {
|
||||
threadId = message.threadId;
|
||||
return false; // found the message, stop iterating
|
||||
}
|
||||
}
|
||||
|
||||
var threadId = null;
|
||||
var deferred = Utils.Promise.defer();
|
||||
|
||||
MessageManager.getMessages({
|
||||
filter: { numbers: [number] },
|
||||
each: checkCandidate,
|
||||
done: function() {
|
||||
if (threadId == null) {
|
||||
deferred.reject(new Error('No thread found for number: ' + number));
|
||||
return;
|
||||
} else {
|
||||
deferred.resolve(threadId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* This method ensures that threads have been registered in the in-memory
|
||||
* cache. In the case it has, the promise returned resolves immediately. If
|
||||
* not, it loads the last message from the thread and creates a new entry in
|
||||
* cache based on it. It can happen when application loaded from non default
|
||||
* page and Thread cache hasn't been populated.
|
||||
* @param {number|string} threadId The ID of the thread we wish to register.
|
||||
* @return {Promise}
|
||||
*/
|
||||
ensureThreadRegistered(threadId) {
|
||||
var getOneMessageForThread = (threadId) => {
|
||||
var defer = Utils.Promise.defer();
|
||||
this.getMessages({
|
||||
each: function(message) {
|
||||
defer.resolve(message);
|
||||
return false;
|
||||
},
|
||||
done: function() {
|
||||
defer.reject(new Error('found no message'));
|
||||
},
|
||||
filter: { threadId: threadId }
|
||||
});
|
||||
return defer.promise;
|
||||
};
|
||||
|
||||
// Checking whether Thread cache has this thread.
|
||||
if (Threads.has(threadId)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return getOneMessageForThread(threadId).then(
|
||||
(message) => Threads.registerMessage(message)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(exports, 'MessageManager', {
|
||||
get: function () {
|
||||
delete exports.MessageManager;
|
||||
|
||||
exports.MessageManager = EventDispatcher.mixin(MessageManager, [
|
||||
'message-sending', 'message-failed-to-send', 'message-delivered',
|
||||
'message-read', 'message-sent', 'message-received', 'threads-deleted'
|
||||
]);
|
||||
|
||||
return exports.MessageManager;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
});
|
||||
|
||||
})(window);
|
||||
125
gaia/apps/sms/services/js/messaging/messaging_client.js
Executable file
125
gaia/apps/sms/services/js/messaging/messaging_client.js
Executable file
|
|
@ -0,0 +1,125 @@
|
|||
/* global bridge */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for messaging functionality.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'messaging-service';
|
||||
/**
|
||||
* Disable default client timeout from bridge by setting the timeout to false.
|
||||
* @type {number|boolean}
|
||||
*/
|
||||
const TIMEOUT = false;
|
||||
|
||||
/**
|
||||
* Reference to active bridge client instance.
|
||||
* @type {Client}
|
||||
*/
|
||||
var client;
|
||||
|
||||
/**
|
||||
* Unique identifier of app instance where client resides in.
|
||||
* @type {string}
|
||||
*/
|
||||
var appInstanceId;
|
||||
|
||||
var MessagingClient = {
|
||||
/**
|
||||
* Initializes connection to MessagingService hosted in a SharedWorker.
|
||||
* @param {string} applicationInstanceId Unique identifier of app instance
|
||||
* where client resides in.
|
||||
*/
|
||||
init(applicationInstanceId) {
|
||||
if (!applicationInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
var serviceEndpoint = new SharedWorker(
|
||||
'/services/js/messaging/messaging_service.js'
|
||||
);
|
||||
|
||||
appInstanceId = applicationInstanceId;
|
||||
|
||||
client = bridge.client({
|
||||
service: SERVICE_NAME,
|
||||
endpoint: serviceEndpoint,
|
||||
timeout: TIMEOUT
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an SMS based on the specified parameter object.
|
||||
* @param {Object} options Options object that contains :-
|
||||
* - recipients (string or array of string): contains the list of
|
||||
* recipients for this message
|
||||
* - content (string): the message's body
|
||||
* - serviceId (optional long or string): the SIM serviceId we use to send
|
||||
* the message
|
||||
* - onsuccess (optional function): will be called when one SMS has been
|
||||
* sent successfully, with the request's result as argument. Can be called
|
||||
* several times.
|
||||
* - onerror (optional function): will be called when one SMS transmission
|
||||
* failed, with the error object as argument. Can be called several times.
|
||||
* - oncomplete (optional function): will be called when all messages have
|
||||
* been sent. Its argument will have the following properties:
|
||||
* + hasError (boolean): whether we had at least one error
|
||||
* + return (array): each item is an object with the following properties:
|
||||
* . success (boolean): whether this is a success or an error
|
||||
* . result (request's result): the request's result object
|
||||
* . recipient (string): the recipient used for this transmission.
|
||||
* @return {Promise}
|
||||
*/
|
||||
sendSMS(options) {
|
||||
return client.method('sendSMS', options, appInstanceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends an MMS based on the specified parameter object.
|
||||
* @param {Object} options Options object is the options field with the
|
||||
* following properties:
|
||||
* - recipients (string or array of string): recipients for this message.
|
||||
* - subject (optional string): subject for this message.
|
||||
* - content (array of SMIL slides): this is the content for the message
|
||||
* (see ConversationView for more information).
|
||||
* - serviceId (optional long or string): the SIM that should be used for
|
||||
* sending this message. If this is not the current default configuration
|
||||
* for sending MMS, then we'll first switch the configuration to this
|
||||
* serviceId, and only then send the message. That means that the
|
||||
* "sending" event will come quite late in this case.
|
||||
* - onsuccess (optional func): called only once, even for several
|
||||
* recipients, when the message is successfully sent.
|
||||
* - onerror (optional func): called only once if there is an error.
|
||||
* @return {Promise}
|
||||
*/
|
||||
sendMMS(options) {
|
||||
return client.method('sendMMS', options, appInstanceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resends message with the given opts.
|
||||
* Takes a formatted message in case you happen to have one.
|
||||
* @param {Object} message The message object containing all the information
|
||||
* depending on the type of the message which can be 'sms' or 'mms'.
|
||||
* @return {Promise}
|
||||
*/
|
||||
resendMessage(message) {
|
||||
return client.method('resendMessage', message, appInstanceId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the MMS object having a given id.
|
||||
* @param {number} id The id of the MMS we wish to get.
|
||||
* @return {Promise<MmsMessage>} A Promise object that handles the success
|
||||
* or error of the operation.
|
||||
*/
|
||||
retrieveMMS(id) {
|
||||
return client.method('retrieveMMS', id, appInstanceId);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MessagingClient = Object.freeze(MessagingClient);
|
||||
|
||||
})(window);
|
||||
197
gaia/apps/sms/services/js/messaging/messaging_service.js
Executable file
197
gaia/apps/sms/services/js/messaging/messaging_service.js
Executable file
|
|
@ -0,0 +1,197 @@
|
|||
/* global bridge,
|
||||
BridgeServiceMixin,
|
||||
MozMobileMessageClient,
|
||||
Settings,
|
||||
SMIL
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
if (!('BridgeServiceMixin' in self)) {
|
||||
importScripts('/services/js/bridge_service_mixin.js');
|
||||
}
|
||||
|
||||
if (!('MozMobileMessageClient' in self)) {
|
||||
importScripts('/services/js/moz_mobile_message/moz_mobile_message_client.js');
|
||||
}
|
||||
|
||||
(function(exports) {
|
||||
|
||||
const SERVICE_NAME = 'messaging-service';
|
||||
|
||||
const METHODS = Object.freeze([
|
||||
'sendSMS', 'sendMMS', 'resendMessage', 'retrieveMMS'
|
||||
]);
|
||||
|
||||
function ensureBridge() {
|
||||
if (!('bridge' in self) || !('service' in bridge)) {
|
||||
importScripts('/lib/bridge/service.js');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 is a valid value so we need to take care to not consider it as a falsy
|
||||
* value. We want to return null for anything that's not a number or a
|
||||
* string containing a number.
|
||||
*/
|
||||
function sanitizeServiceId(serviceId) {
|
||||
if (serviceId == null || // null or undefined
|
||||
isNaN(+serviceId)) {
|
||||
serviceId = null;
|
||||
} else {
|
||||
serviceId = +serviceId;
|
||||
}
|
||||
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
function getSendOptionsFromServiceId(serviceId) {
|
||||
var sendOpts;
|
||||
|
||||
if (serviceId != null && // not null, not undefined
|
||||
Settings.hasSeveralSim()) {
|
||||
sendOpts = { serviceId: serviceId };
|
||||
}
|
||||
|
||||
return sendOpts;
|
||||
}
|
||||
|
||||
var MessagingService = {
|
||||
/**
|
||||
* Initializes our service using the Service mixin's initService and
|
||||
* requests mozMobileMessage shim as client.
|
||||
*/
|
||||
init() {
|
||||
ensureBridge();
|
||||
|
||||
this.initService();
|
||||
},
|
||||
|
||||
/**
|
||||
* Send sms by using send API shim.
|
||||
* @param {String|String[]} recipients Contains the list of recipients for
|
||||
* this message.
|
||||
* @param {String} content The message's body.
|
||||
* @param {Number|String} serviceId The SIM serviceId we use to send the
|
||||
* message.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} Promise with array of the request result.
|
||||
*/
|
||||
sendSMS({ recipients, content, serviceId }, appInstanceId) {
|
||||
recipients = recipients || [];
|
||||
serviceId = sanitizeServiceId(serviceId);
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
var sendOpts = getSendOptionsFromServiceId(serviceId);
|
||||
var mobileMessageClient = MozMobileMessageClient.forApp(appInstanceId);
|
||||
|
||||
var results = recipients.map((recipient) => {
|
||||
return mobileMessageClient.send(recipient, content, sendOpts).then(
|
||||
(message) => ({ message: message }), (error) => ({ error: error })
|
||||
);
|
||||
});
|
||||
|
||||
return Promise.all(results).then((values) =>
|
||||
values.map((result, idx) => ({
|
||||
success: !result.error,
|
||||
result: result.message,
|
||||
code: result.error,
|
||||
recipient: recipients[idx]
|
||||
}))
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Send mms by using sendMMS API shim.
|
||||
* @param {String|String[]} recipients Recipients for this message.
|
||||
* @param {String} subject Subject for this message.
|
||||
* @param {Object[]} content This is the content for the message (
|
||||
* see ConversationView for more information).
|
||||
* @param {Number|String} serviceId The SIM that should be used for
|
||||
* sending this message. If this is not the current default configuration
|
||||
* for sending MMS, then we'll first switch the configuration to this
|
||||
* serviceId, and only then send the message. That means that the
|
||||
* "sending" event will come quite late in this case.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} DOMRequest promise with success result or error.
|
||||
*/
|
||||
sendMMS({ recipients, content, subject, serviceId }, appInstanceId) {
|
||||
serviceId = sanitizeServiceId(serviceId);
|
||||
|
||||
if (!Array.isArray(recipients)) {
|
||||
recipients = [recipients];
|
||||
}
|
||||
|
||||
var message = SMIL.generate(content);
|
||||
|
||||
var sendOpts = getSendOptionsFromServiceId(serviceId);
|
||||
|
||||
return MozMobileMessageClient.forApp(appInstanceId).sendMMS({
|
||||
receivers: recipients,
|
||||
subject: subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
},
|
||||
|
||||
/**
|
||||
* Resend a message by send API shim with existing message information.
|
||||
* @param {Message} message Target message that need to be resent.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} DOMRequest promise with success result or error.
|
||||
*/
|
||||
resendMessage(message, appInstanceId) {
|
||||
if (!message) {
|
||||
throw new Error('Message to resend is not defined.');
|
||||
}
|
||||
|
||||
var serviceId = Settings.getServiceIdByIccId(message.iccId);
|
||||
var sendOpts = getSendOptionsFromServiceId(serviceId);
|
||||
var request;
|
||||
|
||||
var mobileMessageClient = MozMobileMessageClient.forApp(appInstanceId);
|
||||
|
||||
if (message.type === 'sms') {
|
||||
request = mobileMessageClient.send(
|
||||
message.receiver, message.body, sendOpts
|
||||
);
|
||||
} else if (message.type === 'mms') {
|
||||
request = mobileMessageClient.sendMMS({
|
||||
receivers: message.receivers,
|
||||
subject: message.subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, sendOpts);
|
||||
}
|
||||
|
||||
return mobileMessageClient.delete(message.id).then(() => request);
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieve a MMS by retrieveMMS API shim by message ID.
|
||||
* @param {Number} id mms message id for retrieval.
|
||||
* @param {appInstanceId} appInstanceId Unique id of the app instance
|
||||
* requesting service.
|
||||
* @returns {Promise} DOMRequest promise with success result or error.
|
||||
*/
|
||||
retrieveMMS(id, appInstanceId) {
|
||||
return MozMobileMessageClient.forApp(appInstanceId).retrieveMMS(id);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MessagingService = BridgeServiceMixin.mixin(
|
||||
MessagingService,
|
||||
SERVICE_NAME,
|
||||
{ methods: METHODS }
|
||||
);
|
||||
|
||||
if (!self.document) {
|
||||
MessagingService.init();
|
||||
}
|
||||
|
||||
})(self);
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
/* global bridge,
|
||||
BroadcastChannel */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service that is responsible for messaging functionality.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-mobile-connections-shim';
|
||||
|
||||
/**
|
||||
* Disable default client timeout from bridge by setting the timeout to false.
|
||||
* @type {number|boolean}
|
||||
*/
|
||||
const TIMEOUT = false;
|
||||
|
||||
/**
|
||||
* Reference to active bridge client instance.
|
||||
*
|
||||
* @type {Client}
|
||||
*/
|
||||
var client;
|
||||
|
||||
var MozMobileConnectionsClient = {
|
||||
/**
|
||||
* Initializes connection to mozMobileConnectionsShim hosted in iframe.
|
||||
* @param {string} appInstanceId Unique identifier of app instance
|
||||
* where client resides in.
|
||||
*/
|
||||
init(appInstanceId) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
var broadcastChannelName = `${SERVICE_NAME}-channel-${appInstanceId}`;
|
||||
|
||||
client = bridge.client({
|
||||
service: SERVICE_NAME,
|
||||
endpoint: new BroadcastChannel(broadcastChannelName),
|
||||
timeout: TIMEOUT
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Switch the default MMS and data to SIM that matches the given iccId.
|
||||
* @param {string} iccId Id that could map to the target SIM.
|
||||
* @returns {Promise} Promise of shim's method that returned when sim
|
||||
* switching is ready or failed.
|
||||
*/
|
||||
switchMmsSimHandler(iccId) {
|
||||
return client.method('switchMmsSimHandler', iccId);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozMobileConnectionsClient = Object.freeze(
|
||||
MozMobileConnectionsClient
|
||||
);
|
||||
|
||||
})(window);
|
||||
106
gaia/apps/sms/services/js/moz_mobile_connections/moz_mobile_connections_shim.js
Executable file
106
gaia/apps/sms/services/js/moz_mobile_connections/moz_mobile_connections_shim.js
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
/* global BridgeServiceMixin,
|
||||
BroadcastChannel,
|
||||
MozSettingsShim
|
||||
*/
|
||||
|
||||
/* exported MozMobileConnectionsShim */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service for mozMobileConnections API shim.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-mobile-connections-shim';
|
||||
|
||||
/**
|
||||
* Array of method names that need to be exposed for API shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['getServiceIdByIccId', 'switchMmsSimHandler']);
|
||||
|
||||
var mozMobileConnections = null;
|
||||
|
||||
function setMmsSimServiceId(id) {
|
||||
// DSDS: mms & data are both necessary for connection switch.
|
||||
MozSettingsShim.set({
|
||||
'ril.mms.defaultServiceId': id,
|
||||
'ril.data.defaultServiceId': id
|
||||
});
|
||||
}
|
||||
|
||||
var MozMobileConnectionsShim = {
|
||||
init(appInstanceId, conns) {
|
||||
if (!conns) {
|
||||
console.error('Invalid mozMobileConnections for shim initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
mozMobileConnections = conns;
|
||||
|
||||
this.initService(
|
||||
new BroadcastChannel(`${SERVICE_NAME}-channel-${appInstanceId}`)
|
||||
);
|
||||
},
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Shim for returning the service id that matches the icc id.
|
||||
* @param {string} iccId Id that could map to the target SIM.
|
||||
* @returns {?number} matched service id or null if unmatched.
|
||||
*/
|
||||
getServiceIdByIccId(iccId) {
|
||||
var index = Array.prototype.findIndex.call(mozMobileConnections, (conn) =>
|
||||
iccId === conn.iccId
|
||||
);
|
||||
|
||||
return index > -1 ? index : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Switching the default mms/data service id by icc id.
|
||||
* @param {string} iccId Id that could map to the target SIM.
|
||||
* @returns {Promise} Promise that returned when sim switching is ready or
|
||||
* failed.
|
||||
*/
|
||||
switchMmsSimHandler(iccId) {
|
||||
var serviceId = this.getServiceIdByIccId(iccId);
|
||||
|
||||
if (serviceId === null) {
|
||||
return Promise.reject('NoSimCardError');
|
||||
}
|
||||
|
||||
var conn = mozMobileConnections[serviceId];
|
||||
|
||||
if (conn.data.state === 'registered') {
|
||||
// Call resolve directly if state is registered already
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise(function(resolve) {
|
||||
// Listen to MobileConnections datachange to make sure we can start
|
||||
// to retrieve mms only when data.state is registered. But we can't
|
||||
// guarantee datachange event will work in other device.
|
||||
conn.addEventListener('datachange', function onDataChange() {
|
||||
if (conn.data.state === 'registered') {
|
||||
conn.removeEventListener('datachange', onDataChange);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
setMmsSimServiceId(serviceId);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozMobileConnectionsShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
MozMobileConnectionsShim,
|
||||
SERVICE_NAME,
|
||||
{ methods: METHODS }
|
||||
)
|
||||
);
|
||||
|
||||
}(this));
|
||||
97
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_client.js
Executable file
97
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_client.js
Executable file
|
|
@ -0,0 +1,97 @@
|
|||
/*global bridge,
|
||||
BroadcastChannel,
|
||||
streamClient
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
(function(exports) {
|
||||
const debug = 0 ?
|
||||
(arg1, ...args) => console.log(`[MozMobileMessageClient] ${arg1}`, ...args):
|
||||
() => {};
|
||||
|
||||
const priv = {
|
||||
bridgeClient: Symbol('bridgeClient')
|
||||
};
|
||||
|
||||
/**
|
||||
* Disable default client timeout from bridge by setting the timeout to false.
|
||||
* @type {number|boolean}
|
||||
*/
|
||||
const TIMEOUT = false;
|
||||
|
||||
function MobileMessageClient(endpoint) {
|
||||
if (!('bridge' in self) || !('client' in bridge)) {
|
||||
importScripts('/lib/bridge/client.js');
|
||||
}
|
||||
|
||||
if (!('streamClient' in self)) {
|
||||
importScripts('/lib/bridge/plugins/stream/client.js');
|
||||
}
|
||||
|
||||
this[priv.bridgeClient] = bridge.client({
|
||||
service: 'moz-mobile-message-shim',
|
||||
endpoint: endpoint,
|
||||
timeout: TIMEOUT
|
||||
}).plugin(streamClient);
|
||||
}
|
||||
|
||||
MobileMessageClient.prototype = {
|
||||
retrieveMMS(id) {
|
||||
return this[priv.bridgeClient].method('retrieveMMS', id);
|
||||
},
|
||||
|
||||
getThreads() {
|
||||
return this[priv.bridgeClient].stream('getThreads');
|
||||
},
|
||||
|
||||
send(recipient, content, sendOpts) {
|
||||
return this[priv.bridgeClient].method(
|
||||
'send', recipient, content, sendOpts
|
||||
);
|
||||
},
|
||||
|
||||
sendMMS(dataOpts, sendOpts) {
|
||||
return this[priv.bridgeClient].method('sendMMS', dataOpts, sendOpts);
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return this[priv.bridgeClient].method('delete', id);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* List of MobileMessageClient mapped to specific application id.
|
||||
* @type {Map.<string, MobileMessageClient>}
|
||||
*/
|
||||
const mobileMessageClients = new Map();
|
||||
|
||||
exports.MozMobileMessageClient = Object.freeze({
|
||||
forApp(appInstanceId) {
|
||||
if (!appInstanceId) {
|
||||
throw new Error('AppInstanceId is required!');
|
||||
}
|
||||
|
||||
var mobileMessageClient = mobileMessageClients.get(appInstanceId);
|
||||
|
||||
if (!mobileMessageClient) {
|
||||
mobileMessageClient = new MobileMessageClient(
|
||||
new BroadcastChannel(
|
||||
'moz-mobile-message-shim-channel-' + appInstanceId
|
||||
)
|
||||
);
|
||||
|
||||
mobileMessageClients.set(appInstanceId, mobileMessageClient);
|
||||
|
||||
debug(
|
||||
'Create MobileMessageClient for app instance %s', appInstanceId
|
||||
);
|
||||
}
|
||||
|
||||
return mobileMessageClient;
|
||||
},
|
||||
|
||||
cleanup() {
|
||||
mobileMessageClients.clear();
|
||||
}
|
||||
});
|
||||
})(self);
|
||||
276
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_shim.js
Executable file
276
gaia/apps/sms/services/js/moz_mobile_message/moz_mobile_message_shim.js
Executable file
|
|
@ -0,0 +1,276 @@
|
|||
/* global BridgeServiceMixin,
|
||||
BroadcastChannel
|
||||
*/
|
||||
|
||||
/* exported MozMobileMessageShim */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
const debug = 0 ?
|
||||
(arg1, ...args) => console.log(`[MozMobileMessageShim] ${arg1}`, ...args):
|
||||
() => {};
|
||||
|
||||
const mark = 0 ?
|
||||
(...args) => exports.performance.mark(`[MozMobileMessageShim] ${args}`):
|
||||
() => {};
|
||||
|
||||
/**
|
||||
* Name of the service for mozMobileMessage API shim.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-mobile-message-shim';
|
||||
|
||||
/**
|
||||
* Event set that key is corresponding to mozMobileMessage events and value is
|
||||
* the name that broadcasts to client side.
|
||||
* @type {Object.<string, string>}
|
||||
*/
|
||||
const EVENTS = Object.freeze({
|
||||
'received': 'message-received',
|
||||
'sending': 'message-sending',
|
||||
'sent': 'message-sent',
|
||||
'failed': 'message-failed-to-send',
|
||||
'deleted': 'threads-deleted',
|
||||
'readsuccess': 'message-read',
|
||||
'deliverysuccess': 'message-delivered'
|
||||
});
|
||||
|
||||
/**
|
||||
* Array of method names that need to be exposed for API shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['getMessage', 'retrieveMMS', 'send', 'sendMMS',
|
||||
'delete', 'markMessageRead', 'getSegmentInfoForText']);
|
||||
|
||||
/**
|
||||
* Array of stream names that need to return data (messages/threads) in chunk.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const STREAMS = Object.freeze(['getThreads', 'getMessages']);
|
||||
|
||||
var mozMobileMessage = null;
|
||||
|
||||
/**
|
||||
* Clones DOM thread object to plain JS object, so that it's possible to pass it
|
||||
* via postMessage. Should be removed once bug 1172794 is landed.
|
||||
* @param {DOMMozMobileMessageThread} thread DOM Message thread instance.
|
||||
* @returns {Object}
|
||||
*/
|
||||
function cloneThread(thread) {
|
||||
// See nsIDOMMozMobileMessageThread.idl file at:
|
||||
// http://mxr.mozilla.org/mozilla-central/source/dom/mobilemessage/interfaces
|
||||
return {
|
||||
id: thread.id,
|
||||
body: thread.body,
|
||||
participants: thread.participants,
|
||||
timestamp: thread.timestamp,
|
||||
unreadCount: thread.unreadCount,
|
||||
lastMessageType: thread.lastMessageType
|
||||
};
|
||||
}
|
||||
|
||||
var MozMobileMessageShim = {
|
||||
init(appInstanceId, mobileMessage) {
|
||||
if (!mobileMessage) {
|
||||
return;
|
||||
}
|
||||
|
||||
function capitalize(str) {
|
||||
return str[0].toUpperCase() + str.slice(1);
|
||||
}
|
||||
|
||||
var broadcastChannelName = `${SERVICE_NAME}-channel-${appInstanceId}`;
|
||||
mozMobileMessage = mobileMessage;
|
||||
this.initService(new BroadcastChannel(broadcastChannelName));
|
||||
|
||||
Object.keys(EVENTS).forEach((event) => {
|
||||
mozMobileMessage.addEventListener(
|
||||
event,
|
||||
this['on' + capitalize(event)].bind(this)
|
||||
);
|
||||
});
|
||||
|
||||
debug(
|
||||
'Listen incoming connections on "%s" broadcast channel',
|
||||
broadcastChannelName
|
||||
);
|
||||
},
|
||||
|
||||
/* Events */
|
||||
|
||||
onSending(e) {
|
||||
this.broadcast('message-sending', { message: e.message });
|
||||
},
|
||||
|
||||
onFailed(e) {
|
||||
this.broadcast('message-failed-to-send', { message: e.message });
|
||||
},
|
||||
|
||||
onDeliverysuccess(e) {
|
||||
this.broadcast('message-delivered', { message: e.message });
|
||||
},
|
||||
|
||||
onReadsuccess(e) {
|
||||
this.broadcast('message-read', { message: e.message });
|
||||
},
|
||||
|
||||
onSent(e) {
|
||||
this.broadcast('message-sent', { message: e.message });
|
||||
},
|
||||
|
||||
onReceived(e) {
|
||||
this.broadcast('message-received', { message: e.message });
|
||||
},
|
||||
|
||||
onDeleted(e) {
|
||||
if (e.deletedThreadIds && e.deletedThreadIds.length) {
|
||||
this.broadcast('threads-deleted', {
|
||||
ids: e.deletedThreadIds
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/* Methods */
|
||||
|
||||
getMessage(id) {
|
||||
return mozMobileMessage.getMessage(id);
|
||||
},
|
||||
|
||||
/**
|
||||
* Call platform retrieveMMS API to retrieve the MMS by ID.
|
||||
* @param {Number} id MMS message id.
|
||||
* @returns {Promise.<void|Error>} return void if MMS is retrieved
|
||||
* successfully or error while failed.
|
||||
*/
|
||||
retrieveMMS(id) {
|
||||
// Return void instead of message to avoid clone error issue. Remove once
|
||||
// bug 1172794 is resolved.
|
||||
return mozMobileMessage.retrieveMMS(id).then(() => {});
|
||||
},
|
||||
|
||||
send(recipient, content, sendOpts) {
|
||||
return mozMobileMessage.send(recipient, content, sendOpts);
|
||||
},
|
||||
|
||||
sendMMS(dataOpts, sendOpts) {
|
||||
return mozMobileMessage.sendMMS(dataOpts, sendOpts);
|
||||
},
|
||||
|
||||
delete(id) {
|
||||
return mozMobileMessage.delete(id);
|
||||
},
|
||||
|
||||
markMessageRead(id, isRead, sendReadReport) {
|
||||
return mozMobileMessage.markMessageRead(id, isRead, sendReadReport);
|
||||
},
|
||||
|
||||
getSegmentInfoForText(text) {
|
||||
return mozMobileMessage.getSegmentInfoForText(text);
|
||||
},
|
||||
|
||||
/* Streams */
|
||||
|
||||
/**
|
||||
* Stream wrapper for getThreads API for returning threads in chunk.
|
||||
* @param {ServiceStream} stream Channel for returning thread.
|
||||
*/
|
||||
getThreads(stream) {
|
||||
mark('start retrieving conversations');
|
||||
|
||||
var cursor = null;
|
||||
var isStreamCancelled = false;
|
||||
|
||||
stream.cancel = (reason) => {
|
||||
debug('getThreads stream is cancelled: %s', reason);
|
||||
isStreamCancelled = true;
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
// WORKAROUND for bug 958738. We can remove 'try\catch' block once this bug
|
||||
// is resolved
|
||||
try {
|
||||
cursor = mozMobileMessage.getThreads();
|
||||
} catch(e) {
|
||||
console.error('Error occurred while retrieving threads:', e);
|
||||
// Pass error object once the following issue is fixed:
|
||||
// https://github.com/gaia-components/threads/issues/74
|
||||
stream.abort(`[${e.name}] ${e.message || ''}`);
|
||||
return;
|
||||
}
|
||||
|
||||
var index = 0;
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
var result = this.result;
|
||||
|
||||
if (result && !isStreamCancelled) {
|
||||
mark(++index, ' conversation retrieved');
|
||||
stream.write(cloneThread(result));
|
||||
this.continue();
|
||||
return;
|
||||
}
|
||||
|
||||
mark('all conversations retrieved');
|
||||
|
||||
stream.close();
|
||||
};
|
||||
|
||||
cursor.onerror = function onerror() {
|
||||
console.error('Error occurred while reading the database', this.error);
|
||||
|
||||
// Pass error object once the following issue is fixed:
|
||||
// https://github.com/gaia-components/threads/issues/74
|
||||
stream.abort(`[${this.error.name}] ${this.error.message || ''}`);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Stream wrapper for getMessages API for returning messages in chunk.
|
||||
* @param {ServiceStream} stream Channel for returning message.
|
||||
* @param {*} options Options for getMessages API.
|
||||
*/
|
||||
getMessages(stream, options) {
|
||||
/*
|
||||
options {
|
||||
filter: a MobileMessageFilter or similar object
|
||||
invert: option to invert the selection
|
||||
}
|
||||
|
||||
*/
|
||||
var invert = options.invert;
|
||||
var filter = options.filter;
|
||||
var cursor = mozMobileMessage.getMessages(filter, !invert);
|
||||
|
||||
cursor.onsuccess = function onsuccess() {
|
||||
if (!this.done) {
|
||||
stream.write(this.result);
|
||||
this.continue();
|
||||
} else {
|
||||
stream.close();
|
||||
}
|
||||
};
|
||||
|
||||
cursor.onerror = function onerror() {
|
||||
var msg = 'Reading the database. Error: ' + this.error.name;
|
||||
console.error(msg);
|
||||
stream.abort();
|
||||
};
|
||||
|
||||
stream.cancel = function() {
|
||||
stream.close();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozMobileMessageShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(
|
||||
MozMobileMessageShim,
|
||||
SERVICE_NAME, {
|
||||
methods: METHODS,
|
||||
streams: STREAMS,
|
||||
events: Object.keys(EVENTS).map((key) => EVENTS[key])
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
}(this));
|
||||
59
gaia/apps/sms/services/js/moz_settings/moz_settings_shim.js
Executable file
59
gaia/apps/sms/services/js/moz_settings/moz_settings_shim.js
Executable file
|
|
@ -0,0 +1,59 @@
|
|||
/* global BridgeServiceMixin */
|
||||
|
||||
/* exported MozSettingsShim */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Name of the service for mozSettings API shim.
|
||||
* @type {string}
|
||||
*/
|
||||
const SERVICE_NAME = 'moz-settings-shim';
|
||||
|
||||
/**
|
||||
* Array of method names that need to be exposed for API shim.
|
||||
* @type {Array.<string>}
|
||||
*/
|
||||
const METHODS = Object.freeze(['get', 'set']);
|
||||
|
||||
var mozSettings = null;
|
||||
|
||||
var MozSettingsShim = {
|
||||
init(settings) {
|
||||
if (!settings) {
|
||||
console.error('Invalid settings for shim initialization');
|
||||
return;
|
||||
}
|
||||
mozSettings = settings;
|
||||
|
||||
this.initService();
|
||||
},
|
||||
|
||||
/* Methods */
|
||||
|
||||
/**
|
||||
* Shim for mozSettings.createLock().get API.
|
||||
* @param {String} key Key name for Settings.
|
||||
* @returns {Promise} Promise that return the settings value from settings DB.
|
||||
*/
|
||||
get(key) {
|
||||
return mozSettings.createLock().get(key);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shim for mozSettings.createLock().set API.
|
||||
* @param {Object} settings Settings key and value for settings. It can have
|
||||
* several key/value pairs inside.
|
||||
* @returns {Promise} Promise that return saved result.
|
||||
*/
|
||||
set(settings) {
|
||||
return mozSettings.createLock().set(settings);
|
||||
}
|
||||
};
|
||||
|
||||
exports.MozSettingsShim = Object.seal(
|
||||
BridgeServiceMixin.mixin(MozSettingsShim, SERVICE_NAME, { methods: METHODS })
|
||||
);
|
||||
|
||||
}(this));
|
||||
177
gaia/apps/sms/services/js/threads.js
Executable file
177
gaia/apps/sms/services/js/threads.js
Executable file
|
|
@ -0,0 +1,177 @@
|
|||
/*global Drafts */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
var threads = new Map();
|
||||
var messageMap = new Map();
|
||||
|
||||
function Thread(thread) {
|
||||
var length = Thread.FIELDS.length;
|
||||
var key;
|
||||
|
||||
for (var i = 0; i < length; i++) {
|
||||
key = Thread.FIELDS[i];
|
||||
this[key] = thread[key];
|
||||
}
|
||||
|
||||
this.messages = new Map();
|
||||
}
|
||||
|
||||
Thread.prototype.getDraft = function() {
|
||||
return this.isDraft ?
|
||||
Drafts.byDraftId(this.id) : Drafts.byThreadId(this.id);
|
||||
};
|
||||
|
||||
Thread.FIELDS = [
|
||||
'body', 'id', 'lastMessageSubject', 'lastMessageType',
|
||||
'participants', 'timestamp', 'unreadCount', 'isDraft'
|
||||
];
|
||||
|
||||
Thread.fromMessage = function(record, options) {
|
||||
var participants = [];
|
||||
|
||||
if (typeof record.delivery !== 'undefined') {
|
||||
if (record.delivery === 'received' ||
|
||||
record.delivery === 'not-downloaded') {
|
||||
participants = [record.sender];
|
||||
} else {
|
||||
participants = record.receivers || [record.receiver];
|
||||
}
|
||||
}
|
||||
|
||||
return new Thread({
|
||||
id: record.threadId,
|
||||
participants: participants,
|
||||
body: record.body,
|
||||
timestamp: record.timestamp,
|
||||
unreadCount: (options && options.unread) ? 1 : 0,
|
||||
lastMessageType: record.type || 'sms',
|
||||
isDraft: false
|
||||
});
|
||||
};
|
||||
|
||||
Thread.fromDraft = function(draft, options) {
|
||||
var participants = draft.recipients && draft.recipients.length ?
|
||||
draft.recipients : [''];
|
||||
|
||||
var body = draft.content && draft.content.length ?
|
||||
draft.content.find(function(content) {
|
||||
if (typeof content === 'string') {
|
||||
return true;
|
||||
}
|
||||
}) : '';
|
||||
|
||||
return new Thread({
|
||||
id: draft.threadId || draft.id,
|
||||
participants: participants,
|
||||
body: body,
|
||||
timestamp: new Date(draft.timestamp),
|
||||
unreadCount: (options && options.unread) ? 1 : 0,
|
||||
lastMessageType: draft.type || 'sms',
|
||||
isDraft: true
|
||||
});
|
||||
};
|
||||
|
||||
Thread.create = function(record, options) {
|
||||
if (record instanceof Thread) {
|
||||
return record;
|
||||
}
|
||||
return record.delivery ?
|
||||
Thread.fromMessage(record, options) :
|
||||
Thread.fromDraft(record, options);
|
||||
};
|
||||
|
||||
var Threads = exports.Threads = {
|
||||
registerMessage: function(message) {
|
||||
var threadId = +message.threadId;
|
||||
var messageId = +message.id;
|
||||
|
||||
if (!this.has(threadId)) {
|
||||
this.set(threadId, Thread.create(message));
|
||||
}
|
||||
this.get(threadId).messages.set(messageId, message);
|
||||
messageMap.set(messageId, threadId);
|
||||
},
|
||||
unregisterMessage: function(id) {
|
||||
id = +id;
|
||||
var threadId = messageMap.get(id);
|
||||
|
||||
if (this.has(threadId)) {
|
||||
this.get(threadId).messages.delete(id);
|
||||
}
|
||||
messageMap.delete(id);
|
||||
},
|
||||
set: function(id, thread) {
|
||||
var old, length, key;
|
||||
id = +id;
|
||||
if (threads.has(id)) {
|
||||
// Updates the reference
|
||||
old = threads.get(id);
|
||||
length = Thread.FIELDS.length;
|
||||
for (var i = 0; i < length; i++) {
|
||||
key = Thread.FIELDS[i];
|
||||
old[key] = thread[key];
|
||||
}
|
||||
|
||||
return threads;
|
||||
}
|
||||
return threads.set(id, new Thread(thread));
|
||||
},
|
||||
get: function(id) {
|
||||
return threads.get(+id);
|
||||
},
|
||||
has: function(id) {
|
||||
return threads.has(+id);
|
||||
},
|
||||
delete: function(id) {
|
||||
id = +id;
|
||||
|
||||
var thread = this.get(id);
|
||||
|
||||
if (!thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
threads.delete(id);
|
||||
|
||||
var draft = thread.getDraft();
|
||||
if (draft) {
|
||||
Drafts.delete(draft).store();
|
||||
}
|
||||
},
|
||||
clear: function() {
|
||||
threads = new Map();
|
||||
},
|
||||
forEach: function(callback) {
|
||||
threads.forEach(function(v, k) {
|
||||
callback(v, k);
|
||||
});
|
||||
},
|
||||
keys: function() {
|
||||
return threads.keys();
|
||||
},
|
||||
get size() {
|
||||
// support: gecko 18 - size might be a function
|
||||
if (typeof threads.size === 'function') {
|
||||
return +threads.size();
|
||||
}
|
||||
return +threads.size;
|
||||
}
|
||||
};
|
||||
|
||||
Threads.Messages = {
|
||||
get(id) {
|
||||
id = +id;
|
||||
var threadId = messageMap.get(id);
|
||||
|
||||
if (Threads.has(threadId)) {
|
||||
return Threads.get(threadId).messages.get(id);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
exports.Thread = Thread;
|
||||
}(this));
|
||||
3
gaia/apps/sms/services/test/unit/.jshintrc
Executable file
3
gaia/apps/sms/services/test/unit/.jshintrc
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../../../test/unit/.jshintrc"
|
||||
}
|
||||
221
gaia/apps/sms/services/test/unit/activity/activity_client_test.js
Executable file
221
gaia/apps/sms/services/test/unit/activity/activity_client_test.js
Executable file
|
|
@ -0,0 +1,221 @@
|
|||
/*global ActivityClient,
|
||||
BroadcastChannel,
|
||||
MocksHelper,
|
||||
bridge
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/views/shared/test/unit/mock_broadcast_channel.js');
|
||||
require('/shared/js/event_dispatcher.js');
|
||||
require('/services/js/activity/activity_client.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'BroadcastChannel'
|
||||
]).init();
|
||||
|
||||
suite('ActivityClient >', function() {
|
||||
const APP_INSTANCE_ID = 'fake-app-id';
|
||||
|
||||
var clientStub;
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
clientStub = sinon.stub({
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
method: () => {}
|
||||
});
|
||||
|
||||
this.sinon.stub(bridge, 'client').returns(clientStub);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
ActivityClient.offAll();
|
||||
});
|
||||
|
||||
suite('init()', function() {
|
||||
test('throws if app instance id is not provided', function() {
|
||||
assert.throws(() => ActivityClient.init());
|
||||
});
|
||||
|
||||
test('bridge client is correctly initialized', function() {
|
||||
ActivityClient.init(APP_INSTANCE_ID);
|
||||
|
||||
sinon.assert.calledOnce(bridge.client);
|
||||
sinon.assert.calledWith(bridge.client, {
|
||||
service: 'activity-service',
|
||||
endpoint: sinon.match.instanceOf(BroadcastChannel).and(
|
||||
sinon.match.has(
|
||||
'name',
|
||||
'activity-service-channel-' + APP_INSTANCE_ID
|
||||
)
|
||||
),
|
||||
timeout: false
|
||||
});
|
||||
|
||||
assert.isFalse(ActivityClient.hasPendingRequest());
|
||||
});
|
||||
});
|
||||
|
||||
suite('throws in exceptional cases', function() {
|
||||
setup(function() {
|
||||
ActivityClient.init(APP_INSTANCE_ID);
|
||||
});
|
||||
|
||||
test('postResult throws if there is no activity request', function(done) {
|
||||
ActivityClient.postResult('Result').then(() => {
|
||||
throw new Error('postResult should be rejected!');
|
||||
}, (e) => {
|
||||
assert.instanceOf(e, Error);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('postError throws if there is no activity request', function(done) {
|
||||
ActivityClient.postError('Error').then(() => {
|
||||
throw new Error('postError should be rejected!');
|
||||
}, (e) => {
|
||||
assert.instanceOf(e, Error);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('handling incoming activity requests> ', function() {
|
||||
setup(function() {
|
||||
ActivityClient.init(APP_INSTANCE_ID);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
ActivityClient.offAll();
|
||||
});
|
||||
|
||||
test('post error on concurrent request attempt', function() {
|
||||
var onShareActivityRequest = sinon.stub();
|
||||
|
||||
ActivityClient.on('share-activity-request', onShareActivityRequest);
|
||||
|
||||
clientStub.on.withArgs('activity-request').yield({
|
||||
name: 'share',
|
||||
data: { body: 'text' }
|
||||
});
|
||||
|
||||
assert.isTrue(ActivityClient.hasPendingRequest());
|
||||
sinon.assert.calledOnce(onShareActivityRequest);
|
||||
|
||||
clientStub.on.withArgs('activity-request').yield({
|
||||
name: 'share',
|
||||
data: { body: 'text' }
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(onShareActivityRequest);
|
||||
sinon.assert.calledWith(
|
||||
clientStub.method, 'postError', sinon.match.string
|
||||
);
|
||||
assert.isFalse(ActivityClient.hasPendingRequest());
|
||||
});
|
||||
|
||||
test('notifies about "share" activity request', function() {
|
||||
var onShareActivityRequest = sinon.stub();
|
||||
var onNewActivityRequest = sinon.stub();
|
||||
|
||||
ActivityClient.on('share-activity-request', onShareActivityRequest);
|
||||
ActivityClient.on('new-activity-request', onNewActivityRequest);
|
||||
|
||||
clientStub.on.withArgs('activity-request').yield({
|
||||
name: 'share',
|
||||
data: { body: 'text' }
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(onNewActivityRequest);
|
||||
sinon.assert.calledOnce(onShareActivityRequest);
|
||||
sinon.assert.calledWith(onShareActivityRequest, { body: 'text' });
|
||||
});
|
||||
|
||||
test('notifies about "new" activity request', function() {
|
||||
var onShareActivityRequest = sinon.stub();
|
||||
var onNewActivityRequest = sinon.stub();
|
||||
|
||||
ActivityClient.on('share-activity-request', onShareActivityRequest);
|
||||
ActivityClient.on('new-activity-request', onNewActivityRequest);
|
||||
|
||||
clientStub.on.withArgs('activity-request').yield({
|
||||
name: 'new',
|
||||
data: { number: '+123' }
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(onShareActivityRequest);
|
||||
sinon.assert.calledOnce(onNewActivityRequest);
|
||||
sinon.assert.calledWith(onNewActivityRequest, { number: '+123' });
|
||||
});
|
||||
});
|
||||
|
||||
suite('responding to activity request >', function() {
|
||||
setup(function() {
|
||||
ActivityClient.init(APP_INSTANCE_ID);
|
||||
|
||||
clientStub.on.withArgs('activity-request').yield({
|
||||
name: 'new',
|
||||
data: { number: '+123' }
|
||||
});
|
||||
|
||||
assert.isTrue(ActivityClient.hasPendingRequest());
|
||||
});
|
||||
|
||||
test('postResult correctly handles successful method call', function(done) {
|
||||
clientStub.method.returns(Promise.resolve());
|
||||
|
||||
ActivityClient.postResult('Result').then(() => {
|
||||
sinon.assert.calledOnce(clientStub.method);
|
||||
sinon.assert.calledWith(clientStub.method, 'postResult', 'Result');
|
||||
|
||||
assert.isFalse(ActivityClient.hasPendingRequest());
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('postResult correctly handles failed method call', function(done) {
|
||||
var rejectReason = new Error('Fake reason');
|
||||
|
||||
clientStub.method.returns(Promise.reject(rejectReason));
|
||||
|
||||
ActivityClient.postResult('Result').then(() => {
|
||||
throw new Error('postResult should be rejected!');
|
||||
}, (e) => {
|
||||
sinon.assert.calledOnce(clientStub.method);
|
||||
sinon.assert.calledWith(clientStub.method, 'postResult', 'Result');
|
||||
|
||||
assert.isFalse(ActivityClient.hasPendingRequest());
|
||||
|
||||
assert.equal(e, rejectReason);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('postError correctly handles successful method call', function() {
|
||||
ActivityClient.postError('Error');
|
||||
|
||||
sinon.assert.calledOnce(clientStub.method);
|
||||
sinon.assert.calledWith(clientStub.method, 'postError', 'Error');
|
||||
|
||||
assert.isFalse(ActivityClient.hasPendingRequest());
|
||||
});
|
||||
|
||||
test('postError correctly handles failed method call', function(done) {
|
||||
var rejectReason = new Error('Fake reason');
|
||||
|
||||
clientStub.method.returns(Promise.reject(rejectReason));
|
||||
|
||||
ActivityClient.postError('Error').then(() => {
|
||||
throw new Error('postError should be rejected!');
|
||||
}, (e) => {
|
||||
sinon.assert.calledOnce(clientStub.method);
|
||||
sinon.assert.calledWith(clientStub.method, 'postError', 'Error');
|
||||
|
||||
assert.isFalse(ActivityClient.hasPendingRequest());
|
||||
|
||||
assert.equal(e, rejectReason);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
146
gaia/apps/sms/services/test/unit/activity/activity_shim_test.js
Executable file
146
gaia/apps/sms/services/test/unit/activity/activity_shim_test.js
Executable file
|
|
@ -0,0 +1,146 @@
|
|||
/*global ActivityShim,
|
||||
bridge,
|
||||
BroadcastChannel,
|
||||
MocksHelper
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/shared/js/event_dispatcher.js');
|
||||
require('/services/js/bridge_service_mixin.js');
|
||||
require('/services/js/activity/activity_shim.js');
|
||||
require('/views/shared/test/unit/mock_broadcast_channel.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'BroadcastChannel'
|
||||
]).init();
|
||||
|
||||
suite('ActivityShim >', function() {
|
||||
const APP_INSTANCE_ID = 'fake-app-id';
|
||||
|
||||
var serviceStub, setMessageHandlerStub;
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
setup(function() {
|
||||
this.sinon.useFakeTimers();
|
||||
|
||||
serviceStub = sinon.stub({
|
||||
method: () => {},
|
||||
broadcast: () => {},
|
||||
listen: () => {},
|
||||
on: () => {}
|
||||
});
|
||||
|
||||
setMessageHandlerStub = sinon.stub();
|
||||
|
||||
this.sinon.stub(bridge, 'service').returns(serviceStub);
|
||||
});
|
||||
|
||||
suite('init()', function() {
|
||||
test('throws if app instance id is not provided', function() {
|
||||
assert.throws(() => ActivityShim.init());
|
||||
});
|
||||
|
||||
test('bridge service is correctly initialized', function() {
|
||||
ActivityShim.init(APP_INSTANCE_ID, setMessageHandlerStub);
|
||||
|
||||
sinon.assert.calledOnce(bridge.service);
|
||||
sinon.assert.calledWith(bridge.service, 'activity-service');
|
||||
sinon.assert.calledTwice(serviceStub.listen);
|
||||
sinon.assert.calledWith(
|
||||
serviceStub.listen,
|
||||
sinon.match.instanceOf(BroadcastChannel).and(
|
||||
sinon.match.has(
|
||||
'name',
|
||||
'activity-service-channel-' + APP_INSTANCE_ID
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('activity system message >', function() {
|
||||
setup(function() {
|
||||
ActivityShim.init(APP_INSTANCE_ID, setMessageHandlerStub);
|
||||
});
|
||||
|
||||
test('when client is not connected yet', function() {
|
||||
sinon.assert.notCalled(setMessageHandlerStub);
|
||||
});
|
||||
|
||||
suite('once client is connected', function() {
|
||||
var activityRequest;
|
||||
|
||||
setup(function() {
|
||||
activityRequest = sinon.stub({
|
||||
postResult: () => {},
|
||||
postError: () => {},
|
||||
source: {
|
||||
name: 'fake',
|
||||
data: {
|
||||
custom: 'custom'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
serviceStub.on.withArgs('connected').yield();
|
||||
|
||||
setMessageHandlerStub.withArgs('activity').yield(activityRequest);
|
||||
});
|
||||
|
||||
test('activity request is broadcasted', function() {
|
||||
sinon.assert.calledWith(
|
||||
serviceStub.broadcast, 'activity-request', activityRequest.source
|
||||
);
|
||||
});
|
||||
|
||||
test('postResult passes default arguments', function() {
|
||||
serviceStub.method.withArgs('postResult').yield();
|
||||
|
||||
sinon.assert.calledOnce(activityRequest.postResult);
|
||||
sinon.assert.calledWith(activityRequest.postResult, { success: true });
|
||||
|
||||
// Activity request is disposed after first postResult.
|
||||
assert.throw(() => {
|
||||
serviceStub.method.withArgs('postResult').yield();
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(activityRequest.postResult);
|
||||
});
|
||||
|
||||
test('postResult passes custom arguments', function() {
|
||||
serviceStub.method.withArgs('postResult').yield({ custom: 'custom' });
|
||||
|
||||
sinon.assert.notCalled(activityRequest.postError);
|
||||
sinon.assert.calledOnce(activityRequest.postResult);
|
||||
sinon.assert.calledWith(
|
||||
activityRequest.postResult, { custom: 'custom' }
|
||||
);
|
||||
|
||||
// Activity request is disposed after first postResult.
|
||||
assert.throw(() => {
|
||||
serviceStub.method.withArgs('postResult').yield({ custom: 'custom' });
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(activityRequest.postResult);
|
||||
});
|
||||
|
||||
test('postError passes error reason', function() {
|
||||
serviceStub.method.withArgs('postError').yield('error');
|
||||
|
||||
sinon.assert.notCalled(activityRequest.postResult);
|
||||
sinon.assert.calledOnce(activityRequest.postError);
|
||||
sinon.assert.calledWith(activityRequest.postError, 'error');
|
||||
|
||||
// Activity request is disposed after first postError.
|
||||
assert.throw(() => {
|
||||
serviceStub.method.withArgs('postError').yield('error');
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(activityRequest.postError);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
13
gaia/apps/sms/services/test/unit/activity/mock_activity_client.js
Executable file
13
gaia/apps/sms/services/test/unit/activity/mock_activity_client.js
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
/* exported MockActivityClient */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.MockActivityClient = {
|
||||
init: () => {},
|
||||
postResult: () => Promise.resolve(),
|
||||
postError: () => Promise.resolve(),
|
||||
hasPendingRequest: () => false,
|
||||
on: () => {}
|
||||
};
|
||||
})(window);
|
||||
9
gaia/apps/sms/services/test/unit/activity/mock_activity_shim.js
Executable file
9
gaia/apps/sms/services/test/unit/activity/mock_activity_shim.js
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
/* exported MockActivityShim */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.MockActivityShim = {
|
||||
init: () => {}
|
||||
};
|
||||
})(window);
|
||||
107
gaia/apps/sms/services/test/unit/bridge_service_mixin_test.js
Executable file
107
gaia/apps/sms/services/test/unit/bridge_service_mixin_test.js
Executable file
|
|
@ -0,0 +1,107 @@
|
|||
/*global
|
||||
bridge,
|
||||
BridgeServiceMixin,
|
||||
MocksHelper,
|
||||
streamService
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/shared/test/unit/mocks/mocks_helper.js');
|
||||
|
||||
require('/services/js/bridge_service_mixin.js');
|
||||
|
||||
var mocksHelperForServiceMixin = new MocksHelper([
|
||||
'bridge',
|
||||
'streamService'
|
||||
]).init();
|
||||
|
||||
suite('BridgeServiceMixin >', function() {
|
||||
mocksHelperForServiceMixin.attachTestHelpers();
|
||||
|
||||
var target, service;
|
||||
|
||||
setup(function() {
|
||||
service = sinon.stub({
|
||||
method() {},
|
||||
stream() {},
|
||||
broadcast() {},
|
||||
listen() {},
|
||||
on() {},
|
||||
plugin() {}
|
||||
});
|
||||
|
||||
target = sinon.stub({
|
||||
usefulMethod() {},
|
||||
usefulStream() {}
|
||||
});
|
||||
|
||||
var methods = ['usefulMethod'];
|
||||
var streams = ['usefulStream'];
|
||||
var events = ['useful-event'];
|
||||
var name = 'my-service';
|
||||
|
||||
this.sinon.stub(bridge, 'service').withArgs(name).returns(service);
|
||||
|
||||
BridgeServiceMixin.mixin(
|
||||
target, name,
|
||||
{ methods, streams, events }
|
||||
);
|
||||
|
||||
target.initService();
|
||||
});
|
||||
|
||||
test('Service is exposed', function() {
|
||||
sinon.assert.calledWith(bridge.service, 'my-service');
|
||||
sinon.assert.calledOnce(service.listen);
|
||||
// Does not subscribe for any bridge event if target does not need it.
|
||||
sinon.assert.notCalled(service.on);
|
||||
});
|
||||
|
||||
test('Methods and streams are exposed', function() {
|
||||
service.method.withArgs('usefulMethod').yield(1);
|
||||
|
||||
sinon.assert.calledWith(target.usefulMethod, 1);
|
||||
sinon.assert.calledOn(target.usefulMethod, target);
|
||||
|
||||
sinon.assert.calledWith(service.plugin, streamService);
|
||||
|
||||
service.stream.withArgs('usefulStream').yield(2);
|
||||
sinon.assert.calledWith(target.usefulStream, 2);
|
||||
sinon.assert.calledOn(target.usefulStream, target);
|
||||
});
|
||||
|
||||
test('Events are properly checked', function() {
|
||||
var data = {};
|
||||
target.broadcast('useful-event', data);
|
||||
sinon.assert.calledWith(service.broadcast, 'useful-event', data);
|
||||
|
||||
assert.throws(() => target.broadcast('unknown-event'));
|
||||
});
|
||||
|
||||
test('subscribes to "connected" event if target has corresponding handler',
|
||||
function() {
|
||||
var newTarget = sinon.stub({
|
||||
usefulMethod() {},
|
||||
onConnected() {}
|
||||
});
|
||||
|
||||
BridgeServiceMixin.mixin(
|
||||
newTarget, 'my-service', { methods: ['usefulMethod'] }
|
||||
);
|
||||
|
||||
newTarget.initService();
|
||||
|
||||
sinon.assert.calledOnce(service.on);
|
||||
sinon.assert.calledWith(service.on, 'connected');
|
||||
sinon.assert.notCalled(newTarget.onConnected);
|
||||
|
||||
service.on.withArgs('connected').yield();
|
||||
sinon.assert.calledOnce(newTarget.onConnected);
|
||||
|
||||
service.on.withArgs('connected').yield();
|
||||
sinon.assert.calledTwice(newTarget.onConnected);
|
||||
});
|
||||
});
|
||||
|
||||
143
gaia/apps/sms/services/test/unit/conversation/conversation_client_test.js
Executable file
143
gaia/apps/sms/services/test/unit/conversation/conversation_client_test.js
Executable file
|
|
@ -0,0 +1,143 @@
|
|||
/*global bridge,
|
||||
ConversationClient,
|
||||
MocksHelper,
|
||||
streamClient,
|
||||
Thread
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/services/test/unit/mock_shared_worker.js');
|
||||
require('/services/test/unit/mock_threads.js');
|
||||
require('/services/js/conversation/conversation_client.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'SharedWorker',
|
||||
'streamClient',
|
||||
'Thread'
|
||||
]).init();
|
||||
|
||||
suite('ConversationClient >', function() {
|
||||
const APP_ID = 'fake-app-id';
|
||||
|
||||
var clientStub;
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
clientStub = sinon.stub({
|
||||
stream: () => {},
|
||||
plugin: () => {}
|
||||
});
|
||||
|
||||
clientStub.plugin.returns(clientStub);
|
||||
|
||||
this.sinon.spy(window, 'SharedWorker');
|
||||
|
||||
this.sinon.stub(bridge, 'client').returns(clientStub);
|
||||
});
|
||||
|
||||
test('throws if app instance id is not provided', function() {
|
||||
assert.throws(() => ConversationClient.init());
|
||||
});
|
||||
|
||||
test('bridge client is correctly initialized', function() {
|
||||
ConversationClient.init(APP_ID);
|
||||
|
||||
sinon.assert.calledWith(
|
||||
SharedWorker,
|
||||
'/services/js/conversation/conversation_service.js'
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(bridge.client);
|
||||
sinon.assert.calledWith(bridge.client, {
|
||||
service: 'conversation-service',
|
||||
endpoint: SharedWorker.lastCall.returnValue,
|
||||
timeout: false
|
||||
});
|
||||
sinon.assert.calledWith(clientStub.plugin, streamClient);
|
||||
});
|
||||
|
||||
suite('getAllConversations', function() {
|
||||
var streamStub, resolveStream, rejectStream;
|
||||
setup(function() {
|
||||
ConversationClient.init(APP_ID);
|
||||
|
||||
streamStub = sinon.stub({
|
||||
listen: () => {},
|
||||
closed: new Promise((resolve, reject) => {
|
||||
resolveStream = resolve;
|
||||
rejectStream = reject;
|
||||
})
|
||||
});
|
||||
|
||||
clientStub.stream.withArgs('getAllConversations', APP_ID).returns(
|
||||
streamStub
|
||||
);
|
||||
});
|
||||
|
||||
test('calls callback on every retrieved item', function() {
|
||||
var onConversationRetrievedStub = sinon.stub();
|
||||
|
||||
ConversationClient.getAllConversations(onConversationRetrievedStub);
|
||||
|
||||
sinon.assert.notCalled(onConversationRetrievedStub);
|
||||
|
||||
streamStub.listen.yield({ id: 1, body: 'body' });
|
||||
|
||||
sinon.assert.calledOnce(onConversationRetrievedStub);
|
||||
sinon.assert.calledWith(
|
||||
onConversationRetrievedStub, sinon.match({ id: 1, body: 'body'})
|
||||
);
|
||||
assert.instanceOf(onConversationRetrievedStub.lastCall.args[0], Thread);
|
||||
|
||||
streamStub.listen.yield({ id: 2, body: 'body2' });
|
||||
|
||||
sinon.assert.calledTwice(onConversationRetrievedStub);
|
||||
sinon.assert.calledWith(
|
||||
onConversationRetrievedStub, sinon.match({ id: 2, body: 'body2'})
|
||||
);
|
||||
assert.instanceOf(onConversationRetrievedStub.lastCall.args[0], Thread);
|
||||
});
|
||||
|
||||
test('is resolved when stream.closed is resolved', function(done) {
|
||||
var resolveStub = sinon.stub();
|
||||
|
||||
var streamPromise = ConversationClient.getAllConversations(() => {}).then(
|
||||
resolveStub
|
||||
);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
sinon.assert.notCalled(resolveStub);
|
||||
|
||||
resolveStream();
|
||||
|
||||
return streamPromise;
|
||||
}).then(() => {
|
||||
sinon.assert.calledOnce(resolveStub);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('is rejected when stream.closed is rejected', function(done) {
|
||||
var rejectStub = sinon.stub();
|
||||
var error = new Error('Error');
|
||||
|
||||
var streamPromise = ConversationClient.getAllConversations(() => {}).then(
|
||||
() => { throw new Error('getAllConversations should be rejected'); },
|
||||
rejectStub
|
||||
);
|
||||
|
||||
Promise.resolve().then(() => {
|
||||
sinon.assert.notCalled(rejectStub);
|
||||
|
||||
rejectStream(error);
|
||||
|
||||
return streamPromise;
|
||||
}).catch(() => {
|
||||
sinon.assert.calledWith(rejectStub, error);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
253
gaia/apps/sms/services/test/unit/conversation/conversation_service_test.js
Executable file
253
gaia/apps/sms/services/test/unit/conversation/conversation_service_test.js
Executable file
|
|
@ -0,0 +1,253 @@
|
|||
/*global bridge,
|
||||
ConversationService,
|
||||
Drafts,
|
||||
MocksHelper,
|
||||
MozMobileMessageClient
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/services/test/unit/mock_drafts.js');
|
||||
require(
|
||||
'/services/test/unit/moz_mobile_message/mock_moz_mobile_message_client.js'
|
||||
);
|
||||
require('/services/js/bridge_service_mixin.js');
|
||||
require('/services/js/conversation/conversation_service.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'Drafts',
|
||||
'Draft',
|
||||
'MozMobileMessageClient',
|
||||
'streamService'
|
||||
]).init();
|
||||
|
||||
suite('ConversationService >', function() {
|
||||
var serviceStub, mobileMessageClientStub;
|
||||
|
||||
const APP_INSTANCE_ID = 'fake-app-instance-id';
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
serviceStub = sinon.stub({
|
||||
method: () => {},
|
||||
stream: () => {},
|
||||
broadcast: () => {},
|
||||
listen: () => {},
|
||||
plugin: () => {}
|
||||
});
|
||||
|
||||
this.sinon.stub(bridge, 'service').withArgs('conversation-service').returns(
|
||||
serviceStub
|
||||
);
|
||||
|
||||
mobileMessageClientStub = sinon.stub({
|
||||
getThreads: () => {}
|
||||
});
|
||||
|
||||
this.sinon.stub(MozMobileMessageClient, 'forApp').withArgs(
|
||||
APP_INSTANCE_ID
|
||||
).returns(mobileMessageClientStub);
|
||||
|
||||
ConversationService.init();
|
||||
});
|
||||
|
||||
suite('getAllConversations >', function() {
|
||||
var serviceStreamStub, mobileMessageStreamStub, resolveMobileMessageStream,
|
||||
rejectMobileMessageStream, drafts, threadDraft, threads;
|
||||
|
||||
function threadToConversationSummary(thread, threadDraft) {
|
||||
return {
|
||||
id: thread.id,
|
||||
participants: thread.participants,
|
||||
body: thread.body,
|
||||
timestamp: thread.timestamp,
|
||||
status: { hasUnread: thread.unreadCount > 0, hasNewError: false },
|
||||
lastMessageType: thread.lastMessageType,
|
||||
draft: threadDraft || null
|
||||
};
|
||||
}
|
||||
|
||||
function draftToConversationSummary(draft) {
|
||||
return {
|
||||
id: draft.id,
|
||||
participants: draft.recipients,
|
||||
body: draft.content[0],
|
||||
timestamp: new Date(draft.timestamp),
|
||||
status: { hasUnread: false, hasNewError: false },
|
||||
lastMessageType: draft.type || 'sms',
|
||||
draft: draft
|
||||
};
|
||||
}
|
||||
|
||||
setup(function() {
|
||||
serviceStreamStub = sinon.stub({
|
||||
write: () => {},
|
||||
close: () => {},
|
||||
abort: () => {}
|
||||
});
|
||||
|
||||
mobileMessageStreamStub = sinon.stub({
|
||||
listen: () => {},
|
||||
cancel: () => {},
|
||||
closed: new Promise((resolve, reject) => {
|
||||
resolveMobileMessageStream = resolve;
|
||||
rejectMobileMessageStream = reject;
|
||||
})
|
||||
});
|
||||
|
||||
mobileMessageClientStub.getThreads.returns(mobileMessageStreamStub);
|
||||
|
||||
drafts = [{
|
||||
id: 1,
|
||||
recipients: ['+1'],
|
||||
content: ['body1'],
|
||||
type: 'sms',
|
||||
timestamp: 1
|
||||
}, {
|
||||
id: 3,
|
||||
recipients: ['a@abc.xyz'],
|
||||
content: ['body3'],
|
||||
type: 'mms',
|
||||
timestamp: 3
|
||||
}];
|
||||
|
||||
threadDraft = {
|
||||
id: 100,
|
||||
recipients: ['+100'],
|
||||
content: ['body100'],
|
||||
type: 'sms',
|
||||
threadId: 5,
|
||||
timestamp: 100
|
||||
};
|
||||
|
||||
threads = [{
|
||||
id: 2,
|
||||
participants: ['+2'],
|
||||
body: 'body2',
|
||||
timestamp: 2,
|
||||
unreadCount: 0,
|
||||
lastMessageType: 'sms'
|
||||
}, {
|
||||
id: 4,
|
||||
participants: ['a@abc.xyz'],
|
||||
body: 'body4',
|
||||
timestamp: 4,
|
||||
unreadCount: 1,
|
||||
lastMessageType: 'mms'
|
||||
}, {
|
||||
id: 5,
|
||||
participants: ['+5'],
|
||||
body: 'body5',
|
||||
timestamp: 5,
|
||||
unreadCount: 0,
|
||||
lastMessageType: 'sms'
|
||||
}];
|
||||
|
||||
// Sort threads in the reverse order.
|
||||
threads.sort((threadA, threadB) => threadB.timestamp - threadA.timestamp);
|
||||
|
||||
this.sinon.stub(Drafts, 'getAllThreadless').returns(drafts.slice());
|
||||
this.sinon.stub(Drafts, 'byThreadId').returns(null);
|
||||
Drafts.byThreadId.withArgs(5).returns(threadDraft);
|
||||
});
|
||||
|
||||
test('mobileMessage stream is cancelled if main stream is cancelled',
|
||||
function() {
|
||||
ConversationService.getAllConversations(
|
||||
serviceStreamStub, APP_INSTANCE_ID
|
||||
);
|
||||
|
||||
serviceStreamStub.cancel('ReAsOn');
|
||||
|
||||
sinon.assert.calledOnce(mobileMessageStreamStub.cancel);
|
||||
sinon.assert.calledWith(mobileMessageStreamStub.cancel, 'ReAsOn');
|
||||
});
|
||||
|
||||
test('threads and drafts are returned in the correct order',
|
||||
function(done) {
|
||||
var getAllConversationsPromise = ConversationService.getAllConversations(
|
||||
serviceStreamStub, APP_INSTANCE_ID
|
||||
);
|
||||
|
||||
threads.forEach((thread) => {
|
||||
mobileMessageStreamStub.listen.yield(thread);
|
||||
});
|
||||
|
||||
resolveMobileMessageStream();
|
||||
|
||||
getAllConversationsPromise.then(() => {
|
||||
sinon.assert.callCount(serviceStreamStub.write, 5);
|
||||
sinon.assert.callOrder(
|
||||
serviceStreamStub.write.withArgs(
|
||||
sinon.match(threadToConversationSummary(threads[0], threadDraft))
|
||||
),
|
||||
serviceStreamStub.write.withArgs(
|
||||
sinon.match(threadToConversationSummary(threads[1]))
|
||||
),
|
||||
serviceStreamStub.write.withArgs(
|
||||
sinon.match(draftToConversationSummary(drafts[1]))
|
||||
),
|
||||
serviceStreamStub.write.withArgs(
|
||||
sinon.match(threadToConversationSummary(threads[2]))
|
||||
),
|
||||
serviceStreamStub.write.withArgs(
|
||||
sinon.match(draftToConversationSummary(drafts[0]))
|
||||
)
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('only drafts are returned if there is no threads', function(done) {
|
||||
var getAllConversationsPromise = ConversationService.getAllConversations(
|
||||
serviceStreamStub, APP_INSTANCE_ID
|
||||
);
|
||||
|
||||
resolveMobileMessageStream();
|
||||
|
||||
getAllConversationsPromise.then(() => {
|
||||
sinon.assert.callCount(serviceStreamStub.write, 2);
|
||||
sinon.assert.callOrder(
|
||||
serviceStreamStub.write.withArgs(
|
||||
sinon.match(draftToConversationSummary(drafts[1]))
|
||||
),
|
||||
serviceStreamStub.write.withArgs(
|
||||
sinon.match(draftToConversationSummary(drafts[0]))
|
||||
)
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('stream is closed when mobileMessageStream is successfully closed',
|
||||
function(done) {
|
||||
var getAllConversationsPromise = ConversationService.getAllConversations(
|
||||
serviceStreamStub, APP_INSTANCE_ID
|
||||
);
|
||||
|
||||
resolveMobileMessageStream();
|
||||
|
||||
getAllConversationsPromise.then(() => {
|
||||
sinon.assert.called(serviceStreamStub.close);
|
||||
}, () => {
|
||||
throw new Error('getAllConversations should always be resolved');
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('stream is aborted when mobileMessageStream is unexpectedly closed',
|
||||
function(done) {
|
||||
var getAllConversationsPromise = ConversationService.getAllConversations(
|
||||
serviceStreamStub, APP_INSTANCE_ID
|
||||
);
|
||||
|
||||
rejectMobileMessageStream(new Error('Exception'));
|
||||
|
||||
getAllConversationsPromise.then(() => {
|
||||
sinon.assert.calledWith(serviceStreamStub.abort, '[Error] Exception');
|
||||
}, () => {
|
||||
throw new Error('getAllConversations should always be resolved');
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
10
gaia/apps/sms/services/test/unit/conversation/mock_conversation_client.js
Executable file
10
gaia/apps/sms/services/test/unit/conversation/mock_conversation_client.js
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
/* exported MockConversationClient */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.MockConversationClient = {
|
||||
init: () => {},
|
||||
getAllConversations: () => Promise.resolve()
|
||||
};
|
||||
})(window);
|
||||
659
gaia/apps/sms/services/test/unit/drafts_test.js
Executable file
659
gaia/apps/sms/services/test/unit/drafts_test.js
Executable file
|
|
@ -0,0 +1,659 @@
|
|||
/*global asyncStorage,
|
||||
Drafts,
|
||||
Draft,
|
||||
InterInstanceEventDispatcher,
|
||||
MocksHelper
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/shared/js/event_dispatcher.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/services/js/drafts.js');
|
||||
|
||||
require('/shared/test/unit/mocks/mock_async_storage.js');
|
||||
require('/views/shared/test/unit/mock_inter_instance_event_dispatcher.js');
|
||||
|
||||
|
||||
var MocksHelperForDraftsTest = new MocksHelper([
|
||||
'asyncStorage',
|
||||
'InterInstanceEventDispatcher'
|
||||
]).init();
|
||||
|
||||
suite('Drafts', function() {
|
||||
var mocksHelper = MocksHelperForDraftsTest;
|
||||
|
||||
var threadDraft1, threadDraft2, threadDraft3, threadDraft4, threadDraft5;
|
||||
var draft1, draft2;
|
||||
|
||||
suiteSetup(function() {
|
||||
mocksHelper.suiteSetup();
|
||||
|
||||
threadDraft1 = new Draft({
|
||||
recipients: ['555', '666'],
|
||||
content: ['This is a draft message'],
|
||||
timestamp: 1,
|
||||
threadId: 42,
|
||||
subject: 'This is a subject',
|
||||
type: 'sms',
|
||||
id: 1
|
||||
});
|
||||
threadDraft2 = new Draft({
|
||||
recipients: ['555'],
|
||||
content: ['This is a draft message'],
|
||||
timestamp: 2,
|
||||
threadId: 44,
|
||||
subject: 'This is a subject',
|
||||
type: 'sms',
|
||||
id: 2
|
||||
});
|
||||
threadDraft3 = new Draft({
|
||||
recipients: ['555', '222'],
|
||||
content: ['This is a draft message'],
|
||||
timestamp: 3,
|
||||
threadId: 1,
|
||||
subject: 'This is a subject',
|
||||
type: 'sms',
|
||||
id: 3
|
||||
});
|
||||
threadDraft4 = new Draft({
|
||||
recipients: ['555', '333'],
|
||||
content: ['This is a draft message'],
|
||||
timestamp: 4,
|
||||
threadId: 2,
|
||||
subject: 'This is a subject',
|
||||
type: 'sms',
|
||||
id: 4
|
||||
});
|
||||
draft1 = new Draft({
|
||||
recipients: ['555', '444'],
|
||||
content: ['This is a draft message'],
|
||||
timestamp: 5,
|
||||
threadId: null,
|
||||
subject: 'This is a subject',
|
||||
type: 'sms',
|
||||
id: 5
|
||||
});
|
||||
threadDraft5 = new Draft({
|
||||
recipients: ['123456'],
|
||||
content: [
|
||||
'This is a draft MMS...',
|
||||
{
|
||||
blob: {
|
||||
type: 'audio/ogg',
|
||||
size: 12345
|
||||
},
|
||||
name: 'audio.oga'
|
||||
},
|
||||
'...with a recipient and a thread'
|
||||
],
|
||||
timestamp: Date.now() - (3600000 * 2),
|
||||
threadId: 8,
|
||||
type: 'mms',
|
||||
id: 6
|
||||
});
|
||||
draft2 = new Draft({
|
||||
recipients: ['555', '444'],
|
||||
content: ['This is a draft message'],
|
||||
timestamp: 5,
|
||||
threadId: null,
|
||||
subject: 'This is a different subject',
|
||||
type: 'sms',
|
||||
id: 7
|
||||
});
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
mocksHelper.suiteTeardown();
|
||||
});
|
||||
|
||||
suite('init() >', function() {
|
||||
var savedHandler;
|
||||
var deletedHandler;
|
||||
|
||||
setup(function() {
|
||||
this.sinon.spy(InterInstanceEventDispatcher, 'on');
|
||||
this.sinon.stub(Drafts, 'request').returns(Promise.resolve());
|
||||
this.sinon.stub(Drafts, 'emit');
|
||||
Drafts.init();
|
||||
savedHandler = InterInstanceEventDispatcher.on.firstCall.args[1];
|
||||
deletedHandler = InterInstanceEventDispatcher.on.secondCall.args[1];
|
||||
});
|
||||
|
||||
test('draft-saved will be dispatched as saved', function(done) {
|
||||
sinon.assert.calledWith(InterInstanceEventDispatcher.on, 'draft-saved');
|
||||
savedHandler(threadDraft1).then(() => {
|
||||
sinon.assert.calledWith(Drafts.request, true);
|
||||
sinon.assert.calledWith(Drafts.emit, 'saved', threadDraft1);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('draft-deleted will be dispatched as deleted', function(done) {
|
||||
sinon.assert.calledWith(InterInstanceEventDispatcher.on, 'draft-deleted');
|
||||
deletedHandler(threadDraft2).then(() => {
|
||||
sinon.assert.calledWith(Drafts.request, true);
|
||||
sinon.assert.calledWith(Drafts.emit, 'deleted', threadDraft2);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('add() >', function() {
|
||||
var onSavedStub;
|
||||
|
||||
setup(function() {
|
||||
onSavedStub = sinon.stub();
|
||||
Drafts.on('saved', onSavedStub);
|
||||
this.sinon.spy(InterInstanceEventDispatcher, 'emit');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
Drafts.clear().offAll('saved');
|
||||
});
|
||||
|
||||
test('correctly adds thread bound drafts', function() {
|
||||
Drafts.add(threadDraft1);
|
||||
|
||||
assert.deepEqual(Drafts.byThreadId(threadDraft1.threadId), threadDraft1);
|
||||
assert.isNull(Drafts.byThreadId(threadDraft2.threadId));
|
||||
|
||||
sinon.assert.calledOnce(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, threadDraft1);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
threadDraft1
|
||||
);
|
||||
|
||||
Drafts.add(threadDraft2);
|
||||
|
||||
assert.deepEqual(Drafts.byThreadId(threadDraft1.threadId), threadDraft1);
|
||||
assert.deepEqual(Drafts.byThreadId(threadDraft2.threadId), threadDraft2);
|
||||
|
||||
sinon.assert.calledTwice(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, threadDraft2);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
threadDraft2
|
||||
);
|
||||
});
|
||||
|
||||
test('correctly adds thread less drafts', function() {
|
||||
Drafts.add(draft1);
|
||||
|
||||
assert.deepEqual(Drafts.byDraftId(draft1.id), draft1);
|
||||
assert.isNull(Drafts.byDraftId(draft2.id));
|
||||
|
||||
sinon.assert.calledOnce(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, draft1);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
draft1
|
||||
);
|
||||
|
||||
Drafts.add(draft2);
|
||||
|
||||
assert.deepEqual(Drafts.byDraftId(draft1.id), draft1);
|
||||
assert.deepEqual(Drafts.byDraftId(draft2.id), draft2);
|
||||
|
||||
sinon.assert.calledTwice(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, draft2);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
draft2
|
||||
);
|
||||
});
|
||||
|
||||
test('add draft of same threadId replaces previous', function() {
|
||||
assert.equal(
|
||||
Drafts.add(threadDraft2).byThreadId(threadDraft2.threadId).id,
|
||||
threadDraft2.id
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, threadDraft2);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
threadDraft2
|
||||
);
|
||||
|
||||
var updatedDraft = new Draft({
|
||||
recipients: ['555'],
|
||||
content: ['This is a new draft for thread 44'],
|
||||
subject: 'This is a subject',
|
||||
timestamp: 2,
|
||||
threadId: 44,
|
||||
type: 'sms'
|
||||
});
|
||||
|
||||
Drafts.add(updatedDraft);
|
||||
|
||||
assert.notEqual(Drafts.byThreadId(44).id, threadDraft2.id);
|
||||
|
||||
sinon.assert.calledTwice(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, updatedDraft);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
updatedDraft
|
||||
);
|
||||
});
|
||||
|
||||
test('add threadless draft of same draft.id replaces previous', function() {
|
||||
assert.equal(
|
||||
Drafts.add(draft1).byDraftId(draft1.id).content,
|
||||
'This is a draft message'
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, draft1);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
draft1
|
||||
);
|
||||
|
||||
var updatedDraft = new Draft({
|
||||
recipients: ['555', '444'],
|
||||
content: ['This is a new draft message'],
|
||||
timestamp: 5,
|
||||
threadId: null,
|
||||
subject: 'This is a subject',
|
||||
type: 'sms',
|
||||
id: 5
|
||||
});
|
||||
|
||||
Drafts.add(updatedDraft);
|
||||
|
||||
assert.equal(
|
||||
Drafts.byDraftId(draft1.id).content,
|
||||
'This is a new draft message'
|
||||
);
|
||||
|
||||
sinon.assert.calledTwice(onSavedStub);
|
||||
sinon.assert.calledWith(onSavedStub, updatedDraft);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-saved',
|
||||
updatedDraft
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('delete() >', function() {
|
||||
var onDeletedStub;
|
||||
|
||||
setup(function() {
|
||||
onDeletedStub = sinon.stub();
|
||||
Drafts.on('deleted', onDeletedStub);
|
||||
|
||||
[threadDraft1, threadDraft2, threadDraft5, draft2].forEach(
|
||||
Drafts.add, Drafts
|
||||
);
|
||||
this.sinon.spy(InterInstanceEventDispatcher, 'emit');
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
Drafts.clear().offAll('deleted');
|
||||
});
|
||||
|
||||
test('Delete draft with reference', function() {
|
||||
assert.isNotNull(Drafts.byThreadId(threadDraft1.threadId));
|
||||
assert.isNull(
|
||||
Drafts.delete(threadDraft1).byThreadId(threadDraft1.threadId)
|
||||
);
|
||||
|
||||
sinon.assert.calledOnce(onDeletedStub);
|
||||
sinon.assert.calledWith(onDeletedStub, threadDraft1);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-deleted',
|
||||
threadDraft1
|
||||
);
|
||||
});
|
||||
|
||||
test('Deleting new message drafts', function() {
|
||||
assert.isNotNull(Drafts.byThreadId(threadDraft5.threadId));
|
||||
assert.isNotNull(Drafts.byDraftId(draft2.id));
|
||||
|
||||
// First draft removes only the draft from the drafts
|
||||
assert.isNull(
|
||||
Drafts.delete(threadDraft5).byThreadId(threadDraft5.threadId)
|
||||
);
|
||||
assert.isNotNull(Drafts.byDraftId(draft2.id));
|
||||
|
||||
sinon.assert.calledOnce(onDeletedStub);
|
||||
sinon.assert.calledWith(onDeletedStub, threadDraft5);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-deleted',
|
||||
threadDraft5
|
||||
);
|
||||
|
||||
// The last draft in the thread removes the thread from the index
|
||||
assert.isNull(Drafts.delete(draft2).byDraftId(draft2.id));
|
||||
|
||||
sinon.assert.calledTwice(onDeletedStub);
|
||||
sinon.assert.calledWith(onDeletedStub, draft2);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-deleted',
|
||||
draft2
|
||||
);
|
||||
});
|
||||
|
||||
test('delete by non-draft object', function() {
|
||||
var draft = new Draft({
|
||||
recipients: ['999999'],
|
||||
content: ['foo'],
|
||||
timestamp: 2,
|
||||
threadId: 999,
|
||||
subject: 'This is a subject',
|
||||
type: 'sms'
|
||||
});
|
||||
|
||||
Drafts.add(draft);
|
||||
|
||||
assert.deepEqual(Drafts.byThreadId(999), draft);
|
||||
|
||||
var draftToDelete = new Draft({
|
||||
threadId: 999,
|
||||
id: draft.id
|
||||
});
|
||||
|
||||
Drafts.delete(draftToDelete);
|
||||
|
||||
assert.isNull(Drafts.byThreadId(999));
|
||||
|
||||
sinon.assert.calledOnce(onDeletedStub);
|
||||
sinon.assert.calledWith(onDeletedStub, draftToDelete);
|
||||
sinon.assert.calledWith(
|
||||
InterInstanceEventDispatcher.emit,
|
||||
'draft-deleted',
|
||||
draftToDelete
|
||||
);
|
||||
});
|
||||
|
||||
test('does not fire "deleted" event if nothing was deleted', function() {
|
||||
// Try to delete draft that doesn't exist.
|
||||
Drafts.delete(draft1);
|
||||
|
||||
sinon.assert.notCalled(onDeletedStub);
|
||||
sinon.assert.notCalled(InterInstanceEventDispatcher.emit);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Select drafts', function() {
|
||||
suiteSetup(function() {
|
||||
[threadDraft1, threadDraft2, threadDraft3, threadDraft4, draft1].forEach(
|
||||
Drafts.add, Drafts
|
||||
);
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
Drafts.clear();
|
||||
});
|
||||
|
||||
suite('byThreadId', function() {
|
||||
test('get drafts for id 1', function() {
|
||||
assert.deepEqual(
|
||||
Drafts.byThreadId(threadDraft3.threadId), threadDraft3
|
||||
);
|
||||
});
|
||||
|
||||
test('throws if threadId is not a number', function() {
|
||||
assert.throws(() => Drafts.byThreadId(null));
|
||||
});
|
||||
|
||||
test('returns null if there is no draft for the thread id', function() {
|
||||
assert.isNull(Drafts.byThreadId(10));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('get(id) > ', function() {
|
||||
var id = 101;
|
||||
|
||||
suiteSetup(function() {
|
||||
Drafts.add(new Draft({
|
||||
id: id,
|
||||
recipients: [],
|
||||
content: ['A new message draft with no recipients'],
|
||||
type: 'sms'
|
||||
}));
|
||||
});
|
||||
|
||||
suiteTeardown(function() {
|
||||
Drafts.clear();
|
||||
});
|
||||
|
||||
test('get draft for id 101', function() {
|
||||
var draft = Drafts.byDraftId(id);
|
||||
|
||||
assert.equal(draft.id, id);
|
||||
assert.equal(draft.threadId, null);
|
||||
});
|
||||
|
||||
test('get no draft for non-existant id', function() {
|
||||
assert.isNull(Drafts.byDraftId(9999999));
|
||||
});
|
||||
});
|
||||
|
||||
suite('getAll()>', function() {
|
||||
setup(function() {
|
||||
[
|
||||
threadDraft1,
|
||||
threadDraft2,
|
||||
threadDraft3,
|
||||
threadDraft4,
|
||||
threadDraft5,
|
||||
draft1,
|
||||
draft2
|
||||
].forEach(Drafts.add, Drafts);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
Drafts.clear();
|
||||
});
|
||||
|
||||
test('getAll returns all drafts', function() {
|
||||
var orderedDrafts = [
|
||||
threadDraft1,
|
||||
threadDraft2,
|
||||
threadDraft3,
|
||||
threadDraft4,
|
||||
threadDraft5,
|
||||
draft1,
|
||||
draft2
|
||||
];
|
||||
|
||||
for (var draft of Drafts.getAll()) {
|
||||
assert.deepEqual(draft, orderedDrafts.shift());
|
||||
}
|
||||
|
||||
// All drafts were examined
|
||||
assert.equal(orderedDrafts.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getAllThreadless()>', function() {
|
||||
setup(function() {
|
||||
[
|
||||
threadDraft1,
|
||||
draft1,
|
||||
draft2
|
||||
].forEach(Drafts.add, Drafts);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
Drafts.clear();
|
||||
});
|
||||
|
||||
test('getAllThreadless returns only thread less drafts', function() {
|
||||
assert.deepEqual(Drafts.getAllThreadless(), [draft1, draft2]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('clear() >', function() {
|
||||
suiteSetup(function() {
|
||||
[threadDraft1, threadDraft2, threadDraft3, threadDraft4].forEach(
|
||||
Drafts.add, Drafts
|
||||
);
|
||||
});
|
||||
|
||||
test('clear the entire draft index', function() {
|
||||
Drafts.clear();
|
||||
|
||||
assert.isNull(Drafts.byThreadId(42));
|
||||
assert.isNull(Drafts.byThreadId(1));
|
||||
assert.isNull(Drafts.byThreadId(2));
|
||||
});
|
||||
});
|
||||
|
||||
suite('Draft', function() {
|
||||
var draft;
|
||||
|
||||
test('Draft', function() {
|
||||
draft = new Draft();
|
||||
assert.ok(Draft);
|
||||
});
|
||||
|
||||
test('Draft from empty object', function() {
|
||||
draft = new Draft([]);
|
||||
assert.deepEqual(draft.recipients, []);
|
||||
assert.deepEqual(draft.content, []);
|
||||
assert.equal(draft.threadId, null);
|
||||
assert.isFalse(draft.isEdited);
|
||||
});
|
||||
|
||||
test('Draft from Draft object', function() {
|
||||
draft = new Draft(threadDraft1);
|
||||
assert.deepEqual(draft.recipients, ['555', '666']);
|
||||
assert.deepEqual(draft.content, ['This is a draft message']);
|
||||
assert.equal(draft.timestamp, 1);
|
||||
assert.equal(draft.threadId, 42);
|
||||
assert.equal(draft.type, 'sms');
|
||||
assert.isFalse(draft.isEdited);
|
||||
});
|
||||
|
||||
test('Draft with explicit valid id', function() {
|
||||
draft = new Draft({
|
||||
id: 101,
|
||||
recipients: [],
|
||||
content: ['An explicit id'],
|
||||
type: 'sms'
|
||||
});
|
||||
|
||||
assert.equal(draft.id, 101);
|
||||
});
|
||||
|
||||
test('Draft with explicit invalid id', function() {
|
||||
try {
|
||||
draft = new Draft({
|
||||
id: '101',
|
||||
recipients: [],
|
||||
content: ['An explicit id'],
|
||||
timestamp: Date.now(),
|
||||
type: 'sms'
|
||||
});
|
||||
assert.ok(false);
|
||||
} catch (e) {
|
||||
assert.ok(true);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('Storage and Retrieval', function() {
|
||||
setup(function() {
|
||||
Drafts.clear();
|
||||
this.sinon.useFakeTimers();
|
||||
});
|
||||
|
||||
test('Load drafts, has stored data', function(done) {
|
||||
this.sinon.stub(asyncStorage, 'getItem').yields([
|
||||
[42, [threadDraft1]],
|
||||
[44, [threadDraft2]],
|
||||
[null, [draft1, draft2]]
|
||||
]);
|
||||
|
||||
Drafts.request().then(function() {
|
||||
assert.deepEqual(Drafts.byThreadId(42), threadDraft1);
|
||||
|
||||
assert.deepEqual(Drafts.byThreadId(44), threadDraft2);
|
||||
|
||||
var threadLessDrafts = Array.from(Drafts.getAll()).filter(
|
||||
(draft) => !draft.threadId
|
||||
);
|
||||
assert.equal(threadLessDrafts.length, 2);
|
||||
assert.isNull(Drafts.byThreadId(5));
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Load, clear, restore drafts', function(done) {
|
||||
this.sinon.stub(asyncStorage, 'getItem').yields([
|
||||
[42, [threadDraft1]],
|
||||
[44, [threadDraft2]],
|
||||
[null, [draft1, draft2]]
|
||||
]);
|
||||
|
||||
// Load
|
||||
Drafts.request().then(function() {
|
||||
assert.equal(Drafts.size, 3);
|
||||
assert.isTrue(asyncStorage.getItem.calledOnce);
|
||||
|
||||
// Clear (This will set isCached = false)
|
||||
Drafts.clear();
|
||||
|
||||
assert.equal(Drafts.size, 0);
|
||||
|
||||
// Restore
|
||||
return Drafts.request().then(function() {
|
||||
assert.equal(Drafts.size, 3);
|
||||
assert.isTrue(asyncStorage.getItem.calledTwice);
|
||||
});
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('Load drafts, has no stored data', function(done) {
|
||||
this.sinon.stub(asyncStorage, 'getItem').yields([]);
|
||||
|
||||
Drafts.request().then(function() {
|
||||
assert.equal(Drafts.size, 0);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
suite('drafts index cache >', function() {
|
||||
setup(function() {
|
||||
this.sinon.stub(asyncStorage, 'getItem').
|
||||
withArgs('draft index').
|
||||
yields(null);
|
||||
});
|
||||
|
||||
test('is properly populated', function(done) {
|
||||
Drafts.request().then(() => {
|
||||
sinon.assert.calledOnce(asyncStorage.getItem);
|
||||
|
||||
return Drafts.request();
|
||||
}).then(() => {
|
||||
sinon.assert.calledOnce(asyncStorage.getItem);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('is properly refreshed with force parameter', function(done) {
|
||||
Drafts.request().then(() => {
|
||||
sinon.assert.calledOnce(asyncStorage.getItem);
|
||||
|
||||
// "Force" parameter should re-request data from asyncStorage even if
|
||||
// it's already cached
|
||||
return Drafts.request(true);
|
||||
}).then(() => {
|
||||
sinon.assert.calledTwice(asyncStorage.getItem);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1142
gaia/apps/sms/services/test/unit/message_manager_test.js
Executable file
1142
gaia/apps/sms/services/test/unit/message_manager_test.js
Executable file
File diff suppressed because it is too large
Load diff
102
gaia/apps/sms/services/test/unit/messaging/messaging_client_test.js
Executable file
102
gaia/apps/sms/services/test/unit/messaging/messaging_client_test.js
Executable file
|
|
@ -0,0 +1,102 @@
|
|||
/*global bridge,
|
||||
MessagingClient,
|
||||
MocksHelper
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/services/js/messaging/messaging_client.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
]).init();
|
||||
|
||||
suite('Messaging Client >', function() {
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
const APP_INSTANCE_ID = 'fake-app-instance-id';
|
||||
|
||||
var clientStub;
|
||||
var successResult = {};
|
||||
var errorResult = new Error();
|
||||
const METHODS = {
|
||||
'sendSMS': [{
|
||||
recipients: '123',
|
||||
content: 'hola'
|
||||
}],
|
||||
'sendMMS': [{
|
||||
receivers: ['123'],
|
||||
subject: null,
|
||||
smil: 'fake smil',
|
||||
attachments: 'fake attachments'
|
||||
}],
|
||||
'resendMessage': [{
|
||||
id: 'fake message'
|
||||
}],
|
||||
'retrieveMMS': [1]
|
||||
};
|
||||
|
||||
setup(function() {
|
||||
clientStub = sinon.stub({
|
||||
method: () => {}
|
||||
});
|
||||
|
||||
this.sinon.stub(bridge, 'client').returns(clientStub);
|
||||
});
|
||||
|
||||
suite('init', function() {
|
||||
test('throws if app instance id is not provided', function() {
|
||||
assert.throws(() => MessagingClient.forApp());
|
||||
});
|
||||
|
||||
test('client is initialized', function() {
|
||||
MessagingClient.init(APP_INSTANCE_ID);
|
||||
|
||||
sinon.assert.calledWith(bridge.client, {
|
||||
service: 'messaging-service',
|
||||
endpoint: sinon.match.instanceOf(SharedWorker),
|
||||
timeout: false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(METHODS).forEach((method) => {
|
||||
setup(function() {
|
||||
MessagingClient.init(APP_INSTANCE_ID);
|
||||
});
|
||||
|
||||
suite(method, function() {
|
||||
var params = METHODS[method];
|
||||
|
||||
test('onsuccess', function(done) {
|
||||
clientStub.method.withArgs(method, ...params, APP_INSTANCE_ID).returns(
|
||||
Promise.resolve(successResult)
|
||||
);
|
||||
|
||||
MessagingClient[method](...params).then((result) => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.method, method, ...params, APP_INSTANCE_ID
|
||||
);
|
||||
assert.equal(successResult, result);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('onerror', function(done) {
|
||||
clientStub.method.withArgs(method, ...params, APP_INSTANCE_ID).returns(
|
||||
Promise.reject(errorResult)
|
||||
);
|
||||
|
||||
MessagingClient[method](...params).then(() => {
|
||||
throw new Error('Promise should not be resolved!');
|
||||
}, (error) => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.method, method, ...params, APP_INSTANCE_ID
|
||||
);
|
||||
assert.deepEqual(errorResult, error);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
560
gaia/apps/sms/services/test/unit/messaging/messaging_service_test.js
Executable file
560
gaia/apps/sms/services/test/unit/messaging/messaging_service_test.js
Executable file
|
|
@ -0,0 +1,560 @@
|
|||
/*global bridge,
|
||||
MessagingService,
|
||||
MozMobileMessageClient,
|
||||
MocksHelper,
|
||||
MockMessages,
|
||||
Settings,
|
||||
SMIL
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/js/bridge_service_mixin.js');
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require(
|
||||
'/services/test/unit/moz_mobile_message/mock_moz_mobile_message_client.js'
|
||||
);
|
||||
require('/views/shared/test/unit/mock_messages.js');
|
||||
require('/views/shared/test/unit/mock_settings.js');
|
||||
require('/views/shared/test/unit/mock_smil.js');
|
||||
require('/services/js/messaging/messaging_service.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'MozMobileMessageClient',
|
||||
'Settings',
|
||||
'SMIL'
|
||||
]).init();
|
||||
|
||||
suite('Messaging service >', function() {
|
||||
var serviceStub, clientStub, successResult, errorResult;
|
||||
|
||||
const APP_INSTANCE_ID = 'fake-app-instance-id';
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
successResult = {};
|
||||
errorResult = new Error();
|
||||
|
||||
serviceStub = sinon.stub({
|
||||
method: () => {},
|
||||
stream: () => {},
|
||||
broadcast: () => {},
|
||||
listen: () => {},
|
||||
plugin: () => {}
|
||||
});
|
||||
|
||||
clientStub = sinon.stub({
|
||||
send: () => {},
|
||||
sendMMS: () => {},
|
||||
resendMessage: () => {},
|
||||
retrieveMMS: () => {},
|
||||
delete: () => {}
|
||||
});
|
||||
|
||||
this.sinon.stub(MozMobileMessageClient, 'forApp').withArgs(
|
||||
APP_INSTANCE_ID
|
||||
).returns(clientStub);
|
||||
|
||||
this.sinon.stub(bridge, 'service').returns(serviceStub);
|
||||
|
||||
MessagingService.init();
|
||||
});
|
||||
|
||||
suite('init', function() {
|
||||
test('service mixing and client are initialized', function() {
|
||||
sinon.assert.calledWith(bridge.service, 'messaging-service');
|
||||
});
|
||||
});
|
||||
|
||||
suite('sendSMS', function() {
|
||||
var smsOpts;
|
||||
|
||||
test('send to one recipient successfully', function(done) {
|
||||
smsOpts = {
|
||||
recipients: '123',
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
|
||||
);
|
||||
|
||||
assert.deepEqual(results, [{
|
||||
success: true,
|
||||
result: successResult,
|
||||
code: undefined,
|
||||
recipient: '123'
|
||||
}]);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('send to two recipients successfully', function(done) {
|
||||
smsOpts = {
|
||||
recipients: ['123', '456'],
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
|
||||
sinon.assert.calledTwice(clientStub.send);
|
||||
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients[0], smsOpts.content, undefined
|
||||
);
|
||||
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients[1], smsOpts.content, undefined
|
||||
);
|
||||
|
||||
results.forEach((result, idx) => {
|
||||
assert.deepEqual(result, {
|
||||
success: true,
|
||||
result: successResult,
|
||||
code: undefined,
|
||||
recipient: smsOpts.recipients[idx]
|
||||
});
|
||||
});
|
||||
}).then(done, done);
|
||||
|
||||
});
|
||||
|
||||
test('send to one recipient unsuccessfully', function(done) {
|
||||
smsOpts = {
|
||||
recipients: '123',
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.reject(errorResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
|
||||
);
|
||||
|
||||
assert.deepEqual(results, [{
|
||||
success: false,
|
||||
result: undefined,
|
||||
code: errorResult,
|
||||
recipient: '123'
|
||||
}]);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('send to two recipients unsuccessfully', function(done) {
|
||||
smsOpts = {
|
||||
recipients: ['123', '456'],
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.reject(errorResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then((results) => {
|
||||
sinon.assert.calledTwice(clientStub.send);
|
||||
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients[0], smsOpts.content, undefined
|
||||
);
|
||||
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients[1], smsOpts.content, undefined
|
||||
);
|
||||
|
||||
results.forEach((result, idx) => {
|
||||
assert.deepEqual(result, {
|
||||
success: false,
|
||||
result: undefined,
|
||||
code: errorResult,
|
||||
recipient: smsOpts.recipients[idx]
|
||||
});
|
||||
});
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('send with a serviceId in dual SIM setup', function(done) {
|
||||
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
|
||||
smsOpts = {
|
||||
recipients: '123',
|
||||
content: 'hola',
|
||||
serviceId: 0 // we use 0 because it's falsy, to test it still works
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients, smsOpts.content, { serviceId: 0 }
|
||||
);
|
||||
}).then(done, done);
|
||||
|
||||
});
|
||||
|
||||
test('send with a serviceId in non-dual SIM setup', function(done) {
|
||||
this.sinon.stub(Settings, 'hasSeveralSim').returns(false);
|
||||
smsOpts = {
|
||||
recipients: '123',
|
||||
content: 'hola',
|
||||
serviceId: 1
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
|
||||
);
|
||||
}).then(done, done);
|
||||
|
||||
});
|
||||
|
||||
test('serviceId is a string containing a number', function(done) {
|
||||
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
|
||||
smsOpts = {
|
||||
recipients: '123',
|
||||
content: 'hola',
|
||||
serviceId: '0'
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients, smsOpts.content, { serviceId: 0 }
|
||||
);
|
||||
}).then(done, done);
|
||||
|
||||
});
|
||||
|
||||
test('serviceId is a bad string', function(done) {
|
||||
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
|
||||
smsOpts = {
|
||||
recipients: '123',
|
||||
content: 'hola',
|
||||
serviceId: 'oirutoirutoitr'
|
||||
};
|
||||
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendSMS(smsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.send, smsOpts.recipients, smsOpts.content, undefined
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('sendMMS', function() {
|
||||
var smil;
|
||||
|
||||
setup(function() {
|
||||
smil = {
|
||||
smil: {},
|
||||
attachments: {}
|
||||
};
|
||||
this.sinon.stub(SMIL, 'generate').returns(smil);
|
||||
});
|
||||
|
||||
test('send to one recipient successfully', function(done) {
|
||||
var mmsOpts = {
|
||||
recipients: '123',
|
||||
subject: null,
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.sendMMS.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then((result) => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.sendMMS,
|
||||
{
|
||||
receivers: ['123'],
|
||||
subject: null,
|
||||
smil: smil.smil,
|
||||
attachments: smil.attachments
|
||||
}, /* send options */ undefined
|
||||
);
|
||||
assert.equal(result, successResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('send to two recipients successfully', function(done) {
|
||||
var mmsOpts = {
|
||||
recipients: ['123', '456'],
|
||||
subject: null,
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.sendMMS.returns(Promise.resolve(successResult));
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then((result) => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.sendMMS,
|
||||
{
|
||||
receivers: ['123', '456'],
|
||||
subject: null,
|
||||
smil: smil.smil,
|
||||
attachments: smil.attachments
|
||||
}, /* send options */ undefined
|
||||
);
|
||||
assert.equal(result, successResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('send to one recipient unsuccessfully', function(done) {
|
||||
var mmsOpts = {
|
||||
recipients: '123',
|
||||
subject: null,
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.sendMMS.returns(Promise.reject(errorResult));
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).catch((error) => {
|
||||
assert.equal(error, errorResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('send to two recipients unsuccessfully', function(done) {
|
||||
var mmsOpts = {
|
||||
recipients: ['123', '456'],
|
||||
subject: null,
|
||||
content: 'hola'
|
||||
};
|
||||
|
||||
clientStub.sendMMS.returns(Promise.reject(errorResult));
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).catch((error) => {
|
||||
assert.equal(error, errorResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
suite('send with a serviceId', function() {
|
||||
var mmsOpts;
|
||||
|
||||
setup(function() {
|
||||
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
|
||||
|
||||
mmsOpts = {
|
||||
recipients: '123',
|
||||
subject: null,
|
||||
content: 'hola',
|
||||
// we use 0 to check that the code behaves correctly with falsy values
|
||||
serviceId: 0
|
||||
};
|
||||
|
||||
clientStub.sendMMS.returns(Promise.resolve(successResult));
|
||||
});
|
||||
|
||||
test('while the current serviceId is the same', function(done) {
|
||||
Settings.mmsServiceId = 0;
|
||||
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.sendMMS,
|
||||
{
|
||||
receivers: ['123'],
|
||||
subject: null,
|
||||
smil: smil.smil,
|
||||
attachments: smil.attachments
|
||||
}, {
|
||||
serviceId: mmsOpts.serviceId
|
||||
}
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('while the current serviceId is different', function(done) {
|
||||
Settings.mmsServiceId = 1;
|
||||
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.sendMMS,
|
||||
{
|
||||
receivers: ['123'],
|
||||
subject: null,
|
||||
smil: smil.smil,
|
||||
attachments: smil.attachments
|
||||
}, {
|
||||
serviceId: mmsOpts.serviceId
|
||||
}
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('on a non-dual sim setup with different serviceId', function(done) {
|
||||
Settings.hasSeveralSim.returns(false);
|
||||
Settings.mmsServiceId = 1;
|
||||
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.sendMMS,
|
||||
{
|
||||
receivers: ['123'],
|
||||
subject: null,
|
||||
smil: smil.smil,
|
||||
attachments: smil.attachments
|
||||
}, undefined);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('serviceId is a string containing a number', function(done) {
|
||||
mmsOpts.serviceId = '0';
|
||||
Settings.mmsServiceId = 0;
|
||||
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWith(
|
||||
clientStub.sendMMS, sinon.match.any, { serviceId: 0 }
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('serviceId is a bad string', function(done) {
|
||||
mmsOpts.serviceId = 'hjuoriut';
|
||||
Settings.mmsServiceId = 0;
|
||||
|
||||
MessagingService.sendMMS(mmsOpts, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWithExactly(
|
||||
clientStub.sendMMS, sinon.match.any, undefined
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('resendMessage', function() {
|
||||
|
||||
setup(function() {
|
||||
clientStub.delete.returns(Promise.resolve());
|
||||
});
|
||||
|
||||
test('fails if message is not given', function() {
|
||||
assert.throws(function() {
|
||||
MessagingService.resendMessage();
|
||||
});
|
||||
});
|
||||
|
||||
suite('SMS message', function() {
|
||||
var message;
|
||||
|
||||
setup(function() {
|
||||
message = MockMessages.sms({
|
||||
iccId: 100
|
||||
});
|
||||
});
|
||||
|
||||
test('uses message iccId to retrieve service Id in case of multiple SIMs',
|
||||
function(done) {
|
||||
var serviceId = 3;
|
||||
|
||||
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
|
||||
this.sinon.stub(Settings, 'getServiceIdByIccId').returns(serviceId);
|
||||
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWith(
|
||||
Settings.getServiceIdByIccId,
|
||||
message.iccId
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(
|
||||
clientStub.send,
|
||||
sinon.match.any, sinon.match.any, {
|
||||
serviceId: serviceId
|
||||
}
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('deletes old message on service promise resolve', function(done) {
|
||||
clientStub.send.returns(Promise.resolve(successResult));
|
||||
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWith(
|
||||
clientStub.delete, message.id
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('deletes old message on service promise reject', function(done) {
|
||||
clientStub.send.returns(Promise.reject(errorResult));
|
||||
MessagingService.resendMessage(message, APP_INSTANCE_ID).catch(
|
||||
(error) => {
|
||||
sinon.assert.calledWith(clientStub.delete, message.id);
|
||||
|
||||
assert.equal(error, errorResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('MMS message', function() {
|
||||
var message;
|
||||
setup(function() {
|
||||
message = MockMessages.mms({
|
||||
iccId: 100
|
||||
});
|
||||
});
|
||||
|
||||
test('uses message iccId to retrieve service Id in case of multiple SIMs',
|
||||
function(done) {
|
||||
var serviceId = 3;
|
||||
|
||||
clientStub.sendMMS.returns(Promise.resolve(successResult));
|
||||
this.sinon.stub(Settings, 'hasSeveralSim').returns(true);
|
||||
this.sinon.stub(Settings, 'getServiceIdByIccId').returns(serviceId);
|
||||
|
||||
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(
|
||||
(result) => {
|
||||
sinon.assert.calledWith(
|
||||
Settings.getServiceIdByIccId,
|
||||
message.iccId
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(
|
||||
clientStub.sendMMS,{
|
||||
receivers: message.receivers,
|
||||
subject: message.subject,
|
||||
smil: message.smil,
|
||||
attachments: message.attachments
|
||||
}, {
|
||||
serviceId: serviceId
|
||||
}
|
||||
);
|
||||
|
||||
assert.equal(result, successResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('deletes old message on success and calls callback', function(done) {
|
||||
clientStub.sendMMS.returns(Promise.resolve(successResult));
|
||||
MessagingService.resendMessage(message, APP_INSTANCE_ID).then(() => {
|
||||
sinon.assert.calledWith(clientStub.delete, message.id);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('deletes old message on error and calls callback', function(done) {
|
||||
clientStub.sendMMS.returns(Promise.reject(errorResult));
|
||||
MessagingService.resendMessage(message, APP_INSTANCE_ID).catch(
|
||||
(error) => {
|
||||
sinon.assert.calledWith(clientStub.delete, message.id);
|
||||
assert.equal(error, errorResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('retrieveMMS', function() {
|
||||
var id;
|
||||
|
||||
setup(function() {
|
||||
id = 1;
|
||||
});
|
||||
|
||||
test('retrieveMMS success', function(done) {
|
||||
clientStub.retrieveMMS.returns(Promise.resolve(successResult));
|
||||
MessagingService.retrieveMMS(id, APP_INSTANCE_ID).then((result) => {
|
||||
sinon.assert.calledWith(clientStub.retrieveMMS, id);
|
||||
assert.equal(result, successResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('retrieveMMS error', function(done) {
|
||||
clientStub.retrieveMMS.returns(Promise.reject(errorResult));
|
||||
MessagingService.retrieveMMS(id, APP_INSTANCE_ID).catch((error) => {
|
||||
sinon.assert.calledWith(clientStub.retrieveMMS, id);
|
||||
assert.equal(error, errorResult);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
10
gaia/apps/sms/services/test/unit/messaging/mock_messaging_client.js
Executable file
10
gaia/apps/sms/services/test/unit/messaging/mock_messaging_client.js
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
/* exported MockMessagingClient */
|
||||
'use strict';
|
||||
|
||||
var MockMessagingClient = {
|
||||
init: () => {},
|
||||
sendSMS: () => {},
|
||||
sendMMS: () => {},
|
||||
resendMessage: () => {},
|
||||
retrieveMMS: () => {}
|
||||
};
|
||||
13
gaia/apps/sms/services/test/unit/mock_bridge.js
Executable file
13
gaia/apps/sms/services/test/unit/mock_bridge.js
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
/* exported bridge */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.Mockbridge = {
|
||||
client: () => {},
|
||||
service: () => {}
|
||||
};
|
||||
|
||||
exports.MockstreamService = {};
|
||||
exports.MockstreamClient = {};
|
||||
})(window);
|
||||
23
gaia/apps/sms/services/test/unit/mock_drafts.js
Executable file
23
gaia/apps/sms/services/test/unit/mock_drafts.js
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
/*exported MockDrafts,
|
||||
MockDraft
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockDrafts = {
|
||||
init: () => {},
|
||||
add: function() { return this; },
|
||||
delete: function() { return this; },
|
||||
byThreadId: () => null,
|
||||
byDraftId: function() {},
|
||||
clear: function() {},
|
||||
store: () => Promise.resolve(),
|
||||
request: () => Promise.resolve(),
|
||||
getAll: () => [],
|
||||
getAllThreadless: () => [],
|
||||
on: () => {}
|
||||
};
|
||||
|
||||
function MockDraft(draft) {
|
||||
Object.assign(this, { id: 'draftId' }, draft);
|
||||
}
|
||||
30
gaia/apps/sms/services/test/unit/mock_message_manager.js
Executable file
30
gaia/apps/sms/services/test/unit/mock_message_manager.js
Executable file
|
|
@ -0,0 +1,30 @@
|
|||
/* global Promise */
|
||||
|
||||
/* exported MockMessageManager */
|
||||
'use strict';
|
||||
|
||||
var MockMessageManager = {
|
||||
_message: null,
|
||||
init: () => {},
|
||||
getThreads: function() {},
|
||||
getMessages: function() {},
|
||||
getMessage: () => {},
|
||||
deleteMessages: () => Promise.resolve(),
|
||||
sendSMS: function() {
|
||||
return {};
|
||||
},
|
||||
sendMMS: function() {
|
||||
return {};
|
||||
},
|
||||
resendMessage: function() {},
|
||||
retrieveMMS: function() {},
|
||||
markMessagesRead: function() {},
|
||||
markThreadRead: function() {},
|
||||
findThreadFromNumber: function() {},
|
||||
getSegmentInfo: function() {
|
||||
return Promise.reject(new Error('not implemented '));
|
||||
},
|
||||
ensureThreadRegistered() { return Promise.resolve(); },
|
||||
on: function() {},
|
||||
off: function() {}
|
||||
};
|
||||
24
gaia/apps/sms/services/test/unit/mock_moz_mobile_connections.js
Executable file
24
gaia/apps/sms/services/test/unit/mock_moz_mobile_connections.js
Executable file
|
|
@ -0,0 +1,24 @@
|
|||
/*exported MockMozMobileConnections */
|
||||
|
||||
// The real navigator.mozMobileConnections is not a real array
|
||||
'use strict';
|
||||
|
||||
var MockMozMobileConnections = {
|
||||
0: {
|
||||
iccId: 'SIM 1',
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
data: {
|
||||
state: 'searching'
|
||||
}
|
||||
},
|
||||
1: {
|
||||
iccId: 'SIM 2',
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
data: {
|
||||
state: 'searching'
|
||||
}
|
||||
},
|
||||
length: 2
|
||||
};
|
||||
225
gaia/apps/sms/services/test/unit/mock_navigatormoz_sms.js
Executable file
225
gaia/apps/sms/services/test/unit/mock_navigatormoz_sms.js
Executable file
|
|
@ -0,0 +1,225 @@
|
|||
/*exported MockNavigatormozMobileMessage */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockNavigatormozMobileMessage = {
|
||||
_mSmsRequest: null,
|
||||
_mMmsRequest: null,
|
||||
_mSegmentInfoRequests: [],
|
||||
_mMarkReadRequest: null,
|
||||
_mMessagesRequest: null,
|
||||
_mMessageRequest: null,
|
||||
|
||||
send: function(recipients) {
|
||||
this._mSmsRequest = Array.isArray(recipients) ?
|
||||
recipients.map(() => { return {}; }) : {};
|
||||
|
||||
return this._mSmsRequest;
|
||||
},
|
||||
|
||||
sendMMS: function() {
|
||||
this._mMmsRequest = {};
|
||||
return this._mMmsRequest;
|
||||
},
|
||||
|
||||
markMessageRead: function(messageId, value) {
|
||||
this._mMarkReadRequest = {};
|
||||
return this._mMarkReadRequest;
|
||||
},
|
||||
|
||||
/**
|
||||
* mTriggerMarkReadSuccess returns true if there was a read request to act
|
||||
* upon, false otherwise
|
||||
*/
|
||||
mTriggerMarkReadSuccess: function() {
|
||||
var evt = { target: { result: null } };
|
||||
|
||||
if (this._mMarkReadRequest && this._mMarkReadRequest.onsuccess) {
|
||||
var current = this._mMarkReadRequest;
|
||||
this._mMarkReadRequest = null;
|
||||
current.onsuccess.call(evt.target, evt);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* mTriggerMarkReadError returns true if there was a read request to act
|
||||
* upon, false otherwise
|
||||
*/
|
||||
mTriggerMarkReadError: function(errorName) {
|
||||
var evt = { target: { error: { name: errorName || null } } };
|
||||
if (this._mMarkReadRequest && this._mMarkReadRequest.onerror) {
|
||||
var current = this._mMarkReadRequest;
|
||||
this._mMarkReadRequest = null;
|
||||
current.onerror.call(evt.target, evt);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
getMessages: function(filter, invert) {
|
||||
this._mMessagesRequest = {};
|
||||
return this._mMessagesRequest;
|
||||
},
|
||||
|
||||
getMessage: function(aMessageID) {
|
||||
this._mMessageRequest = {};
|
||||
return this._mMessageRequest;
|
||||
},
|
||||
|
||||
mTriggerMessagesRequest: function(messages) {
|
||||
var mock = this;
|
||||
if (this._mMessagesRequest && this._mMessagesRequest.onsuccess) {
|
||||
var cursor = {
|
||||
done: false,
|
||||
continue: function() {
|
||||
var next = messages.shift();
|
||||
if (next === undefined) {
|
||||
this.result = null;
|
||||
this.done = true;
|
||||
} else {
|
||||
this.result = next;
|
||||
}
|
||||
|
||||
var evt = { target: cursor };
|
||||
mock._mMessagesRequest.onsuccess.call(this, evt);
|
||||
}
|
||||
};
|
||||
|
||||
cursor.continue();
|
||||
}
|
||||
},
|
||||
|
||||
mTriggerSuccessMessageRequest: function() {
|
||||
if (this._mMessageRequest && this._mMessageRequest.onsuccess) {
|
||||
this._mMessageRequest.onsuccess.call(this, null);
|
||||
}
|
||||
},
|
||||
getSegmentInfoForText: function() {
|
||||
var lastRequest = {};
|
||||
this._mSegmentInfoRequests.push(lastRequest);
|
||||
return lastRequest;
|
||||
},
|
||||
|
||||
mTriggerSegmentInfoSuccess: function(segmentInfo, index) {
|
||||
segmentInfo = segmentInfo || this._mDefaultSegmentInfo;
|
||||
var evt = { target: { result: segmentInfo } };
|
||||
|
||||
if (this._mSegmentInfoRequests && this._mSegmentInfoRequests.length) {
|
||||
if (index === undefined) {
|
||||
// trigger the last one
|
||||
index = this._mSegmentInfoRequests.length - 1;
|
||||
}
|
||||
|
||||
var request = this._mSegmentInfoRequests.splice(index, 1)[0];
|
||||
if (request.onsuccess) {
|
||||
request.onsuccess.call(evt.target, evt);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mTriggerSegmentInfoError: function(index) {
|
||||
var evt = { target: { error: { name: 'SegmentInfoError' } } };
|
||||
|
||||
if (this._mSegmentInfoRequests && this._mSegmentInfoRequests.length) {
|
||||
if (index === undefined) {
|
||||
// trigger the last one
|
||||
index = this._mSegmentInfoRequests.length - 1;
|
||||
}
|
||||
|
||||
var request = this._mSegmentInfoRequests.splice(index, 1)[0];
|
||||
if (request.onerror) {
|
||||
request.onerror.call(evt.target, evt);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
getThreads: function() {
|
||||
var cursor = {
|
||||
result: null
|
||||
};
|
||||
|
||||
var evt = { target: {} };
|
||||
setTimeout(function() {
|
||||
cursor.onsuccess.call(evt.target, evt);
|
||||
});
|
||||
return cursor;
|
||||
},
|
||||
|
||||
retrieveMMS: function() {},
|
||||
|
||||
_mDefaultSegmentInfo: {
|
||||
segments: 1,
|
||||
charsAvailableInLastSegment: 150
|
||||
},
|
||||
|
||||
mTriggerSmsOnSuccess: function() {
|
||||
var evt = { target: { result: null } };
|
||||
|
||||
if (this._mSmsRequest) {
|
||||
if (Array.isArray(this._mSmsRequest)) {
|
||||
this._mSmsRequest.forEach(function(request) {
|
||||
if (request.onsuccess) {
|
||||
request.onsuccess.call(evt.target, evt);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._mSmsRequest.onsuccess &&
|
||||
this._mSmsRequest.onsuccess.call(evt.target, evt);
|
||||
}
|
||||
this._mSmsRequest = null;
|
||||
}
|
||||
},
|
||||
|
||||
mTriggerSmsOnError: function() {
|
||||
var evt = { target: { error: { name: null } } };
|
||||
|
||||
if (this._mSmsRequest) {
|
||||
if (Array.isArray(this._mSmsRequest)) {
|
||||
this._mSmsRequest.forEach(function(request) {
|
||||
if (request.onerror) {
|
||||
request.onerror.call(evt.target, evt);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._mSmsRequest.onerror &&
|
||||
this._mSmsRequest.onerror.call(evt.target, evt);
|
||||
}
|
||||
|
||||
this._mSmsRequest = null;
|
||||
}
|
||||
},
|
||||
|
||||
mTriggerMmsOnSuccess: function() {
|
||||
var evt = { target: { result: null } };
|
||||
|
||||
if (this._mMmsRequest && this._mMmsRequest.onsuccess) {
|
||||
this._mMmsRequest.onsuccess.call(evt.target, evt);
|
||||
this._mMmsRequest = null;
|
||||
}
|
||||
},
|
||||
|
||||
mTriggerMmsOnError: function() {
|
||||
var evt = { target: { error: { name: null } } };
|
||||
|
||||
if (this._mMmsRequest && this._mMmsRequest.onerror) {
|
||||
this._mMmsRequest.onerror.call(evt.target, evt);
|
||||
this._mMmsRequest = null;
|
||||
}
|
||||
},
|
||||
|
||||
mTeardown: function() {
|
||||
this._mSegmentInfoRequests = [];
|
||||
this._mSmsRequest = null;
|
||||
this._mMmsRequest = null;
|
||||
this._mMarkReadRequest = null;
|
||||
this._mMessagesRequest = null;
|
||||
this._mMessageRequest = null;
|
||||
},
|
||||
|
||||
addEventListener: function() {}
|
||||
};
|
||||
5
gaia/apps/sms/services/test/unit/mock_shared_worker.js
Executable file
5
gaia/apps/sms/services/test/unit/mock_shared_worker.js
Executable file
|
|
@ -0,0 +1,5 @@
|
|||
/*exported MockSharedWorker */
|
||||
|
||||
'use strict';
|
||||
|
||||
function MockSharedWorker() {}
|
||||
25
gaia/apps/sms/services/test/unit/mock_threads.js
Executable file
25
gaia/apps/sms/services/test/unit/mock_threads.js
Executable file
|
|
@ -0,0 +1,25 @@
|
|||
/*exported MockThreads, MockThread */
|
||||
|
||||
'use strict';
|
||||
|
||||
var MockThreads = {
|
||||
registerMessage: () => {},
|
||||
unregisterMessage: () => {},
|
||||
has: () => false,
|
||||
set: () => {},
|
||||
get: () => new MockThread(),
|
||||
delete: () => {},
|
||||
clear: () => {},
|
||||
keys: () => []
|
||||
};
|
||||
|
||||
MockThreads.Messages = {
|
||||
get: () => {}
|
||||
};
|
||||
|
||||
function MockThread(thread) {
|
||||
Object.assign(this, { messages: new Map() }, thread);
|
||||
}
|
||||
|
||||
MockThread.prototype.getDraft = () => null;
|
||||
MockThread.create = () => {};
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
/* exported MockMozMobileConnectionsClient */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.MockMozMobileConnectionsClient = {
|
||||
init: () => {},
|
||||
switchMmsSimHandler: () => {}
|
||||
};
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
/*global bridge,
|
||||
BroadcastChannel,
|
||||
MozMobileConnectionsClient,
|
||||
MocksHelper
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/views/shared/test/unit/mock_broadcast_channel.js');
|
||||
require('/services/js/moz_mobile_connections/moz_mobile_connections_client.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'BroadcastChannel'
|
||||
]).init();
|
||||
|
||||
suite('MozMobileConnectionsClient >', function() {
|
||||
var clientStub;
|
||||
var APP_INSTANCE_ID = 'fake-id';
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
clientStub = sinon.stub({
|
||||
method: () => {}
|
||||
});
|
||||
|
||||
this.sinon.stub(bridge, 'client').returns(clientStub);
|
||||
});
|
||||
|
||||
suite('init', function() {
|
||||
test('throws if app instance id is not provided', function() {
|
||||
assert.throws(() => MozMobileConnectionsClient.init());
|
||||
});
|
||||
|
||||
test('client is initialized', function() {
|
||||
MozMobileConnectionsClient.init(APP_INSTANCE_ID);
|
||||
|
||||
sinon.assert.calledWith(bridge.client, {
|
||||
service: 'moz-mobile-connections-shim',
|
||||
endpoint: sinon.match.instanceOf(BroadcastChannel).and(
|
||||
sinon.match.has(
|
||||
'name',
|
||||
'moz-mobile-connections-shim-channel-' + APP_INSTANCE_ID
|
||||
)
|
||||
),
|
||||
timeout: false
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('switchMmsSimHandler method', function() {
|
||||
setup(function() {
|
||||
MozMobileConnectionsClient.init(APP_INSTANCE_ID);
|
||||
});
|
||||
|
||||
test('client method is called with correct parameter', function() {
|
||||
var iccId = 123;
|
||||
var fakePromise = 'fakePromise';
|
||||
|
||||
clientStub.method.withArgs('switchMmsSimHandler', iccId).returns(
|
||||
fakePromise
|
||||
);
|
||||
assert.equal(
|
||||
fakePromise,
|
||||
MozMobileConnectionsClient.switchMmsSimHandler(iccId)
|
||||
);
|
||||
sinon.assert.calledWithMatch(
|
||||
clientStub.method,
|
||||
'switchMmsSimHandler',
|
||||
iccId
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
/*global bridge,
|
||||
BroadcastChannel,
|
||||
MockMozMobileConnections,
|
||||
MocksHelper,
|
||||
MozMobileConnectionsShim,
|
||||
MozSettingsShim
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/services/test/unit/mock_moz_mobile_connections.js');
|
||||
require('/services/test/unit/moz_settings/mock_moz_settings_shim.js');
|
||||
require('/views/shared/test/unit/mock_broadcast_channel.js');
|
||||
require('/services/js/bridge_service_mixin.js');
|
||||
require('/services/js/moz_mobile_connections/moz_mobile_connections_shim.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'BroadcastChannel',
|
||||
'MozMobileConnections',
|
||||
'MozSettingsShim'
|
||||
]).init();
|
||||
|
||||
suite('MozMobileConnectionsShim >', function() {
|
||||
var serviceStub;
|
||||
|
||||
const APP_ID = 'fake-app-id';
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
suiteSetup(function() {
|
||||
serviceStub = sinon.stub({
|
||||
method: () => {},
|
||||
listen: () => {}
|
||||
});
|
||||
|
||||
sinon.spy(window, 'BroadcastChannel');
|
||||
sinon.stub(bridge, 'service').returns(serviceStub);
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
MozMobileConnectionsShim.init(APP_ID, MockMozMobileConnections);
|
||||
});
|
||||
|
||||
test('bridge service is correctly initialized', function() {
|
||||
sinon.assert.calledOnce(bridge.service);
|
||||
sinon.assert.calledWith(bridge.service, 'moz-mobile-connections-shim');
|
||||
sinon.assert.calledTwice(serviceStub.listen);
|
||||
sinon.assert.calledWith(
|
||||
serviceStub.listen,
|
||||
sinon.match.instanceOf(BroadcastChannel)
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(
|
||||
BroadcastChannel,
|
||||
'moz-mobile-connections-shim-channel-' + APP_ID
|
||||
);
|
||||
});
|
||||
|
||||
suite('methods for wrapping up API', function() {
|
||||
suite('getServiceIdByIccId', function() {
|
||||
test('returns the correct id', function() {
|
||||
assert.equal(MozMobileConnectionsShim.getServiceIdByIccId('SIM 1'), 0);
|
||||
assert.equal(MozMobileConnectionsShim.getServiceIdByIccId('SIM 2'), 1);
|
||||
assert.isNull(MozMobileConnectionsShim.getServiceIdByIccId('SIM 3'));
|
||||
});
|
||||
});
|
||||
|
||||
suite('switchMmsSimHandler', function() {
|
||||
setup(function() {
|
||||
this.sinon.stub(MozSettingsShim, 'set');
|
||||
});
|
||||
|
||||
test('serviceId is null', function(done) {
|
||||
MozMobileConnectionsShim.switchMmsSimHandler('SIM 3').then(() => {
|
||||
throw new Error('Resolve should not be called');
|
||||
}, (err) => {
|
||||
assert.equal(err, 'NoSimCardError');
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('mozMobileConnection is already registered', function(done) {
|
||||
MockMozMobileConnections[0].data.state = 'registered';
|
||||
|
||||
MozMobileConnectionsShim.switchMmsSimHandler('SIM 1').then(() => {
|
||||
// No need to set the default serviceId
|
||||
sinon.assert.notCalled(MozSettingsShim.set);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('switch sim and return when state is ready', function(done) {
|
||||
this.sinon.stub(MockMozMobileConnections[0], 'addEventListener');
|
||||
this.sinon.stub(MockMozMobileConnections[0], 'removeEventListener');
|
||||
MockMozMobileConnections[0].data.state = 'searching';
|
||||
|
||||
MozMobileConnectionsShim.switchMmsSimHandler('SIM 1').then(() => {
|
||||
sinon.assert.calledWith(MozSettingsShim.set, {
|
||||
'ril.mms.defaultServiceId': 0,
|
||||
'ril.data.defaultServiceId': 0
|
||||
});
|
||||
sinon.assert.called(MockMozMobileConnections[0].removeEventListener);
|
||||
}).then(done, done);
|
||||
|
||||
// Change target connection state to registered and invoke the event
|
||||
MockMozMobileConnections[0].data.state = 'registered';
|
||||
MockMozMobileConnections[0].addEventListener.yield();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/* exported MockMozMobileMessageClient */
|
||||
|
||||
(function(exports) {
|
||||
'use strict';
|
||||
|
||||
exports.MockMozMobileMessageClient = {
|
||||
forApp: () => {}
|
||||
};
|
||||
})(window);
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/*global bridge,
|
||||
BroadcastChannel,
|
||||
MozMobileMessageClient,
|
||||
MocksHelper,
|
||||
streamClient
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/views/shared/test/unit/mock_broadcast_channel.js');
|
||||
require('/services/js/moz_mobile_message/moz_mobile_message_client.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'BroadcastChannel',
|
||||
'streamClient'
|
||||
]).init();
|
||||
|
||||
suite('MozMobileMessageClient >', function() {
|
||||
var clientStub;
|
||||
|
||||
function matchMobileMessageShim(channelName) {
|
||||
return {
|
||||
service: 'moz-mobile-message-shim',
|
||||
endpoint: sinon.match.instanceOf(BroadcastChannel).and(
|
||||
sinon.match.has('name', channelName)
|
||||
),
|
||||
timeout: false
|
||||
};
|
||||
}
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
clientStub = sinon.stub({
|
||||
method: () => {},
|
||||
stream: () => {},
|
||||
plugin: () => {}
|
||||
});
|
||||
clientStub.plugin.returns(clientStub);
|
||||
|
||||
this.sinon.spy(self, 'BroadcastChannel');
|
||||
|
||||
this.sinon.stub(bridge, 'client').withArgs(
|
||||
matchMobileMessageShim('moz-mobile-message-shim-channel-1')
|
||||
).returns(clientStub);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
MozMobileMessageClient.cleanup();
|
||||
});
|
||||
|
||||
test('throws if app instance id is not provided', function() {
|
||||
assert.throws(() => MozMobileMessageClient.forApp());
|
||||
});
|
||||
|
||||
test('spawns bridge client for every app instance', function() {
|
||||
// Prepare stub for the second app instance.
|
||||
var clientStub2 = sinon.stub({
|
||||
method: () => {},
|
||||
stream: () => {},
|
||||
plugin: () => {}
|
||||
});
|
||||
clientStub2.plugin.returns(clientStub2);
|
||||
|
||||
bridge.client.withArgs(
|
||||
matchMobileMessageShim('moz-mobile-message-shim-channel-2')
|
||||
).returns(clientStub2);
|
||||
|
||||
var mobileMessageClient1 = MozMobileMessageClient.forApp(1);
|
||||
sinon.assert.calledOnce(bridge.client);
|
||||
sinon.assert.calledWith(
|
||||
bridge.client,
|
||||
matchMobileMessageShim('moz-mobile-message-shim-channel-1')
|
||||
);
|
||||
sinon.assert.calledWith(clientStub.plugin, streamClient);
|
||||
|
||||
var mobileMessageClient2 = MozMobileMessageClient.forApp(2);
|
||||
sinon.assert.calledTwice(bridge.client);
|
||||
sinon.assert.calledWith(
|
||||
bridge.client,
|
||||
matchMobileMessageShim('moz-mobile-message-shim-channel-2')
|
||||
);
|
||||
sinon.assert.calledWith(clientStub2.plugin, streamClient);
|
||||
|
||||
// Once we created client for the app instance we should reuse for
|
||||
// consequent requests.
|
||||
bridge.client.reset();
|
||||
|
||||
MozMobileMessageClient.forApp(1);
|
||||
MozMobileMessageClient.forApp(2);
|
||||
|
||||
sinon.assert.notCalled(bridge.client);
|
||||
|
||||
// Check that method calls go to the correct client.
|
||||
mobileMessageClient1.send();
|
||||
sinon.assert.calledOnce(clientStub.method);
|
||||
sinon.assert.notCalled(clientStub2.method);
|
||||
|
||||
mobileMessageClient2.send();
|
||||
sinon.assert.calledOnce(clientStub.method);
|
||||
sinon.assert.calledOnce(clientStub2.method);
|
||||
});
|
||||
|
||||
suite('method calls', function() {
|
||||
var mobileMessageClient, methodResult, streamResult;
|
||||
setup(function() {
|
||||
methodResult = sinon.stub();
|
||||
streamResult = sinon.stub();
|
||||
|
||||
clientStub.method.returns(methodResult);
|
||||
clientStub.stream.returns(streamResult);
|
||||
|
||||
mobileMessageClient = MozMobileMessageClient.forApp(1);
|
||||
});
|
||||
|
||||
test('send is correctly forwarded to bridge client', function() {
|
||||
mobileMessageClient.send('recipient', 'content', { serviceId: 100 });
|
||||
|
||||
sinon.assert.calledWith(
|
||||
clientStub.method, 'send', 'recipient', 'content', { serviceId: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
test('sendMMS is correctly forwarded to bridge client', function() {
|
||||
var mmsOptions = {
|
||||
receivers: ['+1'],
|
||||
subject: 'subject',
|
||||
smil: 'smil',
|
||||
attachments: [{}]
|
||||
};
|
||||
|
||||
mobileMessageClient.sendMMS(mmsOptions, { serviceId: 100 });
|
||||
|
||||
sinon.assert.calledWith(
|
||||
clientStub.method, 'sendMMS', mmsOptions, { serviceId: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
test('retrieveMMS is correctly forwarded to bridge client', function() {
|
||||
mobileMessageClient.retrieveMMS(100);
|
||||
|
||||
sinon.assert.calledWith(clientStub.method, 'retrieveMMS', 100);
|
||||
});
|
||||
|
||||
test('delete is correctly forwarded to bridge client', function() {
|
||||
mobileMessageClient.delete(100);
|
||||
|
||||
sinon.assert.calledWith(clientStub.method, 'delete', 100);
|
||||
});
|
||||
|
||||
test('getThreads is correctly forwarded to bridge client', function() {
|
||||
mobileMessageClient.getThreads();
|
||||
|
||||
sinon.assert.calledWith(clientStub.stream, 'getThreads');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,391 @@
|
|||
/*global bridge,
|
||||
BroadcastChannel,
|
||||
DOMError,
|
||||
MockNavigatormozMobileMessage,
|
||||
MocksHelper,
|
||||
MozMobileMessageShim,
|
||||
streamService
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/services/test/unit/mock_navigatormoz_sms.js');
|
||||
require('/views/shared/test/unit/mock_broadcast_channel.js');
|
||||
require('/views/shared/js/utils.js');
|
||||
require('/services/js/bridge_service_mixin.js');
|
||||
require('/services/js/moz_mobile_message/moz_mobile_message_shim.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge',
|
||||
'BroadcastChannel',
|
||||
'streamService'
|
||||
]).init();
|
||||
|
||||
suite('MozMobileMessageShim >', function() {
|
||||
var serviceStub;
|
||||
|
||||
const APP_ID = 'fake-app-id';
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
suiteSetup(function() {
|
||||
serviceStub = sinon.stub({
|
||||
method: () => {},
|
||||
stream: () => {},
|
||||
broadcast: () => {},
|
||||
plugin: () => {},
|
||||
listen: () => {}
|
||||
});
|
||||
|
||||
sinon.spy(window, 'BroadcastChannel');
|
||||
sinon.stub(bridge, 'service').returns(serviceStub);
|
||||
sinon.stub(MockNavigatormozMobileMessage, 'addEventListener');
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
MozMobileMessageShim.init(APP_ID, MockNavigatormozMobileMessage);
|
||||
});
|
||||
|
||||
test('bridge service is correctly initialized', function() {
|
||||
sinon.assert.calledOnce(bridge.service);
|
||||
sinon.assert.calledWith(bridge.service, 'moz-mobile-message-shim');
|
||||
sinon.assert.calledWith(serviceStub.plugin, streamService);
|
||||
sinon.assert.calledTwice(serviceStub.listen);
|
||||
sinon.assert.calledWith(
|
||||
serviceStub.listen,
|
||||
sinon.match.instanceOf(BroadcastChannel)
|
||||
);
|
||||
|
||||
sinon.assert.calledWith(
|
||||
BroadcastChannel,
|
||||
'moz-mobile-message-shim-channel-' + APP_ID
|
||||
);
|
||||
});
|
||||
|
||||
suite('event broadcast', function() {
|
||||
var messageEvent, deleteEvent, eventMap;
|
||||
|
||||
setup(function() {
|
||||
messageEvent = { message: {} };
|
||||
deleteEvent = { deletedThreadIds: [1, 2, 3] };
|
||||
eventMap = new Map([
|
||||
['sending', {
|
||||
apiEvent: messageEvent,
|
||||
broadcastEventName: 'message-sending',
|
||||
broadcastParameters: { message: messageEvent.message }
|
||||
}],
|
||||
['failed', {
|
||||
apiEvent: messageEvent,
|
||||
broadcastEventName: 'message-failed-to-send',
|
||||
broadcastParameters: { message: messageEvent.message }
|
||||
}],
|
||||
['deliverysuccess', {
|
||||
apiEvent: messageEvent,
|
||||
broadcastEventName: 'message-delivered',
|
||||
broadcastParameters: { message: messageEvent.message }
|
||||
}],
|
||||
['readsuccess', {
|
||||
apiEvent: messageEvent,
|
||||
broadcastEventName: 'message-read',
|
||||
broadcastParameters: { message: messageEvent.message }
|
||||
}],
|
||||
['sent', {
|
||||
apiEvent: messageEvent,
|
||||
broadcastEventName: 'message-sent',
|
||||
broadcastParameters: { message: messageEvent.message }
|
||||
}],
|
||||
['deleted', {
|
||||
apiEvent: deleteEvent,
|
||||
broadcastEventName: 'threads-deleted',
|
||||
broadcastParameters: { ids: deleteEvent.deletedThreadIds }
|
||||
}]
|
||||
]);
|
||||
});
|
||||
|
||||
test('event broadcasted by service with correct parameter', function() {
|
||||
eventMap.forEach((eventDescription, apiEventName) => {
|
||||
MockNavigatormozMobileMessage.addEventListener
|
||||
.withArgs(apiEventName).yield(eventDescription.apiEvent);
|
||||
|
||||
sinon.assert.calledWith(
|
||||
serviceStub.broadcast,
|
||||
eventDescription.broadcastEventName,
|
||||
eventDescription.broadcastParameters
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('methods for wrapping up API', function() {
|
||||
|
||||
setup(function() {
|
||||
this.sinon.spy(MockNavigatormozMobileMessage, 'getMessage');
|
||||
this.sinon.stub(MockNavigatormozMobileMessage, 'retrieveMMS');
|
||||
this.sinon.spy(MockNavigatormozMobileMessage, 'send');
|
||||
this.sinon.spy(MockNavigatormozMobileMessage, 'sendMMS');
|
||||
this.sinon.spy(MockNavigatormozMobileMessage, 'markMessageRead');
|
||||
this.sinon.spy(MockNavigatormozMobileMessage, 'getSegmentInfoForText');
|
||||
MockNavigatormozMobileMessage.delete = sinon.stub();
|
||||
});
|
||||
|
||||
test('getMessage', function() {
|
||||
var args = [1];
|
||||
|
||||
assert.equal(
|
||||
MozMobileMessageShim.getMessage(...args),
|
||||
MockNavigatormozMobileMessage.getMessage.firstCall.returnValue
|
||||
);
|
||||
sinon.assert.calledWith(
|
||||
MockNavigatormozMobileMessage.getMessage,
|
||||
...args
|
||||
);
|
||||
});
|
||||
|
||||
suite('retrieveMMS', function() {
|
||||
var args;
|
||||
|
||||
setup(function() {
|
||||
args = [1];
|
||||
});
|
||||
|
||||
test('success', function(done) {
|
||||
MockNavigatormozMobileMessage.retrieveMMS.returns(Promise.resolve({
|
||||
id: 'fake message'
|
||||
}));
|
||||
MozMobileMessageShim.retrieveMMS(...args).then(() => {
|
||||
sinon.assert.calledWith(
|
||||
MockNavigatormozMobileMessage.retrieveMMS,
|
||||
...args
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('DOMError', function(done) {
|
||||
var error = new DOMError('fakeError');
|
||||
MockNavigatormozMobileMessage.retrieveMMS.returns(
|
||||
Promise.reject(error)
|
||||
);
|
||||
MozMobileMessageShim.retrieveMMS(...args).catch((err) => {
|
||||
assert.equal(err, error);
|
||||
sinon.assert.calledWith(
|
||||
MockNavigatormozMobileMessage.retrieveMMS,
|
||||
...args
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('other error', function(done) {
|
||||
var error = 'fakeError';
|
||||
MockNavigatormozMobileMessage.retrieveMMS.returns(
|
||||
Promise.reject(error)
|
||||
);
|
||||
MozMobileMessageShim.retrieveMMS(...args).catch((err) => {
|
||||
assert.equal(err, error);
|
||||
sinon.assert.calledWith(
|
||||
MockNavigatormozMobileMessage.retrieveMMS,
|
||||
...args
|
||||
);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
test('send', function() {
|
||||
var args = [['123'], 'body', {}];
|
||||
|
||||
assert.equal(
|
||||
MozMobileMessageShim.send(...args),
|
||||
MockNavigatormozMobileMessage.send.firstCall.returnValue
|
||||
);
|
||||
sinon.assert.calledWith(MockNavigatormozMobileMessage.send, ...args);
|
||||
});
|
||||
|
||||
test('sendMMS', function() {
|
||||
var args = [{}, {}];
|
||||
|
||||
assert.equal(
|
||||
MozMobileMessageShim.sendMMS(...args),
|
||||
MockNavigatormozMobileMessage.sendMMS.firstCall.returnValue
|
||||
);
|
||||
sinon.assert.calledWith(MockNavigatormozMobileMessage.sendMMS, ...args);
|
||||
});
|
||||
|
||||
test('delete', function() {
|
||||
var args = [1];
|
||||
|
||||
assert.equal(
|
||||
MozMobileMessageShim.delete(...args),
|
||||
MockNavigatormozMobileMessage.delete.firstCall.returnValue
|
||||
);
|
||||
sinon.assert.calledWith(MockNavigatormozMobileMessage.delete, ...args);
|
||||
});
|
||||
|
||||
test('markMessageRead', function() {
|
||||
var args = [1, true, {}];
|
||||
|
||||
assert.equal(
|
||||
MozMobileMessageShim.markMessageRead(...args),
|
||||
MockNavigatormozMobileMessage.markMessageRead.firstCall.returnValue
|
||||
);
|
||||
sinon.assert.calledWith(
|
||||
MockNavigatormozMobileMessage.markMessageRead,
|
||||
...args
|
||||
);
|
||||
});
|
||||
|
||||
test('getSegmentInfoForText', function() {
|
||||
var args = ['body'];
|
||||
var mockAPI = MockNavigatormozMobileMessage.getSegmentInfoForText;
|
||||
|
||||
assert.equal(
|
||||
MozMobileMessageShim.getSegmentInfoForText(...args),
|
||||
mockAPI.firstCall.returnValue
|
||||
);
|
||||
sinon.assert.calledWith(mockAPI, ...args);
|
||||
});
|
||||
});
|
||||
|
||||
suite('streams for wrapping up API', function() {
|
||||
var streamStub;
|
||||
|
||||
setup(function() {
|
||||
streamStub = sinon.stub({
|
||||
write: () => {},
|
||||
close: () => {},
|
||||
abort: () => {}
|
||||
});
|
||||
});
|
||||
|
||||
suite('getThreads', function() {
|
||||
var cursor;
|
||||
|
||||
setup(function() {
|
||||
cursor = {
|
||||
result: {
|
||||
id: 1,
|
||||
body: 'body',
|
||||
participants: ['+1234'],
|
||||
timestamp: 0,
|
||||
unreadCount: 0,
|
||||
lastMessageType: 'sms'
|
||||
},
|
||||
|
||||
continue: sinon.stub()
|
||||
};
|
||||
|
||||
this.sinon.stub(MockNavigatormozMobileMessage, 'getThreads');
|
||||
});
|
||||
|
||||
test('continue', function() {
|
||||
MockNavigatormozMobileMessage.getThreads.returns(cursor);
|
||||
MozMobileMessageShim.getThreads(streamStub);
|
||||
|
||||
cursor.onsuccess();
|
||||
|
||||
sinon.assert.calledWith(streamStub.write, cursor.result);
|
||||
sinon.assert.called(cursor.continue);
|
||||
});
|
||||
|
||||
test('stream cancelled', function() {
|
||||
MockNavigatormozMobileMessage.getThreads.returns(cursor);
|
||||
MozMobileMessageShim.getThreads(streamStub);
|
||||
|
||||
streamStub.cancel();
|
||||
|
||||
cursor.onsuccess();
|
||||
|
||||
sinon.assert.notCalled(streamStub.write);
|
||||
sinon.assert.notCalled(cursor.continue);
|
||||
sinon.assert.called(streamStub.close);
|
||||
});
|
||||
|
||||
test('done', function() {
|
||||
MockNavigatormozMobileMessage.getThreads.returns(cursor);
|
||||
MozMobileMessageShim.getThreads(streamStub);
|
||||
cursor.result = null;
|
||||
cursor.onsuccess();
|
||||
|
||||
sinon.assert.called(streamStub.close);
|
||||
});
|
||||
|
||||
test('error while retrieving threads', function() {
|
||||
var error = new Error('retrieving error');
|
||||
|
||||
this.sinon.spy(console, 'error');
|
||||
MockNavigatormozMobileMessage.getThreads.throws(error);
|
||||
MozMobileMessageShim.getThreads(streamStub);
|
||||
|
||||
sinon.assert.calledWith(
|
||||
console.error, 'Error occurred while retrieving threads:', error
|
||||
);
|
||||
sinon.assert.calledWith(streamStub.abort, '[Error] retrieving error');
|
||||
});
|
||||
|
||||
test('error while reading the database', function() {
|
||||
MockNavigatormozMobileMessage.getThreads.returns(cursor);
|
||||
MozMobileMessageShim.getThreads(streamStub);
|
||||
cursor.error = new Error('fake error');
|
||||
this.sinon.spy(console, 'error');
|
||||
cursor.onerror();
|
||||
|
||||
sinon.assert.calledWith(
|
||||
console.error,
|
||||
'Error occurred while reading the database',
|
||||
cursor.error
|
||||
);
|
||||
sinon.assert.calledWith(streamStub.abort, '[Error] fake error');
|
||||
});
|
||||
});
|
||||
|
||||
suite('getMessages', function() {
|
||||
var opts, cursor;
|
||||
|
||||
setup(function() {
|
||||
opts = {
|
||||
invert: true,
|
||||
filter: {}
|
||||
};
|
||||
cursor = {};
|
||||
|
||||
this.sinon.stub(MockNavigatormozMobileMessage, 'getMessages')
|
||||
.withArgs(opts.filter, !opts.invert).returns(cursor);
|
||||
MozMobileMessageShim.getMessages(streamStub, opts);
|
||||
});
|
||||
|
||||
test('continue', function() {
|
||||
cursor.done = false;
|
||||
cursor.result = {};
|
||||
cursor.continue = sinon.stub();
|
||||
cursor.onsuccess();
|
||||
|
||||
sinon.assert.calledWith(streamStub.write, cursor.result);
|
||||
sinon.assert.called(cursor.continue);
|
||||
});
|
||||
|
||||
test('done', function() {
|
||||
cursor.done = true;
|
||||
cursor.onsuccess();
|
||||
|
||||
sinon.assert.called(streamStub.close);
|
||||
});
|
||||
|
||||
test('error', function() {
|
||||
cursor.error = { name: 'fake error' };
|
||||
this.sinon.spy(console, 'error');
|
||||
cursor.onerror();
|
||||
|
||||
sinon.assert.calledWith(
|
||||
console.error,
|
||||
'Reading the database. Error: fake error'
|
||||
);
|
||||
sinon.assert.called(streamStub.abort);
|
||||
});
|
||||
|
||||
test('cancel stream', function() {
|
||||
streamStub.cancel();
|
||||
|
||||
sinon.assert.called(streamStub.close);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
6
gaia/apps/sms/services/test/unit/moz_settings/mock_moz_settings_shim.js
Executable file
6
gaia/apps/sms/services/test/unit/moz_settings/mock_moz_settings_shim.js
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
/*exported MockMozSettingsShim */
|
||||
'use strict';
|
||||
|
||||
var MockMozSettingsShim = {
|
||||
set: () => {}
|
||||
};
|
||||
106
gaia/apps/sms/services/test/unit/moz_settings/moz_settings_shim_test.js
Executable file
106
gaia/apps/sms/services/test/unit/moz_settings/moz_settings_shim_test.js
Executable file
|
|
@ -0,0 +1,106 @@
|
|||
/*global bridge,
|
||||
MockNavigatorSettings,
|
||||
MocksHelper,
|
||||
MozSettingsShim
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/test/unit/mock_bridge.js');
|
||||
require('/services/js/bridge_service_mixin.js');
|
||||
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
|
||||
require('/services/js/moz_settings/moz_settings_shim.js');
|
||||
|
||||
var MocksHelperForAttachment = new MocksHelper([
|
||||
'bridge'
|
||||
]).init();
|
||||
|
||||
suite('MozSettingsShim >', function() {
|
||||
var serviceStub;
|
||||
|
||||
MocksHelperForAttachment.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
serviceStub = sinon.stub({
|
||||
method: () => {},
|
||||
listen: () => {}
|
||||
});
|
||||
|
||||
this.sinon.stub(bridge, 'service').returns(serviceStub);
|
||||
});
|
||||
|
||||
test('service is correctly initialized with settings provided', function() {
|
||||
MozSettingsShim.init(MockNavigatorSettings);
|
||||
|
||||
sinon.assert.calledOnce(bridge.service);
|
||||
sinon.assert.calledWith(bridge.service, 'moz-settings-shim');
|
||||
});
|
||||
|
||||
test('service isn\'t initialized without settings', function() {
|
||||
this.sinon.stub(console, 'error');
|
||||
|
||||
MozSettingsShim.init();
|
||||
|
||||
sinon.assert.notCalled(bridge.service);
|
||||
sinon.assert.called(console.error);
|
||||
});
|
||||
|
||||
suite('Settings methods >', function() {
|
||||
var lockStub;
|
||||
|
||||
setup(function() {
|
||||
lockStub = sinon.stub({
|
||||
get: () => {},
|
||||
set: () => {}
|
||||
});
|
||||
this.sinon.stub(MockNavigatorSettings, 'createLock').returns(lockStub);
|
||||
MozSettingsShim.init(MockNavigatorSettings);
|
||||
});
|
||||
|
||||
suite('get >', function() {
|
||||
|
||||
test('returns value successfully', function(done) {
|
||||
var key = 'key';
|
||||
var value = 'result';
|
||||
lockStub.get.withArgs(key).returns(Promise.resolve(value));
|
||||
MozSettingsShim.get(key).then((result) => {
|
||||
sinon.assert.calledWith(lockStub.get, key);
|
||||
assert.equal(result, value);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('returns reject while error', function(done) {
|
||||
var error = new Error('moz settings get error');
|
||||
lockStub.get.returns(Promise.reject(error));
|
||||
MozSettingsShim.get().then(() => {
|
||||
throw new Error('Should not be resolved');
|
||||
}, (result) => {
|
||||
sinon.assert.called(lockStub.get);
|
||||
assert.equal(result, error);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
|
||||
suite('set >', function() {
|
||||
|
||||
test('returns revolve successfully', function(done) {
|
||||
var settings = { 'key': 'value' };
|
||||
lockStub.set.withArgs(settings).returns(Promise.resolve());
|
||||
MozSettingsShim.set(settings).then(() => {
|
||||
sinon.assert.calledWith(lockStub.set, settings);
|
||||
}).then(done, done);
|
||||
});
|
||||
|
||||
test('returns reject while error', function(done) {
|
||||
var error = new Error('moz settings set error');
|
||||
lockStub.set.returns(Promise.reject(error));
|
||||
MozSettingsShim.set().then(() => {
|
||||
throw new Error('Should not be resolved');
|
||||
}, (result) => {
|
||||
sinon.assert.called(lockStub.set);
|
||||
assert.equal(result, error);
|
||||
}).then(done, done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
315
gaia/apps/sms/services/test/unit/threads_test.js
Executable file
315
gaia/apps/sms/services/test/unit/threads_test.js
Executable file
|
|
@ -0,0 +1,315 @@
|
|||
/*global Thread, Threads, MockMessages, Drafts, MocksHelper */
|
||||
|
||||
'use strict';
|
||||
|
||||
require('/services/js/threads.js');
|
||||
|
||||
require('/services/test/unit/mock_drafts.js');
|
||||
require('/views/shared/test/unit/mock_messages.js');
|
||||
|
||||
|
||||
var mocksHelperForThreadsTest = new MocksHelper([
|
||||
'Drafts'
|
||||
]).init();
|
||||
|
||||
function assertDeepEqual(test, expected) {
|
||||
for (var key in expected) {
|
||||
assert.deepEqual(test[key], expected[key]);
|
||||
}
|
||||
}
|
||||
|
||||
suite('Threads', function() {
|
||||
var message;
|
||||
|
||||
mocksHelperForThreadsTest.attachTestHelpers();
|
||||
|
||||
setup(function() {
|
||||
// Create a message with read status 'true'
|
||||
message = MockMessages.sms();
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
Threads.clear();
|
||||
});
|
||||
|
||||
suite('create', function() {
|
||||
test('Thread.fromMessage, Thread.fromDraft', function() {
|
||||
assert.ok(Thread.fromMessage);
|
||||
assert.ok(Thread.fromDraft);
|
||||
});
|
||||
|
||||
test(' > create with unread status in options', function() {
|
||||
var options = { unread: true };
|
||||
var thread = Thread.create(message, options);
|
||||
|
||||
assert.equal(thread.unreadCount, 1);
|
||||
});
|
||||
|
||||
test(' > create without options', function() {
|
||||
var thread = Thread.create(message);
|
||||
assert.equal(thread.unreadCount, 0);
|
||||
});
|
||||
|
||||
test(' > create with read status in options', function() {
|
||||
var options = { read: true };
|
||||
var thread = Thread.create(message, options);
|
||||
assert.equal(thread.unreadCount, 0);
|
||||
});
|
||||
|
||||
test(' > create from SMS sended', function() {
|
||||
var options = { read: true };
|
||||
message.delivery = 'sent';
|
||||
var thread = Thread.create(message, options);
|
||||
assert.equal(thread.participants.length, 1);
|
||||
assert.equal(thread.participants[0], message.receiver);
|
||||
});
|
||||
|
||||
test(' > create from SMS received', function() {
|
||||
var options = { unread: true };
|
||||
message.delivery = 'received';
|
||||
var thread = Thread.create(message, options);
|
||||
assert.equal(thread.participants.length, 1);
|
||||
assert.equal(thread.participants[0], message.sender);
|
||||
});
|
||||
|
||||
test(' > create from MMS sending', function() {
|
||||
var options = { read: true };
|
||||
var mms = MockMessages.mms();
|
||||
mms.delivery = 'sent';
|
||||
var thread = Thread.create(mms, options);
|
||||
assert.equal(thread.participants, mms.receivers);
|
||||
});
|
||||
|
||||
test(' > create from MMS received', function() {
|
||||
var options = { unread: true };
|
||||
var mms = MockMessages.mms();
|
||||
mms.delivery = 'received';
|
||||
var thread = Thread.create(mms, options);
|
||||
assert.equal(thread.participants.length, 1);
|
||||
assert.equal(thread.participants[0], message.sender);
|
||||
});
|
||||
|
||||
test(' > create from Draft with null threadId', function() {
|
||||
var now = Date.now();
|
||||
var thread = Thread.create({
|
||||
id: 1,
|
||||
recipients: ['555'],
|
||||
content: ['This is a new draft for thread 44'],
|
||||
subject: 'This is a subject',
|
||||
timestamp: now,
|
||||
threadId: null,
|
||||
type: 'sms'
|
||||
});
|
||||
|
||||
assertDeepEqual(thread, {
|
||||
// id was used
|
||||
id: 1,
|
||||
participants: ['555'],
|
||||
body: 'This is a new draft for thread 44',
|
||||
timestamp: new Date(now),
|
||||
unreadCount: 0,
|
||||
lastMessageSubject: undefined,
|
||||
lastMessageType: 'sms',
|
||||
messages: new Map()
|
||||
});
|
||||
});
|
||||
|
||||
test(' > create from Draft with a threadId', function() {
|
||||
var now = Date.now();
|
||||
var thread = Thread.create({
|
||||
id: 1,
|
||||
recipients: ['555'],
|
||||
content: ['This is a new draft for thread 44'],
|
||||
subject: 'This is a subject',
|
||||
timestamp: now,
|
||||
threadId: 44,
|
||||
type: 'sms'
|
||||
});
|
||||
|
||||
assertDeepEqual(thread, {
|
||||
// threadId was used
|
||||
id: 44,
|
||||
participants: ['555'],
|
||||
body: 'This is a new draft for thread 44',
|
||||
timestamp: new Date(now),
|
||||
unreadCount: 0,
|
||||
lastMessageSubject: undefined,
|
||||
lastMessageType: 'sms',
|
||||
messages: new Map()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Collection', function() {
|
||||
test('is like a Map', function() {
|
||||
assert.ok(Threads);
|
||||
assert.isFunction(Threads.set);
|
||||
assert.isFunction(Threads.get);
|
||||
assert.isFunction(Threads.has);
|
||||
assert.isFunction(Threads.delete);
|
||||
assert.isFunction(Threads.clear);
|
||||
assert.isNumber(Threads.size);
|
||||
});
|
||||
|
||||
test('Threads.set(key, val)', function() {
|
||||
Threads.set(1, {});
|
||||
assertDeepEqual(Threads.get(1), {
|
||||
body: undefined,
|
||||
id: undefined,
|
||||
lastMessageSubject: undefined,
|
||||
lastMessageType: undefined,
|
||||
participants: undefined,
|
||||
timestamp: undefined,
|
||||
unreadCount: undefined,
|
||||
messages: new Map()
|
||||
});
|
||||
assert.equal(Threads.size, 1);
|
||||
});
|
||||
|
||||
test('Threads.get(key)', function() {
|
||||
Threads.set(1, {});
|
||||
var value = Threads.get(1);
|
||||
assert.ok(value.messages instanceof Map);
|
||||
assert.equal(Threads.size, 1);
|
||||
});
|
||||
|
||||
test('Threads.has(key)', function() {
|
||||
assert.equal(Threads.has(1), false);
|
||||
Threads.set(1, {});
|
||||
assert.equal(Threads.has(1), true);
|
||||
});
|
||||
|
||||
test('Threads.delete()', function() {
|
||||
Threads.set(1, {});
|
||||
assert.equal(Threads.has(1), true);
|
||||
assert.equal(Threads.size, 1);
|
||||
Threads.delete(1);
|
||||
assert.equal(Threads.has(1), false);
|
||||
assert.equal(Threads.size, 0);
|
||||
});
|
||||
|
||||
test('Threads.delete() calls Drafts.delete()', function() {
|
||||
this.sinon.spy(Drafts, 'delete');
|
||||
this.sinon.stub(Drafts, 'store');
|
||||
|
||||
this.sinon.stub(Threads, 'get').returns({
|
||||
id: 1,
|
||||
getDraft: () => { return { threadId: 1 }; }
|
||||
});
|
||||
|
||||
Threads.delete(1);
|
||||
|
||||
sinon.assert.calledWith(Drafts.delete, { threadId: 1 });
|
||||
sinon.assert.callOrder(Drafts.delete, Drafts.store);
|
||||
});
|
||||
|
||||
suite('Threads.registerMessage/unregisterMessage', function () {
|
||||
setup(function() {
|
||||
this.sinon.spy(Threads, 'get');
|
||||
});
|
||||
test('thread existed', function() {
|
||||
Threads.set(1, {});
|
||||
Threads.registerMessage(message);
|
||||
assert.equal(Threads.get(1).messages.get(message.id), message);
|
||||
assert.equal(Threads.Messages.get(message.id), message);
|
||||
|
||||
// unregister the message
|
||||
Threads.unregisterMessage(message.id);
|
||||
assert.isUndefined(Threads.get(1).messages.get(message.id));
|
||||
});
|
||||
|
||||
test('thread does not exist', function() {
|
||||
Threads.registerMessage(message);
|
||||
assert.isTrue(Threads.has(1));
|
||||
assert.equal(Threads.get(1).messages.get(message.id), message);
|
||||
assert.equal(Threads.Messages.get(message.id), message);
|
||||
});
|
||||
|
||||
test('remove the message that does not exist in Threads', function() {
|
||||
Threads.unregisterMessage(message);
|
||||
sinon.assert.notCalled(Threads.get);
|
||||
assert.isNull(Threads.Messages.get(message.id));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
suite('Thread', function() {
|
||||
var date = new Date();
|
||||
var fixture = {
|
||||
id: 1,
|
||||
participants: ['555'],
|
||||
lastMessageType: 'sms',
|
||||
body: 'Hello 555',
|
||||
timestamp: date,
|
||||
unreadCount: 0
|
||||
};
|
||||
|
||||
mocksHelperForThreadsTest.attachTestHelpers();
|
||||
|
||||
teardown(function() {
|
||||
Threads.clear();
|
||||
});
|
||||
|
||||
setup(function() {
|
||||
Threads.set(1, fixture);
|
||||
});
|
||||
|
||||
test('Thread object', function() {
|
||||
var thread = new Thread(fixture);
|
||||
|
||||
assertDeepEqual(thread, {
|
||||
id: 1,
|
||||
participants: ['555'],
|
||||
lastMessageSubject: undefined,
|
||||
lastMessageType: 'sms',
|
||||
body: 'Hello 555',
|
||||
timestamp: date,
|
||||
unreadCount: 0,
|
||||
messages: new Map()
|
||||
});
|
||||
});
|
||||
|
||||
test('thread has draft', function() {
|
||||
var draft = {
|
||||
id: 101,
|
||||
recipients: ['555'],
|
||||
content: ['This is a new draft for thread 1'],
|
||||
subject: 'This is a subject',
|
||||
timestamp: 2,
|
||||
threadId: 1,
|
||||
type: 'sms'
|
||||
};
|
||||
|
||||
this.sinon.stub(Drafts, 'byThreadId').withArgs(1).returns(draft);
|
||||
|
||||
Threads.set(1, {
|
||||
id: 1,
|
||||
participants: ['555'],
|
||||
lastMessageType: 'sms',
|
||||
body: 'Hello 555',
|
||||
timestamp: date,
|
||||
unreadCount: 0,
|
||||
messages: new Map()
|
||||
});
|
||||
|
||||
assert.deepEqual(Threads.get(1).getDraft(), draft);
|
||||
});
|
||||
|
||||
test('thread does not have draft', function() {
|
||||
this.sinon.stub(Drafts, 'byThreadId').withArgs(1).returns(null);
|
||||
|
||||
Threads.set(1, {
|
||||
id: 1,
|
||||
participants: ['555'],
|
||||
lastMessageType: 'sms',
|
||||
body: 'Hello 555',
|
||||
timestamp: date,
|
||||
unreadCount: 0,
|
||||
messages: new Map()
|
||||
});
|
||||
|
||||
assert.isNull(Threads.get(1).getDraft());
|
||||
});
|
||||
|
||||
});
|
||||
Loading…
Reference in a new issue