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:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,3 @@
{
"extends": "../../../test/unit/.jshintrc"
}

View 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);
});
});
});

View 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);
});
});
});
});

View file

@ -0,0 +1,13 @@
/* exported MockActivityClient */
(function(exports) {
'use strict';
exports.MockActivityClient = {
init: () => {},
postResult: () => Promise.resolve(),
postError: () => Promise.resolve(),
hasPendingRequest: () => false,
on: () => {}
};
})(window);

View file

@ -0,0 +1,9 @@
/* exported MockActivityShim */
(function(exports) {
'use strict';
exports.MockActivityShim = {
init: () => {}
};
})(window);

View 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);
});
});

View 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);
});
});
});

View 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);
});
});
});

View file

@ -0,0 +1,10 @@
/* exported MockConversationClient */
(function(exports) {
'use strict';
exports.MockConversationClient = {
init: () => {},
getAllConversations: () => Promise.resolve()
};
})(window);

View 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);
});
});
});
});

File diff suppressed because it is too large Load diff

View 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);
});
});
});
});

View 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);
});
});
});

View file

@ -0,0 +1,10 @@
/* exported MockMessagingClient */
'use strict';
var MockMessagingClient = {
init: () => {},
sendSMS: () => {},
sendMMS: () => {},
resendMessage: () => {},
retrieveMMS: () => {}
};

View file

@ -0,0 +1,13 @@
/* exported bridge */
(function(exports) {
'use strict';
exports.Mockbridge = {
client: () => {},
service: () => {}
};
exports.MockstreamService = {};
exports.MockstreamClient = {};
})(window);

View 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);
}

View 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() {}
};

View 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
};

View 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() {}
};

View file

@ -0,0 +1,5 @@
/*exported MockSharedWorker */
'use strict';
function MockSharedWorker() {}

View 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 = () => {};

View file

@ -0,0 +1,10 @@
/* exported MockMozMobileConnectionsClient */
(function(exports) {
'use strict';
exports.MockMozMobileConnectionsClient = {
init: () => {},
switchMmsSimHandler: () => {}
};
})(window);

View file

@ -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
);
});
});
});

View file

@ -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();
});
});
});
});

View file

@ -0,0 +1,9 @@
/* exported MockMozMobileMessageClient */
(function(exports) {
'use strict';
exports.MockMozMobileMessageClient = {
forApp: () => {}
};
})(window);

View file

@ -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');
});
});
});

View file

@ -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);
});
});
});
});

View file

@ -0,0 +1,6 @@
/*exported MockMozSettingsShim */
'use strict';
var MockMozSettingsShim = {
set: () => {}
};

View 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);
});
});
});
});

View 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());
});
});